第07章 - 函数表达式

常规方式定义函数

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

//函数提升

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;
}();

推荐阅读更多精彩内容