第七章:函数表达式
本章内容:
- 函数表达式的特征
- 使用函数实现递归
- 使用闭包定义私有变量
定义函数的方式有两种,一种是函数声明,另一种是函数表达式
// 函数声明
function functionName(arg0){
// 函数体
};
// 函数表达式
var functionName = function(arg0){
// 函数体
}
关于函数声明,它有个特征是函数声明提升
。这个在第五章节有讲过。意思在执行代码之前会先读取声明函数。
关于函数表达式,就是创建了一个匿名函数(anonymous function)再赋值给一个变量。
7.2 闭包
闭包是值有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在函数内部创建另一个函数。来看例子:
function createComparisionFunction(propertyName){
return function(obj1, obj2){
// 下面两行能获取createComparisionFunction中的propertyName属性就是因为闭包
var value1 = obj1[propertyName];
var value2 = obj2[propertyName];
if(value1 < value2){
return -1;
} else if(value1 > value2){
return 1;
} else {
return 0
}
}
}
4、5两行能够访问外部函数变量propertyName。即使这个函数被返回了,而且是在其他地方被调用。之所以能够访问变量,是因为内部函数的作用域链中包含了createComparisionFunction的变量对象。要理解细节,则从函数被调用时,发生什么开始入手。
当某个函数被调用,会创建一个执行环境(execution context)以及创建相应的作用域链,然后,使用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()执行环境的作用域链中出于第二位。关系如下图:
后台的每个执行环境都有一个表示变量的对象-变量对象。全局环境中的变量对象始终存在,而像compare()函数这样的局部环境的变量对象,则只有在函数执行的过程中存在。在创建compare()函数时,就创建一个预先包含全局变量对象的作用域链,这个作用域链会保存在内部[[Scope]]中。在调用compare()函数的时候,就会为函数创建一个执行环境,然后复制[[Scope]]属性中的对象构建起执行环境中的作用域链。之后,又有一个活动对象(当前的变量对象即为活动对象)被创建并推入执行环境作用域链的前端。
作用域链的本质是一个指向变量对象的指针列表
无论在什么时候在函数中访问一个变量时,都会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部的活动对象就会销毁,内存仅保存全局的变量对象。 但是闭包的情况又有所不同:
在另一个函数内部定义的函数会将包含函数(即外部函数)的变量对象添加到它的作用域链中。因此,在createComparisionFunction内部定义的匿名函数的作用域链中,实际上包含外部函数的createComparisionFunction的变量对象。
var compare = createComparisionFunction("name");
var result = compare({name:'Nicholas'},{name:'Greg'});
匿名函数从createComparisionFunction返回后,它的作用域链被初始化包含createComparisionFunction()函数的活动对象与全局的变量对象。
这样匿名函数就可以访问createComparisionFunction()中定义的变量。更为重要的是,createComparisionFunction函数执行完毕后,其活动对象也不会销毁,因为匿名函数的作用域链仍然引用这个活动对象,但它的活动对象一直保存在内存中;知道匿名函数被销毁后(compare = null),createComparisionFunction的活动对象才会被销毁。
7.2.1 闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能读取包含函数中任何变量的最后一个值(可能中间变量值发生多次变换)。别忘了闭包保存的事整个变量对象,而不是某个特殊的变量值。
function createFunction(){
var result = [];
for(var i=0; i<10; i++){
result[i] = function(){
return i;
}
}
return result;
}
var selfFunction = createFunction();
console.log(selfFunction[0]()); // 10
console.log(selfFunction[1]()); // 10
这个函数会返回一个函数数组。表面上看,似乎每一个数组都应该返回自己的索引值,即位置0的函数返回0,位置1的函数返回1,以此类推。但事实上,每个函数都返回10。因为每个函数的作用域链中都包含了createFunction的变量对象,所以他们都指向了同一个变量i。当createFunction返回之后变量i的值就变成了10。所以每个函数查找的i都是10。 但我们可以创建另外一个匿名函数强制生成一个闭包。
function createFunction(){
var result = [];
for(var i=0; i<10; i++){
result[i] = (function(num){
return function(){
return num
}
})(i)
}
return result;
}
var selfFunction = createFunction();
console.log(selfFunction[0]()); // 0
console.log(selfFunction[1]()); // 1
当调用匿名函数时,我们传递了变量i.由于变量是按值传递的,所以这回将变量i的当前值赋值给参数num。而在这个匿名函数的内部,又创建了一个返回num的闭包。这样result每个函数都有自己的一份num变量副本。
延伸阅读1: 详细图解作用域链与闭包
闭包是一种特殊的对象。
它由两部分组成。执行上下文(代号A),以及在该执行上下文中创建的函数(代号B)。
当B执行时,如果访问了A中变量对象中的值,那么闭包就会产生。
在大多数理解中,包括许多著名的书籍,文章里都以函数B的名字代指这里生成的闭包。而在chrome中,则以执行上下文A的函数名代指闭包。
因此我们只需要知道,一个闭包对象,由A、B共同组成,在以后的篇幅中,我将以chrome的标准来称呼。
// demo01
function foo() {
var a = 20;
var b = 30;
function bar() {
return a + b;
}
return bar;
}
var bar = foo();
bar();
上面的例子,首先有执行上下文foo,在foo中定义了函数bar,而通过对外返回bar的方式让bar得以执行。当bar执行时,访问了foo内部的变量a,b。因此这个时候闭包产生。
JavaScript拥有自动的垃圾回收机制,关于垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。
而我们知道,函数的执行上下文,在执行完毕之后,生命周期结束,那么该函数的执行环境就会失去引用。其占用的内存空间很快就会被垃圾回收器释放。可是闭包的存在,会阻止这一过程。
var fn = null;
function foo(){
var a = 2;
function innerFoo(){
console.log(a);
}
fn = innerFoo;
}
function bar(){
fn(); //此处的保留的innerFoo的引用
}
foo();
bar(); //2
在上面的例子中,foo()
执行完毕之后,按照常理,其执行环境生命周期会结束,所占内存被垃圾收集器释放。但是通过fn = innerFoo
,函数innerFoo的引用被保留了下来,复制给了全局变量fn。这个行为,导致了foo的变量对象,也被保留了下来。于是,函数fn在函数bar内部执行时,依然可以访问这个被保留下来的变量对象。所以此刻仍然能够访问到变量a的值。
这样,我们就可以称foo为闭包。
下图展示了闭包foo的作用域链。
我们可以在chrome浏览器的开发者工具中查看这段代码运行时产生的函数调用栈与作用域链的生成情况。如下图。
7.2.2 关于this
作为函数的特殊对象,this对象是运行时基于函数的执行环境板定的:
- 在全局函数中,this等于window
- 而当函数作为某个对象的方法调用时,this等于那个对象。
不过,匿名函数的执行环境具有全局性,因此this对象通常指window。但有时候编写方式的不同,这一点不那么明显。
var name = 'window';
var object = {
name: 'my object',
getNameFun: function(){
return function(){
return this.name;
}
}
};
alert(object.getNameFun()()); //window (在全局环境中执行,this即为window)
每个函数在被调用时都会自动获取两个特殊变量:this
和argument
。内部函数在搜索这两个变量时,只会搜索其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的this对象保存在一个闭包能够访问的变量中,就可以让闭包访问该对象了。
var name = 'window';
var object = {
name: 'my object',
getNameFun: function(){
var that = this;
return function(){
return that.name;
}
}
};
alert(object.getNameFun()()); // my object
在定义匿名函数之前,我们把this对象赋值给了一个叫that的变量。而在定义闭包之后,闭包可以访问这个变量,
this和arguments也存在同样的问题,如果访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。
延伸阅读2: 全访问解读this
重新回顾一下执行环境
在执行环境的创建阶段,会分别生成变量对象,建立作用域链,确定this指向。其中变量对象与作用域链我们都已经仔细总结过了,而这里的关键,就是确定this指向。
首先我们需要得出一个非常重要一定要牢记于心的结论,this的指向,是在函数被调用的时候确定的。也就是执行环境被创建时确定的。因此,一个函数中的this指向,可以是非常灵活的。比如下面的例子中,同一个函数由于调用方式的不同,this指向了不一样的对象。
var a = 10;
var obj = {
a: 20
}
function fn () {
console.log(this.a);
}
fn(); // 10
fn.call(obj); // 20
除此之外,在函数执行过程中,this一旦被确定,就不可更改了。
var a = 10;
var obj = {
a: 20
}
function fn () {
this = obj; // 这句话试图修改this,运行后会报错 ReferenceError: Invalid left-hand side in assignment
console.log(this.a);
}
fn();
1. 全局对象中的this
关 于全局对象的this,我之前在总结变量对象的时候提到过,它是一个比较特殊的存在。全局环境中的this,指向它本身。因此,这也相对简单,没有那么多复杂的情况需要考虑。
// 通过this绑定到全局对象
this.a2 = 20;
// 通过声明绑定到变量对象,但在全局环境中,变量对象就是它自身
var a1 = 10;
// 仅仅只有赋值操作,标识符会隐式绑定到全局对象
a3 = 30;
// 输出结果会全部符合预期
console.log(a1); // 10
console.log(a2); // 20
console.log(a3); // 30
2. 函数中的this
在总结函数中this指向之前,我想我们有必要通过一些奇怪的例子,来感受一下函数中this的捉摸不定。
// demo01
var a = 20;
function fn(){
console.log(this.a)
}
fun(); //20
// demo02
var a = 20;
function fn(){
var a = 10;
function foo(){
console.log(this.a);
}
foo();
}
fn(); // 20
var a = 20;
var obj = {
a: 10,
c: this.a + 20,
fn: function(){
return this.a;
}
}
console.log(obj.c); // 40
console.log(obj.fn()); // 10
如果你暂时没想明白怎么回事,也不用着急,我们一点一点来分析。
分析之前,我们先直接了当抛出结论。
在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。
从结论中我们可以看出,想要准确确定this指向,找到函数的调用者以及区分他是否是独立调用就变得十分关键。
// 为了能够准确判断,我们在函数内部使用严格模式,因为非严格模式会自动指向全局
function fn() {
'use strict';
console.log(this);
}
fn(); // fn是调用者,独立调用
window.fn(); // fn是调用者,被window所拥有
在上面的简单例子中,fn()
作为独立调用者,按照定义的理解,它内部的this指向就为undefined。而window.fn()
则因为fn被window所拥有,内部的this就指向了window对象。
但是我们需要特别注意的是demo03。在demo03中,对象obj中的c属性使用this.a + 20
来计算。这里我们需要明确的一点是,单独的{}
是不会形成新的作用域的,因此这里的this.a
,由于并没有作用域的限制,所以它仍然处于全局作用域之中。所以这里的this其实是指向的window对象。
再来看一些容易理解错误的例子,加深一下对调用者与是否独立运行的理解。
var a = 20;
var foo = {
a: 10,
getA: function(){
return this.a
}
}
console.log(foo.getA()); // 10
var test = foo.getA();
console.log(test()); // 20
foo.getA()
中,getA是调用者,他不是独立调用,被对象foo所拥有,因此它的this指向了foo。而test()
作为调用者,尽管他与foo.getA的引用相同,但是它是独立调用的,因此this指向undefined,在非严格模式,自动转向全局window。
稍微修改一下代码,大家自行理解。
var a = 20;
function getA() {
return this.a;
}
var foo = {
a: 10,
getA: getA
}
console.log(foo.getA()); // 10
function foo(){
console.log(this.a);
}
function active(fn){
fn(); //真实调用者
}
var a = 20;
var obj = {
a: 10,
getA: foo
}
active(obj.getA); // 20
7.3 模仿块级作用域
javascript没有块级作用域的概念。
function outputNumber(count){
for(var i=0; i<count; i++){
alert(i)
}
alert(i) // 5
}
outputNumber(5);
可以使用闭包来实现临时变量
function outputNumber(count){
(function(){
for(var i=0; i<count; i++){
alert(i)
}
})()
alert(i) // 报错
}
小结:
在JavaScript编程中,函数表达式是一种非常有用的技术。实现函数表达式可以无需对函数命名,从而实现动态编程。匿名函数,也成为拉姆达函数,是一种使用Javascript函数强大的方式。以下总结了函数表达式的特点:
- 函数表达式不同于函数声明。函数声明要求有名字,但函数表达式不需要。没有名字的函数表达式叫做匿名函数;
- 在无法确定如何引用函数的情况下,递归函数就会变得很复杂;
- 递归函数应该始终使用arguments.callee来递归调用自身,不要使用函数名--函数名可能会发生变化;
当在函数内部定义其他函数,其他函数又使用了父函数的变量时,就创建了闭包。闭包有权访问函数内部的所有变量。原理如下:
- 在后台的执行环境中,闭包的作用连会包含它自己的变量对象,函数的变量对象,和全局变量对象;
- 通常,函数的作用域以及其所有的变量会在函数执行后被销毁;
- 但是,当函数返回一个闭包的时候,这个函数的变量对象将会一直在内存直到闭包消失为止;
使用闭包可以在Javascript模仿块级作用域(javascript只有全局作用域的概念),要点如下:
- 创建并立即调用一个函数,这样既可以执行其中的代码,又不会再内存中留下该函数的引用;
- 结果就是函数内部的所有变量都会被立即销毁--除非将默写变量赋值给了包含作用域(外部作用域)中的变量;
闭包还可以在对象中创建私有变量,相关概念如下:
- 即使Javascript中没有正式的私有对象属性的概念,但可以用闭包来实现公有方法,而使用公有方法可以访问在包含作用域中的定义变量;
- 有权访问私有变量的公有方法叫做特权方法;
- 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块模式、增强的模块模式来实现单例的特权方法;