资讯专栏INFORMATION COLUMN

函数表达式

zebrayoung / 785人阅读

摘要:可以使用函数表达式的形式将结果赋值给变量即使把函数赋值给了另一个变量函数的名字依然有效所以递归依然可以正常运行。

最近一直在复习巩固知识,以前写的笔记现在也在看,为了勘误之前的理解,加深印象,把markdown上所写的又拿下来再叙述一遍,章节顺序都是按照当时看《高程》的顺序整理的,如有不对的地方还请拍砖指教,感谢!

========================================================================

函数表达式

递归、闭包、模仿块级作用域、私有变量

定义函数有两种方式,函数声明和函数表达式,函数声明的语法是:

    function functionName(arguments){
        //函数体;
    }   

函数声明的一个重要特征就是函数声明提升,在执行代码之前,都会读取函数声明,以下的例子不会出现错误:

    sayHi();
    function sayHi(){
        alert("hi");
    }

函数表达式的语法是:

    var functionName = function(arguments){
        //函数体;
    }
这种情况下创建的函数是匿名函数,匿名函数的name属性是空字符串,如果以这样的方式调用会出现错误:
    sayHi();//错误,函数还不存在;
    var sayHi = function(){
        alert("hi");    
    }
如果使用if...else语句判断一个条件,执行同一个functionName的函数声明,JavaScript的引擎会尝试修正错误,将其转换为合理的状态,而修正的机制不一样,大多数会在condition为true时返回第一个声明,少部分会为第二个声明,因此在if...else中最好使用函数表达式,它会根据condition赋值给变量返回正确的函数结果。通过condition之后,函数赋值给变量,再选择执行函数。
    var sayHi;
    if(condition){
        sayHi = function(){
            alert("Hi");
        }
    }else{
        sayHi = function(){
            alert("Yo!");
        }
    }
    sayHi();

能够创建函数赋值给变量,当然也可以把函数作为其它函数的值返回。以下:

    function compare(propertyName){
        return function(object1,object2){
            var value1 = object1[propertyName];
            var value2 = object2[propertyName];
            if(value1 < value2){
                return -1;
            }else if(value1 > value2){
                return 1;
            }else{
                return 0;
            }
        }
    }

递归
递归函数即自己调用自己,一个阶乘函数:

    function sum(num){
        if(num<=1){
            return 1;
        }else{
            return num * sum(num-1);
        }
    }

这样一看好像没有问题,但是如果把函数存在变量中,然后改变函数的引用为null,那么在执行函数就会出错。

    var other = sum;
    sum = null;
    other(4);//sum isn"t a function;

使用callee()方法可以解耦,callee保存的是拥有这个arguments参数的函数。

    function sum(num){
        if(num<=1){
            return 1;
        }else{
            return num * arguments.callee(num-1);
        }
    }   

而在strict模式下,callee是不可用的,并且考虑到代码安全的因素callee和caller已经被弃用了,所以尽量不要使用这个方法。可以使用函数表达式的形式,将结果赋值给变量,即使把函数赋值给了另一个变量,函数的名字依然有效,所以递归依然可以正常运行。

    var factorial = function sum(num){
        if(num<=1){
            return 1;
        }else{
            return num * sum(num-1);
        }
    }
    var other = factorial;
    factorial = null;
    other(7);//

ES6中关于尾递归的优化

   函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"-------摘自阮一峰老师的博客

    function fac(n,total=1){
        if(n == 1){
            return total;
        }
        return fac(n - 1,n * total);
    }

2.闭包

        
    执行环境定义了所有变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有与之关联的变量对象,环境中定义的所以变量和函数都保存在这个对象中。当代码在执行环境中运行时,会创建变量对象的一个作用域链(保证对执行环境有权访问的所有变量和函数的有序访问)。
    当某个函数被调用时,会创建一个执行环境及相应的作用域链,然后arguments和其他命名参数的值来初始化函数的活动对象,但在作用域链中,外部函数的活动对象处于第二位,在它之外的函数处于第三位,...直至作为作用域终点的全局执行环境。
    function compare(value1,value2){
        if(value1 < value2){
            return -1;
        }else if(value1 > value2){
            return 1;
        }else{
            return 0;
        }
    }
    var result = compare(5,10);
    在这段代码中,当调用compare()函数时,会创建一个包含arguments、value1、value2的活动对象,而全局执行环境的变量对象result、compare则在这个作用域链的第二位。
    每个执行环境都有一个表示变量的对象-----变量对象,全局环境的对象会一直存在,而compare函数里的局部环境的变量对象,只在函数执行时存在,创建compare函数时,会创建一个预先包含了全局变量对象compare、result的作用域链,此后又有一个活动对象被创建并被推入作用域链的前端。compare()的活动对象是arguments[5,10],value1 : 5,value2 : 10。所以此时的作用域链包含了本地活动对象和全局变量对象。作用域链是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

