本章内容
- 函数表达式的特征
- 使用函数实现递归
- 使用闭包定义私有变量
函数声明提升
函数声明的一个重要特征是函数声明提升(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)
}
}
- 当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。
- 使用 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 的实例,因为最终要通过一个对象字面量来表示它。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。