JavaScript 高级程序设计笔记(七)

第七章 函数表达式

定义函数有两种方式:函数声明和函数表达式

函数声明

function functionName(arg0, arg1, arg2) {  
    //函数体   
}

函数表达式

var functionName = function(arg0, arg1, arg2){  
    //函数表达式   
};

区别在于函数声明可以进行提升,而函数表达式不可以。

// 正确
sayHi();  
function sayHi(){  
    alert("Hi!");
}

// 错误,函数还不存在
sayHi();  
var sayHi = function(){  
    alert("Hi!");
};

下面这种会比较危险

if(condition){  
    function sayHi(){
        alert("Hi!");
    }
} else {
    function sayHi(){
        alert("Yo!");
    } 
}

改为函数表达式就没问题,因为不会声明提升

var sayHi;  
if(condition){  
    sayHi = function(){
        alert("Hi!");
    };
} else {
    sayHi = function(){
        alert("Yo!");
    };
}

7.1 递归

递归是指子一个函数内部调用自身的情况

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

但在函数内部使用函数名调用有一定风险,我们修改为 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 闭包

闭包指有权访问另一个函数作用域中的变量的函数

当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后使用 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() 执行环境的作用域链中处于第二位。

在后台,每个执行环境都有一个对象用来表示其中的所有变量,而函数执行时才会创建执行环境。

  1. 创建 compare() 函数时,会创建一个预先包含全局变量的作用域链,保存在内部 [[Scope]] 属性中
  2. 调用 compare() 函数时,会创建一个执行环境,复制 [[Scope]] 构建起执行环境的作用域链。
  3. 之后又有一个活动对象(参数对象)被创建并推入执行环境作用域的前端

作用域链本质上是一个指向变量对象的指针列表,只包含引用但不实际包含变量对象。函数访问变量时,会从作用域链中搜索具有相应名字的变量。一般函数执行完毕,局部变量销毁,内存中仅保存全局作用域,但闭包又不同。

如果一个函数(内层)定义在一个函数(外层)体内,那么这个内层函数会将外层函数的活动对象添加到自己的作用域链中。

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;
        }
    };
}
// 创建函数
var compareNames = createComparisonFunction("name");  
// 调用函数
var result = compareNames({ name: "Nicholas" }, { name: "Greg" });  
// 解除对匿名函数的引用
compareNames = null;  
  1. 首先创建的比较函数(匿名函数)保存在变量 compareNames 中,它作用域链包含 createComparisonFunction() 函数的活动对象和全局变量对象。
  2. createComparisonFunction("name") 执行完毕后,其活动对象也不会被销毁(主要指匿名函数外层的属性 propertyName),因为匿名函数的作用域仍在引用这个活动对象,虽然 createComparisonFunction() 的执行环境销毁了,但它的活动对象 propertyName 仍在内存中
  3. 只有匿名函数被销毁后,createComparisonFunction() 的活动对象才会被销毁

闭包会携带包含它的函数的作用域,因此内存会占用比较大

7.2.1 闭包与变量

作用域链机制有个副作用,即闭包只能取得包含函数中任何变量的最后一个值

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

每个匿名函数内部都保存了 createFunctions() 函数的活动对象 i,而且这些匿名函数它们引用的都是同一个变量。

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

立即执行了闭包函数,将 i 的值传递给 num,因为参数是值传递的这样结果与 i 的引用无关了

7.2.2 关于 this 对象

闭包中使用 this 会有坑,this 对象是在运行时基于函数执行环境绑定的:

  1. 全局函数中,this 等同于 window
  2. 函数作为某个对象的方法调用时,this 等于那个对象

而匿名函数执行具有全局性,因此它的 this 对象指向 window

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

每个函数在被调用时都会自动获取两个特殊变量: this 和 arguments,内部函数在搜索这两个变量时,只会搜索到其活动对象为止。而闭包的 this 已经是 window 了,所以不会再去 object 中找了

解决办法也很简单

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

arguments 和 this 也存在相同的问题,如果想访问作用域中的 arguments 对象,必须将该对象的引用保存到闭包能够访问的变量中

思考下面的题目,结果是什么

// 1
var o = {};  
var name = "haha";

