# 函数表达式
# 一、函数的特征
在引用类型#Function 类型里介绍过定义函数的三种方式:函数声明、函数表达式、构造函数。
其中 函数声明 有个重要的特征就是 函数声明提升,意思是在执行代码之前会 先读取函数声明。这就意味着可以把函数声明放在调用它的语句的 后面。
foo(); // 输出数值3,因为(1)和(3)会“函数声明提升”,并且最后(3)会覆盖(1)
function foo() {
// (1) 函数声明提升
console.log(1);
}
var foo = function () {
// (2) 变量声明提升,但是函数声明提升会优先。并且还是同名,那就不会进行变量声明提升了
console.log(2);
};
function foo() {
// (3) 函数声明提升
console.log(3);
}
2
3
4
5
6
7
8
9
10
11
12
13
函数表达式 ,看起来像是常规的变量赋值语句,即创建一个函数并将它赋值给变量。这种情况下创建的函数叫 匿名函数(拉姆达函数),因为 function 关键字后面没有标识符。理解 函数声明提升 就要要区分函数声明和函数表达式之间的区别,例:
sayHi(); // 错误:函数还不存在
var sayHi = function () {
console.log("Hi!");
};
/*********也不要这样做****************/
if (condition) {
function sayHi() {
console.log("Hi!");
}
} else {
function sayHi() {
console.log("Yo!");
}
}
/*********可以这样做******************/
var sayHi;
if (condition) {
sayHi = function () {
console.log("Hi!");
};
} else {
sayHi = function () {
console.log("Yo!");
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在引用类型#作为值的函数里提过函数 作为值来使用,比如作为函数的返回值:
function comparisonFunction(propertyName) {
return function (object1, object2) {
// 作为了comparisonFunction的返回值来使用
var v1 = object1[propertyName];
var v2 = object2[propertyName];
if (v1 < v2) {
return -1;
} else if (v1 > v2) {
return 1;
} else {
return 0;
}
};
}
var values = [{ age: 3 }, { age: 10 }, { age: 15 }, { age: 1 }, { age: 5 }];
values.sort(comparisonFunction("age"));
console.log(values); // [ { age: 1 }, { age: 3 }, { age: 5 }, { age: 10 }, { age: 15 } ]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 二、递归
一个函数通过名字调用自身,这样的结构是 递归函数。
// 经典的递归阶乘函数
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
2
3
4
5
6
7
8
这个函数表面看起来没什么问题,但下面的代码可能导致它出错:
var anotherFactorial = factorial;
factorial = null; // anotherFactorial还指引着原函数,而factorial引进断开了
console.log(anotherFactorial(4)); // 出错,到factorial(num - 1);时发现factorial已经不存在了
2
3
arguments.callee
是一个指向正在执行的函数的指针,可以使用来它来解决这个问题:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
2
3
4
5
6
7
严格模式下不能使用arguments.callee
,可以使用命名函数表达式来达成相同的结果:
var factorial = function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
};
2
3
4
5
6
7
# 三、闭包
在“变量、作用域和内存问题”里介绍过执行环境及作用域,可以简单回顾一下函数的创建以及调用。
- 在某个环境(全局或某个函数内部)中执行时,假如遇到了“创建函数”的语句。创建 这个新函数时,会复制环境里的 作用域链 并保存到新函数的
[[Scope]]
属性中。 - 当这个函数被 调用 时,会初始化内部的环境(变量对象、作用域链、this)。用 this 绑定函数调用处的上下文对象;初始化函数内部的变量保存到 变量对象 中;复制函数的
[[Scope]]
属性作为函数内部的 作用域链,并将变量对象相关的指针也保存在这个作用域链上。
其实 闭包 这个结构存在于上面的过程中了,创建函数时 函数本身和它所处的环境 构成了一个闭包结构,这个闭包涵盖了 嵌套外层函数的环境 和 全局环境 里所有的 变量对象 以及它们的指针也就是 作用域链。
var a = 1; // (1)
function foo() {
// (2)
var b = 2; // (3)
function bar() {
// (4)
console.log(a); // (5),可以访问全局变量a,输出1
console.log(b); // (6),可以访问嵌套外层变量b,输出2
console.log(c); // (7),可以访问嵌套外层变量c,输出3
} // (8)
var c = 3; // (9)
bar(); // (10)
} // (11)
foo(); // (12)
2
3
4
5
6
7
8
9
10
11
12
13
14
分析一下上面这个例子,bar 函数在创建时,bar 与 bar 所处的环境(foo 的内部环境和 foo 外部的全局环境)一起构成了一个 闭包 结构。在(10)
调用 bar 函数,在(5)(6)(7)
里可以访问a b c
三个变量。但严格地说,这三个变量不是通过闭包访问的,其实是单纯地通过 作用域链 访问的,这个作用域链其实也是闭包的一部分。这样不好观察闭包,下面这个例子可以很清晰地观察闭包。
var a = 1; // (1)
function foo() {
// (2)
var b = 2; // (3)
function bar() {
// (4)
console.log(a); // (5),可以访问全局变量a,输出1
console.log(b); // (6),可以访问嵌套外层变量b,输出2
console.log(c); // (7),可以访问嵌套外层变量b,输出2
} // (8)
var c = 3; // (9)
return bar; // (10),返回给了非定义时的环境,这里是返回给了全局环境
} // (11)
var baz = foo(); // (12)
baz(); // (13)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
这个例子中,bar 与 bar 所处的环境一起构成了一个 闭包 结构,在(10)
将 bar 函数作为值返回给了外部(非定义时的环境)。其实 foo 已经执行完了,foo 内的作用域链也已经销毁了,但是 foo 内的变量对象还没有销毁,因为是 bar 的闭包阻挡了它销毁,bar 的闭包涵盖了 foo 函数的环境以及全局环境里 所有的变量对象 以及它们的指针也就是 作用域链。然后在(13)
调用 bar 函数,bar 就可以通过闭包访问a b c
三个变量。即使不在非定义时的环境里调用,都能通过闭包来访问被闭包涵盖到的变量。
# 3.1 闭包与变量
函数只能从它闭包中访问到变量的最后有效值
function createFunction() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i;
};
}
return result;
}
2
3
4
5
6
7
8
9
返回的 result 是个函数数组,数组里的每个函数都有自己的闭包,它们的闭包里都有指向i
的指针,并且由于 i 使用var
这种没有块级作用域的定义关键词来定义的,导致 i 是在 createFunction 里是同一个变量,那么所有的闭包都共享它,最后 createFunction 执行完,i 是 10,那么函数数组中每个函数最后通过闭包访问的 i 都是 10。
function createFunction() {
var result = new Array();
for (var i = 0; i < 10; i++) {
result[i] = (function (num) {
return function () {
return num;
};
})(i);
}
return result;
}
2
3
4
5
6
7
8
9
10
11
上面例子使用了一个匿名函数存储每一次 for 循环的 i 值,函数数组里每一个函数的闭包优先访问外层匿名函数的 num 也就是每次循环的 i 值。其实在 es6 中引入 let 后直接将原先的例子中的var i
改为let i
,这个效果跟这个匿名函数包裹的效果一样。
# 3.2 闭包与 this 对象
函数作为一个值被 return 到定义时环境之外,这个函数不属于谁,它的 this 一般会绑定到 window 对象(非严格模式),那么在闭包中使用 this 的话就会得到 window 对象。如果想访问外层函数里的 this,可以将外层函数里的 this 使用 其他变量保存。arguments 跟 this 一样,也需要特殊处理。
var name = "The Window";
var obj = {
name: "My Object",
getNameFunc: function () {
var that = this;
return function () {
return that.name; // 如果不使用that,此时这个匿名函数内的this就是指向window对象的
};
},
};
2
3
4
5
6
7
8
9
10
# 3.3 内存泄漏
闭包涵盖了 嵌套外层函数的环境 和 全局环境 里所有的 变量对象 以及它们的指针也就是 作用域链。如果函数被作为值被 return 到定义时环境之外,使用完后需要尽快将函数的引用置为 null,否则会让闭包里的对象一直占用着内存。当然,如果直接将闭包里的对象置为 null 也可以减少内存的占用。
# 四、模仿块级作用域
js 没有块级作用域(es6 有了),但可以通过匿名函数来模仿块级作用域。
将行数声明包含在一对圆括号中表示它是一个函数表达式,再在其后添加另一对圆括号会立即调用这个函数。
function outputNumbers(count) {
(function () {
for (var i = 0; i < count; i++) {
console.log(i);
}
})();
console.log(i); // 导致一个错误,i定义在一个匿名函数内部,在函数外部就访问不了
}
2
3
4
5
6
7
8
# 五、私有变量
对象属性都是公有的,因为用 this.xxx 给实例绑定的,而函数内部可定义私有变量,不使用 this,直接在内部定义一个变量,外部就访问不到了。
可以在函数内部创建一个闭包,那么闭包可以访问那些是有变量,并且这个闭包还可以用 this 设置为公有方法供外部使用,这种有权访问私有变量和私有函数的公有方法称为特权方法。
function MyObject() {
var privateVariable = 10; // 私有变量
function privateFunction() {
// 私有方法
return false;
}
this.publicMethod = function () {
// 特权方法
privateVariable++;
return privateVariable;
};
}
2
3
4
5
6
7
8
9
10
11
12