第07章 - 函数表达式

作者: 勤劳的悄悄 | 来源:发表于2016-02-18 18:13 被阅读22次

    常规方式定义函数

    定义函数有两种方式,第一种方式为常规声明方式,该方式下函数可以先使用,后声明,即"函数声明提升"特性。

    //函数提升
    
    sayHi();
    
    function sayHi(){
        alert("Hi!");
    }
    

    常规声明方式方式定义的函数有 name 属性,可以获取函数的名字

    // name 属性
    
    function functionName(arg0, arg1, arg2) {
    //function body
    }
    
    alert(functionName.name); //"functionName"
    

    匿名方式定义函数

    第二种方式为匿名函数方式,即函数没有名字。但是函数对象可以赋值给变量,可以作为参数,也可以作为返回值

    var functionName = function(arg0, arg1, arg2){
        //function body
    };
    

    7.1 递归

    递归就是函数自己调用自己,下面求阶乘的函数是个典型的递归函数

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

    但是上面这种写法在 Javascirpt 中可能会出现错误,因为在 Javascript 中,函数的名字可能发生变化

    var anotherFactorial = factorial;
    
    factorial = null;//这个名字不再指向函数对象
    alert(anotherFactorial(4)); //出错
    

    可以用 arguments.callee 来解决这个问题,arguments.callee 始终指向当前正在执行的函数对象

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

    在有些情况下,arguments.callee 并不存在,可以用命名函数表达式来实现递归

    //在函数定义外部包裹括号,可以获取函数对象
    var factorial = (function f(num){
        if (num <= 1){
            return 1;
        } else {
            return num * f(num-1);
        }
    });
    

    7.2 闭包

    闭包也是一个函数,但是这个函数保存了另外一个函数作用域中的变量。通常创建闭包的方式

    • 在一个函数中创建另外一个函数
    • 内部函数访问了外部函数中的局部变量
    • 外部函数执行完毕后被销毁,而内部函数对象因为有更长的生命周期而被保留,因而变成了闭包
    function createComparisonFunction(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;
            }
        };
    }
    

    执行环境

    • 执行环境是一组数据,这组数据定义了代码运行的边界,防止出现混乱。
    • 执行环境在函数执行时候被创建,函数执行完毕后,被销毁。
    • window 对象是一个全局执行环境,不会被销毁。

    变量对象

    每一个执行环境都有一个对应的变量对象,变量对象中保存着函数能访问到的局部变量和函数的名字。

    作用域链

    每个执行环境都会包含一个作用域链,作用域链的顶端指向当前环境的变量对象,下一个指向外部环境的变量对象,最后指向全局环境的变量对象。作用域链保存在函数对象的 [[Scope]] 属性中

    查找变量

    函数查找一个变量的时候,先从作用域链顶端的变量对象中查找,找不到则到下一个变量对象中查找,一直查找到最后一个全局环境的变量对象为止。

    作用域链创建的流程

    函数运行时,会:

    • 先创建函数自己的局部执行环境
    • 向执行环境中加入一个默认的作用域链,该作用域链只包含全局环境的变量对象
    • 将函数自己的变量对象添加到作用域链的顶端

    下面的代码运行后,会产生如图的数据结构

    function compare(value1, value2){
        if (value1 < value2){
            return -1;
        } else if (value1 > value2){
            return 1;
        } else {
            return 0;
        }
    }
    
    var result = compare(5, 10);
    
    07-01.jpg

    闭包为什么能保存外部环境中的变量

    当内部环境访问外部环境的时候,外部环境的变量对象会被添加到内部环境的作用域链中,因此内部环境可以访问外部环境。

    一般情况下,内部环境总是先于外部环境执行完毕,因此先被销毁

    特殊的情况下,外部环境先被销毁,而内部环境被长期保留。这时候,由于内部环境的作用域链中引用了外部环境的变量对象,因此外部环境的变量对象会被保留下来,被销毁的只有外部环境本身和他的作用域链。

    07-02.jpg

    适度使用闭包并及时释放闭包占用的内存

    • 因为闭包会阻止外部环境销毁其变量对象,因此闭包会占用额外内存,过度的闭包会导致性能下降
    • 可以闭包使用完毕后,及时将其设置为 null ,手动释放内存

    7.2.1 闭包与变量的状态

    闭包中引用的外部变量,其值可能会在闭包创建以后继续发生变化,而闭包并不能记住创建时外部变量的状态。

    function createFunctions(){
        var result = new Array();
        for (var i=0; i < 10; i++){
            //所有闭包引用的都是同一个外部变量对象中的同一个 i ,因此只能得到最后的 i 的状态
            result[i] = function(){
                return i;
            };
        }
        return result;
    }
    

    要想保持闭包创建时外部变量的状态,可以将外部变量作为闭包的参数传进去,这样闭包中只有对本地变量使用,不受外部变量变化的影响

    function createFunctions(){
        var result = new Array();
    
        for (var i=0; i < 10; i++){
            result[i] = function(num){
                return function(){
                    return num;
                };
            }(i);//将状态作为参数穿进去
        }
    
        return result;
    }
    

    7.2.2 关于 this 指针

    • 在全局作用于下,this 指向 window 对象
    • 在方法中,this 指向方法所在的对象
    • 在闭包中,this 指向 window 对象

    在下面的例子中,object.getNameFunc()() 将返回一个闭包。这是因为,最终返回的匿名函数的外部环境已经不存在了,因此匿名函数变成了闭包。所以,其中的 this 将指向 window 对象

    var name = "The Window";
    
    var object = {
        name : "My Object",
        getNameFunc : function(){
            return function(){
                return this.name;
            };
        }
    };
    
    alert(object.getNameFunc()()); //"The Window" (in non-strict mode)
    

    要想在这种情况下让 this 指向对象本身,可以先用一个临时变量保存 this ,然后利用闭包的特性引用临时变量,而不是直接使用 this

    var name = "The Window";
    
    var object = {
        name : "My Object",
        getNameFunc : function(){
            //用一个临时变量保存 this
            var that = this;
    
            return function(){
                //在内部引用临时变量
                return that.name;
            };
        }
    };
    
    alert(object.getNameFunc()()); //"My Object"
    

    7.2.3 闭包的内存泄漏:

    一般情况下 HTML 对象的事件属性会指向一个函数对象, HTML 对象被销毁,函数对象也会被销毁。

    某些特殊情况下,事件处理函数中,又反过来引用了 HTML 对象,事件处理函数就变成了一个闭包。这是一种循环引用,导致 HTML 对象将不会被销毁

    function assignHandler(){
        var element = document.getElementById("someElement");
        //事件处理函数引用了外部变量,且生存周期比外部环境长,因此变成了闭包
        //闭包中引用的对象将不会被释放
        element.onclick = function(){
            alert(element.id);
        };
    }
    

    解决办法:可以先将对象的属性赋值给外部函数的一个局部变量,闭包中引用这个局部变量而不引用 HTML 对象。 HTML 对象使用完成后应该赋值 null

    function assignHandler(){
        var element = document.getElementById("someElement");
        //将 HTML 对象复制给一个局部变量,闭包中引用这个局部变量
        var id = element.id;    
        element.onclick = function(){
            alert(id);
        };
    
        //释放这个 HTML 对象 
        element = null;
    }
    

    7.3 块级作用域

    Javascript 中没有块级作用域的概念。下面的代码中,临时变量 i 超出了作用于仍然存在

    function outputNumbers(count){
        for (var i=0; i < count; i++){
            alert(i);
        }
    
        alert(i); //count
    }
    

    可以将使用临时变量的代码放到块级作用域中,这样可以防止命名空间污染,减少内存占用

    function outputNumbers(count){
        (function () {
            for (var i=0; i < count; i++){
                alert(i);
            }
        })();
    
        alert(i); //causes an error
    }
    

    7.4 私有变量

    函数本身就具有封装性,在函数内部定义的变量和函数都是私有的,外部不能访问

    ///外部看不见 num1 num2 等变量
    function add(num1, num2){
        var sum = num1 + num2;
        return sum;
    }
    

    访问接口

    完全私有的变量是没有用处的,可以留一个公共接口供外部访问

    function MyObject(){
        //私有变量
        var privateVariable = 10;
    
        //私有函数
        function privateFunction(){
            return false;
        }
    
        //特权方法,外部可以访问
        this.publicMethod = function (){
            privateVariable++;
            return privateFunction();
        };
    }
    

    实例成员私有模式

    这是最基本的模式,每个实例都会创建自己独立的成员,不同实例之间互不影响;通过特权函数可以访问实例的私有成员。

    缺点是:特权函数也会在每个实例创建一份,浪费资源。

    function Person(name){
    
        this.getName = function(){
            return name;
        };
    
        this.setName = function (value) {
            name = value;
        };
    }
    
    var person = new Person("Nicholas");
    alert(person.getName()); //"Nicholas"
    
    person.setName("Greg");
    alert(person.getName()); //"Greg"
    

    7.4.1 静态私有变量

    实现方式:

    • 所有成员,包括私有成员、特权函数和构造函数都在一个自调用的匿名函数中定义
    • 通过匿名函数的自调用创建所有成员
    • 其中构造函数不使用 var 关键字,因此可以全局访问
    • 访问接口在原型中定义
    (function(){
        //私有成员和私有方法
        var privateVariable = 10;
    
        function privateFunction(){
            return false;
        }
    
        //构造函数,没有用 var 关键字,所以可以全局调用
        MyObject = function(){
        };
    
        //特权方法
        MyObject.prototype.publicMethod = function(){
            privateVariable++;
            return privateFunction();
        };
    })();//通过自调用创建成员
    

    模式特点:

    • 所有成员,包括私有成员、特权函数和构造函数只在匿名函数自调用的时候创建一次
    • 私有成员因为被构造函数和特权函数引用,因此得以在匿名函数自调用完成以后,仍然保留在内存中
    • 构造函数可以多次调用,创建多个实例,但是私有成员不会被再次创建
    • 所有实例共享私有变量,就好像其他语言中的静态变量
    (function(){
        //name 本来是匿名函数的私有变量,在匿名函数自调用的时候创建
    
        var name = "";
    
        //构造函数闭包引用了外部变量,并且生命周期比外部环境长,因此变成了闭包
        //因此,匿名函数自调用完毕后,私有变量不会被销毁
        Person = function(value){
            name = value;
        };
    
        Person.prototype.getName = function(){
            return name;
        };
    
        Person.prototype.setName = function (value){
            name = value;
        };
    })();
    
    var person1 = new Person("Nicholas");
    alert(person1.getName()); //"Nicholas"
    person1.setName("Greg");
    alert(person1.getName()); //"Greg"
    
    var person2 = new Person("Michael");
    alert(person1.getName()); //"Michael"
    alert(person2.getName()); //"Michael"
    

    7.4.2 模块模式(单例模式)

    在匿名函数中没有定义构造函数,而是在匿名函数自调用以后返回一个对象,对象中包含了公共接口

    var singleton = function(){
        //私有成员
        var privateVariable = 10;
    
        function privateFunction(){
            return false;
        }
    
        //返回对象,
        return {
            publicProperty: true,
            publicMethod : function(){
                privateVariable++;
                return privateFunction();
            }
        };
    }();
    

    这种模式下,私有成员同样只创建一次,通过公共接口访问

    var application = function(){
        //私有成员
        var components = new Array();
    
        //一些其他的初始化动作
        components.push(new BaseComponent());
    
        //公共接口
        return {
            getComponentCount : function(){
                return components.length;
            },
    
            registerComponent : function(component){
                if (typeof component == "object"){
                    components.push(component);
                }
            }
        };
    }();
    

    7.4.2 增强单例模式

    和上面的单例模式差不多,只是将返回值由字面量对象改成普通对象

    var singleton = function(){
        //私有成员
        var privateVariable = 10;
    
        function privateFunction(){
            return false;
        }
    
        //创建一个对象
        var object = new CustomType();
    
        //添加特权接口
        object.publicProperty = true;
        object.publicMethod = function(){
            privateVariable++;
            return privateFunction();
        };
    
        //返回对象
        return object;
    }();
    

    例程

    var application = function(){
        //private variables and functions
        var components = new Array();
    
        //initialization
        components.push(new BaseComponent());
    
        //create a local copy of application
        var app = new BaseComponent();
    
        //public interface
        app.getComponentCount = function(){
            return components.length;
        };
    
        app.registerComponent = function(component){
            if (typeof component == "object"){
                components.push(component);
            }
        };
        
        //return it
        return app;
    }();
    

    相关文章

      网友评论

        本文标题:第07章 - 函数表达式

        本文链接:https://www.haomeiwen.com/subject/sxgmkttx.html