一般来说,当函数执行完毕后,局部活动对象就会销毁,内存中仅保存全局作用域,但是闭包的情况有所不同。
在一个函数内部定义的函数,会将外部函数的活动对象添加到它的作用域链中。

    function createCompare(propertyName){
        return function(object1,object2){
            var value1 = object1[propertyName];
            var value2 = object2[propertyName];
            if(value1 < value2){
                return -1;
            }else if(value1 > value2){
                return 1;
            }else{
                return 0;
            }
        }
    }
    var compare = createCompare("name");
    var result = compare({name : "Nicholas"},{name : "Greg"});
匿名函数在被返回之后,它的作用域链被初始化为外部函数的活动对象以及全局变量对象,因此它可以访问外部函数的所有变量,并且在外部函数执行完毕后,外部函数的作用域链会被销毁,但是它的活动对象仍然保存着,因为此时匿名函数还在引用这个活动对象arguments:"name",propertyName : name。如果:
    compare = null;

那么这时就会解除对匿名函数的引用,以便释放内存。

1)闭包与变量
闭包的一个副作用就是只能取得外部函数中任何变量的最后一个值,以下例子可以说明:

    function create(){
        var result = new Array();
        for(var i = 0;i<10;i++){
            result[i] = function(){
                return i;
            }
        }
        return result;
    }
    console.log(result);

在这个函数内部返回result时,它可以引用外部函数的活动对象以及全局变量对象,而当create()执行完之后,这个活动对象仍然在被引用,此时i已经变为了10,所以result只会返回同一个i值,即i=10;可以修改为:

    function create(){
        var result = new Array();
        for(var i = 0;i<10;i++){
            result[i] = function(num){
                return function(){
                    return num;
                }
            }(i);
        }   
        return result;
    }
    console.log(create());

修改后的函数闭包得到的值并不直接赋值给result,而是通过闭包,使这个匿名函数的内部再返回一个匿名函数,这个匿名函数可以访问外部的活动对象num,再通过给内部的函数传递变量i,赋值给num,所以最后可以得到function(1)、function(2)...function(10),并且他们的内部属性[[scope]]还会包含对应的i值。

2)关于this对象
匿名函数的执行环境具有全局性,因此它的this指向一般指向window

    var name = "The Window";
    var object = {
        name : "My object",
        getName : function(){
            return function(){
                return this.name;
            };
        }
    }
    alert(object.getName()());//The Window

以上例子中,匿名函数的的this指向的是window,所以得到的是"The Window",
因为this对象其实是函数执行时的上下文,与如何定义函数并没有关系,Object.getName(),指向的是Object对象,但是继续调用匿名函数,显然this指向的不是这个对象,此时是全局环境下调用的这个函数,因此this.name为"The Window".如果在定义匿名函数之前把this对象赋值给一个变量,那么调用匿名函数时会改变this的指向。

    var name = "The Window";
    var object = {
        name : "My object",
        getName : function(){
            var _this = this; 
            return function(){
                return _this.name;
            };
        }
    }
    alert(object.getName()());//My object 

3)内存泄漏
在IE9之前的浏览器中,IE中有一部分对象并不是原生对象,其DOM、BOM中的对象就是以COM对象的形式实现的,而针对COM对象垃圾回收机制是引用计数,在闭包中很容易出现循环引用的问题,因此可能会出现内存泄漏。

    function Handle(){
        var elment = document.getElementById("someElement");
        element.onclick = function(){
            alert(element.id);
        }
    }

只要匿名函数存在,对element的引用数也至少为1,因此它占用的内存永远都不会回收,可以做以下的修改:

    function Handle(){
        var elment = document.getElementById("someElement");
        var id = element.id;
        element.onclick = function(){
            alert(id);
        }
        element = null;
    }

