本章内容

  • 函数表达式的特征
  • 使用函数实现递归
  • 使用闭包定义私有变量

函数声明提升

函数声明的一个重要特征是函数声明提升(function declaration hoisting),在执行代码前会先读取函数声明。意味着可以把函数声明放在调用它的语句后面。
但函数表达式不存在函数声明提升,因此如果这么使用会报错,这也是“函数声明”和“函数表达式”的区别。

例如:

sayHi();
function sayHi() {
    //...
}

匿名函数(anonymous function)

又叫拉姆达函数,其 name 属性是空字符串。

function 关键字后没有标识符。

var functionName = function() {
    // ...
}

函数表达式

既然能够创建函数再复制给变量,也就能够把函数作为其他函数的值返回。

function foo() {
    return function() {
        // do sth.
        return 1;
    }
}

把函数当成值来使用的情况下,都可以使用匿名函数。

7.1 递归

递归函数是在一个函数内部通过名字调用自身的情况下构成的:

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

上面是个经典的“递归阶乘”函数,但这个函数还有些缺陷:

var anotherFoo = foo;
foo = null;
alert(anotherFoo(4)); // 出错

因为 foo 不再是函数,所以导致了错误。可以用 arguments.callee 解决:

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

在编写递归函数时,使用 arguments.callee 比使用函数名更保险。

但在严格模式下,不能通过脚本访问 arguments.callee,访问的话会导致错误。不过,可以使用命名函数表达式来达到同样的效果。

var foo = (function f(num) {
    if (num <= 1) {
        return 1;
    } else {
        return num * f(num - 1);
    }
});

以上代码创建了一个名为 f() 的命名函数表达式,然后将它赋给了变量 foo,即使把函数赋给了另一个变量,函数的名字 f 仍然有效,所以递归调用能够正常完成。这种方式在严格模式和非严格模式均可以执行。

7.2 闭包

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

创建闭包

在一个函数内部创建另一个函数。

function foo(num) {
    return function () {
        console.log(num)
    }
}
  1. 当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。
  2. 使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,以此类推,直到作为作用域链终点的全局执行环境。

例子:

function compare(a, b) {
    if (a < b) {
        return -1;
    } else if (a > b) {
        return 1;
    } else {
        return 0;
    }
}

后台的每个执行环境都有一个表示变量的对象——变量对象。

创建函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的 [[Scope]] 属性中。

调用函数时,会为函数创建一个执行环境,通过复制函数的 [[Scope]] 属性中的变量对象来构建执行环境的作用域链。

接着,一个活动对象被创建并被推入执行环境作用域链的前端。

作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

7.2.1 闭包与变量

作用域有个副作用:闭包只能取得包含函数中任何变量的最后一个值,因为闭包所保存的是整个变量对象,而不是某个特殊的变量。

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

在调用每个匿名函数时,我们传入了变量 i,由于函数参数是按值传递的,所以就会将变量 i 的当前值复制给参数 num。而在这个匿名函数内部,又创建并返回了一个访问 num 的闭包。所以,result 中的每个函数都有自己 num 变量的一个副本,因此就可以返回各自不同的数值。

7.2.2 关于 this 对象

this 对象是在运行时基于函数的执行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等于调用它的对象。不过,匿名函数的执行环境具有全局性,因此 this 对象通常指向 window。

为什么匿名函数不能取得其包含作用域(外部作用域)的this呢?

每个函数在被调用时,都会自动获得两个特殊变量:this,arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的 this 对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。

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

7.2.3 内存泄露

function foo() {
    var element = document.getElementById('test');
    var id = element.id;
    
    element.onclick = function() {
        alert(id);
    };
    element = null;
}

闭包会引用包含函数的整个活动对象,而其中包含着 element。即使闭包不直接引用 element,包含函数的活动对象仍然会保存一个引用。因此,有必要把 element 变量设置为 null。这样就能够接触对 DOM 对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。

7.3 模仿块级作用域

ES6 之前,js 没有块级作用域。

同名变量声明时,后续的变量声明将被忽略。

var a = 1;
var a = 2; // var a 被忽略,只执行初始化赋值 a = 2

使用匿名函数可以模仿块级作用域:

(function() {
  // 块级作用域
  var i = 1;
})()
// 括号包括的 function 其实是一个函数表达式,不是一个函数声明。

alert(i) // 报错

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

7.4 私有变量

严格说:JS 没有私有成员的概念(ts 语法有),所有对象属性都是共有的。但有一个私有变量的概念。

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。

我们把有权访问私有变量和函数的公有方法称为“特权方法”(privileged method)。有两种在对象上创建特权方法的方式。

第一个种在构造函数中定义特权方法:

function MyObject() {
    // 私有变量(函数)
    var privateVariable = 10;
    
    function privateFunction() {
        return false;
    }
    
    // 特权方法,是一个闭包,所以可以访问构造函数中的私有变量和函数,外部环境只能通过 MyObject.publicMethod() 来访问内部私有变量和函数。
    this.publicMethod = function() {
        privateVariable++;
        return privateFunction();
    }
    
}

但是这有个弊端,弊端来自构造函数,它针对每个实例都会创建同样一组新方法。

7.4.1 静态私有变量

(function() {

var name = '';
Person = function(value) {
    name = value;
}
Person.prototype.getName = function() {
    return name;
}
Person.prototype.setName = function(value) {
    name = value;
}

})();

以这种方式创建静态私有变量,会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。变量 name 变成了一个静态的、由所有实例共享的属性,在某个实例上调用 setName() 后,会影响所有实例。

7.4.2 模块模式(module pattern)

模块模式是为单例创建私有变量和特权方法。单例,指的是只有一个实例的对象。

创建单例:

var singleton = {
    name: value,
    method: function() {
        // ...
    }
}

模块模式:

var singleton = function() {
    // 私有变量和私有函数
    var privateVariable = 10;
    var privateFunction() {
        return false;
    };
    return {
        publicProperty: 10,
        publicFunction: function() {
            privateVariable++;
            privateFunction();
        }
    }
}

如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。

以这种模式创建的每个单例都是 Object 的实例,因为最终要通过一个对象字面量来表示它。


盐酥鸡加甘梅粉
91 声望1 粉丝