o.func = function(){

    var name = "gaga";

    let func2 = function(i){ 
        alert(this.name);
    };

    func2(name);
};

o.func()

// 2
var o = {};

var name = "haha";

o.func = function(){

    var name = "gaga";

    (function(i){ 
        alert(this.name);
    })();
};

o.func()  

7.2.3 内存泄露

主要是闭包循环引用引起的内存泄露

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

解决办法

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

仅仅把 element.id 的一个副本保存在变量中是不够的,因为闭包会引用包含函数的整个活动对象,其中就包含 element ,即使闭包不直接引用 element,包含函数的活动对象仍然会保存一个引用,所以要有必要把 element 变量设置为 null。

7.3 模块级作用域

Javascript 没有块级作用域概念,只有函数中才有作用域级概念。

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

哪怕重新定义 i 变量也不会改变 i 值

function outputNumbers(count){  
    for (var i=0; i < count; i++){
        alert(i); 
    }
    var i;          // 重新声明变量      
    alert(i);       // 计数  
}

匿名函数可以通过模仿块级作用域来避免这个问题

(function(){ 
    //这里是块级作用域        
})();

以上代码定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式

但是这种方式是错误的:

// Error
function(){  
    //这里是块级作用域        
}(); //出错!   

因为 Javascript 将 function 关键字当做一个函数声明的开始,而函数声明后面不能跟圆括号。而函数表达式后面可以跟圆括号。要将函数声明转换成函数表达式,只需给下面加一对圆括号即可,现在我们的临时变量就可以用私有作用域了:

(function(){
        var now = new Date();
        if (now.getMonth() == 0 && now.getDate() == 1){
            alert("Happy new year!");
        }
})();

这种做法还可以减少闭包占用内存的问题,因为没有指向匿名函数的引用,执行完毕就立即销毁其作用域链了。

7.4 私有变量

JavaScript 没有私有成员,但有私有变量。任何函数中定义的变量都是可以认为是私有变量。而函数内部的闭包是可以通过自己的作用域链访问这些函数的私有变量,利用这点可以创建用于访问私有变量的公有方法(特权方法)

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 静态私有变量

在私有作用域中定义私有变量或函数,也可以创建特权方法

(function(){
    //私有变量和私有函数
    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"  

注意定义构造函数时没有使用函数声明,而使用了函数表达式。函数声明只能创建局部函数,不是我们想要的。同样的原因,在声明 Person 时没有使用 var 关键字。初始化未经声明的变量,总会创建一个全局变量。所以 Person 就成了全局变量,能够在私有作用域之外被访问到。

私有作用域中的变量不加 var 能提升到全局环境突破次元壁,但严格模式下给未经声明的变量赋值会报错

这个例子中 Person 构造函数与 getName()setName() 方法一样都有权访问私有变量 name,这样 name 就变成了一个静态的、由所有实例共享的属性。

以这种方式创建静态变量使用了原型,增加代码复用,但每个实例都没有自己的私有变量。所有各有利弊。

多查找作用域链一个层次,就会影响查找速度。这也是使用闭包和私有变量的一个明显不足

7.4.2 模块模式

上面的模式都是为自定义类型创建私有变量和特权方法的。而模块模式是为单例创建私有变量和特权方法的。

JavaScript 以字面量方式创建单例对象

var singleton = {  
    name : value,
    method : function () { 
        //这是方法的代码        
    } 
};

模块模式为单例添加私有变量和特权方法使其增强

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.3 增强的模块模式

增强的模块模式适合单例必须是某种类型的实例,同时还必须添加某些属性或方法的情形

var application = function(){  
//私有变量和函数      
var components = new Array();  
//初始化   
components.push(new BaseComponent());  
//创建 application 的一个局部副本         
var app = new BaseComponent();  
//公共接口    
app.getComponentCount = function(){  
            return components.length;
        };
        app.registerComponent = function(component){
            if (typeof component == "object"){
                components.push(component);
            }
    }; 
    //返回这个副本
    return app;
}();      

主要不同在于创建了变量 app,它必须是 BaseComponent 的实例,这个实例实际上是 application 对象的局部变量版。最后仍然赋值给全局变量 application。


-EOF-

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!