3)模仿块级作用域

JS没有块级作用域的概念,也就是说在块语句中定义的变量其实是在包含函数中创建的。以下例子可以说明:
    function outNumbers(){
        for(var i = 0;i<10;i++){
            alert(i);
        }
        var i;
        alert(i);//10
    }

在for循环中i从0-9,i=10会直接给后一个声明赋值,所以会再次计数,当然在ES6中,如果使用let或const声明,for语句就会形成块级作用域。这一节主要讨论的是利用匿名函数自执行形成块级作用域。

如果是匿名函数自执行,那么在它的内部自然就会形成块级作用域,例如:
    (function(){
        //块级作用域;
    })();
如果采用函数声明的形式创建函数表达式:
    var someFunction = function(){
        //块级作用域:
    };
    someFunction();

像第一个例子中创建的函数可以利用匿名函数自执行修改为:

    function outNumbers(court){
        (function(){
            for(var i = 0;i

匿名函数自执行并不影响闭包的特性,court仍然可以作为外部函数的活动对象被引用。通过模仿块级作用域可以避免全局变量被污染以及函数的命名冲突,而且可以减少闭包占用的内存问题,因为没有指向匿名函数的引用,只要函数执行完毕,就可以立即销毁作用域链了。

4)私有变量

    任何在函数中定义的变量都是私有变量,外部函数不能访问到这些变量,如果在函数内部创建一个闭包,那么闭包可以利用外部函数的活动对象,即外部函数的私有变量,利用这一点可以创建用于访问私有变量的特权方法。以下两种方式都可以为自定义类型创建私有变量和特权方法。
    第一种是在构造函数中定义特权方法,方法如下:
    function MyObject(){
        var private = 10;
        function SomeFun = {
            return false;
        }
        this.public = function(){
            alert(private++);
            return someFun();
        };
    }
    var person = new MyObject();
    console.log(person.public());
    在这个方法中必须要创建构造函数,在实例上才可以调用这个特权方法从而访问私有变量,但是这样做会带来构造函数实例标识符重解析以及不同的作用域链,这一点和构造函数模式相同。

①.静态私有变量

    第二种方法为通过私有作用域定于私有变量和函数,依靠原型模式,如下:
    (function(){
        var private = 10;
        function PrivateFun(){
            return false;
        }
        MyObject = function(){
        
        }
        MyObject.prototype.public = function(){
            private++;
            return PrivateFun();
        }
    })();
使用函数表达式是因为如果使用函数声明,那么function内部为块级作用域,无法实现实例化对象的目的,从而也就无法达到访问函数内部私有变量和私有函数的目的,并且在匿名函数内部的构造函数也不能声明,这样以来变量就成为了全局变量,能够在这个块级作用域之外被使用到。示例:
    (function(){
        var name = "";
        Person = function(value){
            name = value;
        };
        Person.prototype.setName = function(value){
            name = value;
        };
        Person.prototype.getName = function(){
            return name;
        };
    })();
    var person1 = new Person("Nicholas");
    var person2 = new Person("Michael");
    alert(person1.getName());//Michael
    alert(person2.getName());//Michael

Person构造函数与setName()和getName()一样,都可以访问匿名函数的私有变量name,变量name就成了静态的、由所有实例共享的属性。但是由于原型链的特性,一旦更改了name属性,那么所有实例都会受到影响。

闭包和私有变量方法的一个明显不足之处就是,他们都会多查找作用域链的一个层次,这显然会在一定程度上影响查找速度。

②.模块模式

如果是只有一个实例的对象访问私有变量和函数,在必须以对象的形式创建的前提下,而且需要以某些数据初始化,同时还要公开一些能够访问这些私有数据的方法,可以采用这种方式:
    var singleton = function(){
        var private = 10;
        function privateFun(){
            return false;
        }
        return{
            public : true,
            publicMethod : function(){
                private++;
                return privateFun();
            }
        }
    }();这一个()相当于  singleton();
    //当使用公有办法时,singleton.publicMethod();

为什么使用函数声明?

这仅仅是一个单例,所以不能将它实例化,仅将函数的返回值以对象的形式返回给变量就可以使用公有办法访问私有变量、函数。

为什么在内部以对象形式返回?

如果采用this对象形式访问,这样相当于构造函数结构,并且在函数声明的前提下,this对象为window(严格模式下为undefined)。

下面的这个例子说明模块模式可以应用的场景:

    var application = function(){
        //私有变量和函数
        var components = new Array();
        //初始化
        components.push(new BaseComponent());
        //公共
        return {
            getComponent : function(){
                return components.length;  
            },
            registerComponent : function(component){
                if(typeof component == "object"){
                    components.push(component);
                }
            }
        }
    }();

在这个单例的公共接口中,前者可以返回已注册的组件数目,后者用于注册新组件。

③.增强的模块模式

    在返回对象之前加入对其增强的代码,单例是自定义类型的实例,同时还必须添加某些属性或方法对其加强的情况下,可以使用增强模块模式。application的例子可以改写为:
    var application  = function(){
        var components = new Array();
        components.push(new BaseComponent());
        //创建一个自定义类型实例,作为application的局部副本使用
        var app = new BaseComponent();
        app.getComponent = function(){
            return components.length;  
        };
        app.registerComponent = function(component){
            if(typeof component == "object"){
                components.push(component);
            }
        };
        //返回副本
       return app; 
    }();

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82471.html

相关文章

  • 函数声明,函数达式,匿名函数,立即执行函数的简单分析

    摘要:第一种方法是上面已经说到的匿名函数表达式,即将匿名函数赋值给一个变量,然后通过变量名去调用。因为函数声明是不能被执行符号执行的,除非通过函数名调用,只有表达式才能被执行符号执行匿名函数又没有函数名,另外的办法就是把它变成表达式。 函数声明 function funcName(){ }; console.log(funcName); // 打印结果是funcName这个函数体。 声明一...

    spademan 评论0 收藏0
  • JavaScript 函数的定义

    摘要:关键词必须是小写的,并且必须以与函数名称相同的大小写来调用函数。当调用函数时,这些标识符则指代传入函数的实参。函数表达式其实是忽略函数名称的,并且不可以使用函数名这种形式调用函数。注意构造函数无法指定函数名称,它创建的是一个匿名函数。 一、关于函数 JavaScript函数是指一个特定代码块,可能包含多条语句,可以通过名字来供其他语句调用以执行函数包含的代码语句。 比如我们有一个特定的...

    mudiyouyou 评论0 收藏0
  • 「JavaScript」函数声明和函数达式

    摘要:函数表达式的值是在运行时确定,并且在表达式赋值完成后,该函数才能调用 1.定义 在javascript中我们定义函数有以下两种方式: 函数声明 function say(){ console.log(函数声明); } 函数表达式 var say = function(){ console.log(函数表达式); } 2.实例解析 在平时开发中,...

    Kerr1Gan 评论0 收藏0
  • 深入理解JavaScript系列2:揭秘命名函数达式

    摘要:标识符有效性正是导致函数语句与函数表达式不同的关键所在下一小节我们将会展示命名函数表达式的具体行为。归根结底,只有给函数表达式取个名字,才是最稳妥的办法,也就是使用命名函数表达式。 前言 网上还没用发现有人对命名函数表达式进去重复深入的讨论,正因为如此,网上出现了各种各样的误解,本文将从原理和实践两个方面来探讨JavaScript关于命名函数表达式的优缺点。简单的说,命名函数表达式只有...

    chenjiang3 评论0 收藏0
  • js函数探索

    摘要:关于构造函数有几点需要特别注意构造函数允许在运行时动态的创建并编译函数。而函数本身的表示该函数的形参。每一个函数都包含不同的原型对象,当将函数用作构造函数的时候,新创建的对象会从原型对象上继承属性。 该文章以收录: 《JavaScript深入探索之路》 前言 函数是这样的一段JavaScript代码,它只定义一次,但是可能被执行或调用任意次。你可能已经从诸如子例程或者过程这些名字里...

    thursday 评论0 收藏0
  • JavaScript—— 函数声明和函数达式有什么不同吗?

    摘要:函数声明和函数表达式的区别函数声明只能出现在程序或函数体内。所以,在等语义为语句的代码块中存在函数声明,由于函数提升特性,会破坏掉原本的语义。 这篇谈一下JS函数声明与函数表达式的区别及要注意的地方: 函数声明主要有两种类型: 函数声明 function fn() {}; 函数表达式 var fn = function () {}; 这两种函数创建方式...

    FleyX 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<