在这章里我们讨论ECMAScript中的一个基本对象——函数。我们将会看到不同类型的函数如何影响一个上下文中的变量对象,以及这些函数的作用域链中都包含什么。我们将会回答像下面这样经常被问到的问题:“下面这两种创建函数的方式有什么区别吗(如果有的话,区别是什么呢)?”
// case 1:
var foo = function() {
...
};
// case2
function foo() {
...
}
或者,“为什么下面的调用中函数外加了括号?”
(function() {
...
}) ();
由于这些内容涉及到之前的章节,要完全理解这些部分需要掌握第二章.变量对象和第四章.作用域链中的知识点。
下面让我们一步步来,首先我们来讨论函数的类型。
在ECMAScript中有三种函数类型,每一种都有其自身的特点。
一个函数声明(Function Declaration,简写为FD)是这样一种函数:
o 有一个必需的名称
o 在源代码中的位置:或者在程序级别(Program level)上,或者直接在另一个函数的函数体内(FunctionBody)
o 在进入上下文的阶段中创建
o 影响变量对象
o 并且以下面的方式声明:
function exampleFunc() {
...
}
这类函数的主要特点是:只有它们会影响到变量对象(它们储存在上下文的变量对象中)。这个特点也导致了第二个重点——在进入代码执行阶段时它们已经可以访问了(因为FD是在执行代码前的进入上下文阶段中储存到变量对象中的)。
例如(FD类型的函数的调用位置可以在它的声明位置之前):
foo();
function foo() {
alert("foo");
}
同样重要的是函数在源代码中定义的位置(见上面函数声明定义的第二点):
//函数声明(FD)可以在全局上下文:
function globalFD() {
//也可以在另一个函数体内
function innerFD() {
...
}
}
函数声明只能在这两种位置中进行(也就是说,不能在一个表达式位置或代码块中声明)。
函数声明的一个替代者称为函数表达式(function expression),下面我们来看下这个类型。
一个函数表达式(Function Expression,简写为FE)是这样一类函数:
o 在源代码中,只能在表达式位置(expression position)上定义
o 可以有一个非必需的名称
o 它的定义不影响变量对象
o 在代码执行阶段创建
这类函数的主要特征是,在代码中它们总是定义在表达式位置(expression position)上。下面是一个赋值表达式(assignment expression)的简单例子:
var foo = function() {
...
};
这个例子显示了一个匿名的函数表达式(anonymous FE)是如何赋值给变量‘foo’的。在赋值后,这个函数可以通过变量名’foo’访问——foo()。
从定义中可以看到,这类函数可以有一个可选的函数名称:
var foo = function _foo() {
...
};
这里的重点是,在FE的外面只能通过变量名foo来访问FE——foo();而在函数体内(比如,递归调用),也可以使用函数名称_foo。
当一个FE有了函数名称后很难从外形上将它和FD区分开来。但是如果你了解了它的定义,就能简单地区分它们:FE总是在表达式位置上。在下面的例子中我们能看到各种ECMAScript的表达式,而其中的函数都是FE:
// 小括号(分组运算符 grouping operator)中的函数只能是表达式
(funciton foo() {});
// 中括号(数组初始化符)中的函数也只能是表达式
[function bar(){}];
//逗号也是表达式的运算符
1, function baz() {};
定义也显示了,FE在代码执行阶段创建,并且不储存在变量对象中。例如:
//函数表达式在定义前无法查询,因为它在代码执行阶段创建
alert(foo); // 'foo' is not defined
(function foo() {});
//在定义后也无法通过函数名查询,因为它不在变量对象中
alert(foo); // 'foo' is not defined
那么,一个逻辑上的问题是,为什么我们需要这类函数?答案是显而易见的——为了以表达式的方式使用这些函数而不“污染”变量对象。比如将一个函数作为参数传入另一个函数中:
function foo(callback) {
callback();
}
foo(function bar() {
alert('foo.bar');
});
foo(function baz() {
alert('foo.baz');
});
而在FE赋值给变量的情况中,函数仍然储存在内存中以便于之后通过变量名称访问(因为如我们所知变量储存在VO中):
var foo = function() {
alert('foo');
};
foo();
另一个例子是通过创建封装作用域来对外部上下文隐藏其中的辅助数据(在下面的例子中我们使用了创建后马上调用的FE):
var foo = {};
(function initialize() {
var x = 10;
foo.bar = function() {
alert(x);
};
}) ();
foo.bar(); // 10
alert(x); // 'x' is undefined
我们看到函数foo.bar可以访问函数initialize的内部变量‘x’(通过它自身的[[Scope]]属性)。而在外部,这个‘x‘却不能被直接访问。这个策略在许多框架中被用来创建“私有”数据和隐藏辅助内容。这种初始化FE的模式常常省略为:
(function() {
// 初始化作用域
}) ();
下面是另一个FE的例子——在运行时有条件地创建,而不污染VO:
var foo = 10;
var bar = ( foo % 2 == 0
? function() {alert(0);}
: function() {alert(1);}
);
bar(); // 0
现在让我们回过头来回答文章开头的问题——“如果我们要在一个函数定义后马上调用它,为什么需要给这个函数外加上括号?”答案是:这是表达式语句(expression statement)的限制。
根据标准,为了和代码块(block)相区分,表达式语句(ExpressionStatement)不能开始于一个开放的大括号——“{”;同时为了和函数声明相区分,它也不能开始于一个“function”关键字。也就是说,如果我们试图以下面的方式(开始于一个function关键字)定义一个马上调用的函数:
function () {
...
}();
//甚至加上函数名称
function foo() {
...
}();
那么我们事实上在进行的是变量声明(function declaration),而且这两种写法在解析时都会报错。解析错误的原因不同。
如果我们在全局代码(即程序级别)中使用上面的定义,解析器会尝试将这个函数看做一个声明,因为它以“function”关键字开始。第一种情况下我们会看到一个缺少函数名的语法错误(SyntaxError)(正如前面所说,函数声明必须要有一个函数名称)。
在第二种情况下我们已经有了函数名称’foo‘,那么函数声明应当正常创建了。但是情况不是这样,因为这里有另一个错误——一个缺少内部表达式的分组操作符。注意,在这种情况中函数声明后面实际上是一个分组运算符”()”,而不是一个函数调用的括号!所以,如果我们用下面的代码:
alert(foo); // function
//下面的foo是一个函数声明,而它后面的括号是一个分组运算符
function foo(x) {
alert(x);
} (1);
foo(10); //10, 这里的括号才是调用
这回没有再报错了,因为这里有两种句法——一个函数声明和一个内部表达式为1的分组运算符。上面的例子和下面这个是等价的:
//函数声明
function foo(x) {
alert(x);
}
//有表达式1的分组运算符
(1);
// 分组运算符内的表达式还可以是一个函数,也就是开始提到的情况
(function() {});
而如果我们把函数定义在语句(statement)中,因为前面所说的歧义的关系,我们“应当”看到一个语法错误:
if (true) function foo() {alert(1);}
上面的结构在规范中被认为是语法结构不正确(syntactically incorrect)(因为一个表达式语句不能以“function”关键字开始)。但是,正如我们在实际中看到的,没有一个实现器报出语法错误,而是以各自的方式处理了这种情况。
看了上面所有这些情况,那么当我们需要在创建函数后马上调用它时我们该如何告诉解析器我们的真实目的呢?答案很明确,这个函数应该写成函数表达式(FE),而不是函数声明(FD)。而最简单的创建函数表达式的方式就是上面提到的,使用分组运算符。分组运算符内部总是一个表达式。这样,解析器就能没有歧义地识别一个函数表达式的代码。这样的函数将在代码执行阶段创建,然后执行,然后移除(如果没有其他指向它的引用)。
(function foo(x) {
alert(x);
}) (1); // 1
上面的例子中最后的括号(传入参数)已经是函数的调用而不是一个分组操作符了。
注意,下面的例子也是一个函数马上执行的情况,但在这个例子中不需要在函数外添加括号,因为这个函数已经在表达式位置(expression position)上了(对象属性的赋值表达式),因此解析器知道它是FE,而在代码执行阶段创建它:
var foo = {
bar: function(x) {
return x % 2 == 1 ? "true" : "false"
} (1)
};
alert(foo.bar); // "yes"
如我们所见,返回的是一个字符串而不是一个函数。这里的函数只用于属性的初始化——创建后马上调用(译者按,如果函数内没有返回值,属性值为undefined)。
因此,关于“为什么添加括号”的完整的答案如下:
分组括号用于当一个函数不在表达式位置(expression position)上但我们又需要在它创建后马上调用的情况下——这种情况下我们手动将函数类型转变成了FE(通过添加括号)。
当解析器知道所处理的是FE时——即函数已经在表达式位置上的情况下,则不需要添加括号。
除了添加括号,也可以用任何其他方式将函数转变为FE类型,例如:
//通过逗号
1, function() {
alert("FE");
} ();
//逻辑表达式
!function(){
alert("FE, again");
} ();
//等等
然而,分组括号仍然是使用最广泛和优雅的操作方式。
顺便说一句,分组运算符可以只包括函数描述部分而不包括调用括号,也可以把调用括号一并包括在内。换句话说,下面两种都是正确的FE:
(function () {}) ();
(function () {} ());
下面的例子中的代码没有一个实现器是根据标准来处理的:
if (true) {
function foo() {
alert(1);
}
} else {
function foo() {
alert(0);
}
}
foo(); // 1 or 0 ? 在不同浏览器下测试看看
//译者按——FF:1,其他:0
这里要说的一点是,根据标准,这种语法结构是不正确的。因为根据上面提到过的,一个函数声明(FD)不能发生在一个代码块(code block)中(这里的if和else包括的代码块)。FD只出现在两种情况下:程序级别中或直接在另一个函数体内。
而一个代码块中只能包含语句(statements)。而对于函数来说,可以存在于代码块中的唯一情况即是当函数也是语句的时候——表达式语句(expression statement)。但是从定义上说它不能开始于大括号(因为要和代码块相区别),也不能开始于function关键字(以区别于FD)。
然而,在错误处理章节中,相关标准允许不同实现器对于程序语法(program syntax)的扩展。而其中一个扩展就是代码块中函数的情况。现在的所有实现器在上面的情况中都进行了不报错的处理,但是处理的方式各不相同。
上面的例子中,if-else语句的不同分支中各自定义了不同的函数,而在执行中选择其一。由于选择是在运行中决定的,因此函数应当使用的是函数表达式(FE)的形式。然而,大部分的实现器只是简单地在进入上下文阶段时就创建了两个函数声明(FD),而因为这两个函数同名,因此只有后面声明的那个会被调用。在这个例子中运行结果为0,虽然else代码块从未执行。
然而,SpiderMonkey实现器(译者按:FF的早期js引擎,新引擎JaegerMonkey的这部分结果和它相同)处理这种情况的方式分为两个方面:一方面,它不把这种情况下的函数看做声明(也就是说,函数创建是发生在代码执行阶段),但另一方面这些函数也不是真正的函数表达式,因为函数表达式不能在没有嵌套小括号时调用(和FD相区别),也不能储存在变量对象中。
我的观点是,SpiderMonkey处理这种情况的方式是正确的(译者按:从结果上说,返回了if true代码块中的内容而不是else中的),尽管使用的是自身扩展的中间类型的函数(FD+FE)。这类函数在创建的时间(FE的创建时间)和条件语句选择上说是正确创建的,但又能在外部通过函数名调用(FD的特征)。这个句法扩展在SpiderMonkey中命名为函数语句(Function Statement, FS)。这个术语在MDN中有谈到,js的发明者Brendan Eich也注意到了SpiderMonkey中的这类函数。
当FE有函数名(named function expression, NFE)时,它拥有一个重要的特性。如我们从定义中所知,函数表达式不影响一个上下文中的变量对象(这意味着不能在函数定义前或定义后通过函数名调用函数)。然而,可以通过函数名递归调用:
(function foo(bar) {
if (bar) {
return;
}
foo(true); // 通过'foo'递归调用
})();
foo(); // error: 'foo' is not defined
那么,函数名‘foo’储存在哪里?在函数‘foo’的活化对象中?显然不是,因为函数体内并未定义任何‘foo’名称相关的变量,函数声明或形参。在创建foo函数的父上下文的变量对象中?也不是,因为FE的定义——不影响变量对象——正如我们在外部调用函数名时看到的那样。那么究竟是在什么地方?
它的原理是这样的:当解释器在代码执行阶段发现一个命名的FE时,在创建FE前,它首先创建一个特殊的辅助对象并把它添加到当前作用域链的前端。然后FE被创建,并在这一阶段获得 [[Scope]]属性(第四章作用域链,[[Scope]]即创建这一函数的上下文的作用域链),在这里,[[Scope]]包含特殊的辅助对象。在这之后,FE的函数名添加到这个特殊对象上并作为它的唯一属性,它的值是对这个FE的引用。最后,将这个特殊对象从父作用域链中移除。让我们通过伪代码来看一下这个算法:
specialObject = {};
Scope = specialObject + Scope;
foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0];
这样,从外部就不能访问函数名(因为不存在于外部作用域链中),但在函数的[[Scope]]属性中可以找到这个特殊对象因此在函数内可以访问。
然而,需要注意的是,在一些实现器中(比如Rhino),储存函数名的不是特殊对象而是FE的活化对象。而微软的实现器——JScript,则完全打破了FE的规则,将函数名储存在了父变量对象中,因而可以在外部调用到。
让我们看看不同的实现器是如何处理这个问题的。一些版本的SpiderMonkey有一个关于上述特殊对象的特性,这个特性可以认为是一个bug(虽然,所有的内容都是根据标准来实现的,因此更多意义上算是规范本身的缺陷)。这个特性是关于标示符解析(identifier resolution)的机制:作用域链分析是二维的,当对标示符进行解析时,它也对作用域链中每一个节点对象的原型链进行了分析。
这个机制的表现是当我们在Object.prototype中定义了一个属性然后在代码中使用了一个“不存在的”变量的时候。在下面的代码中,我们解析了名称‘x’,尽管在全局对象中并未找到x。然而,由于在SpiderMonkey中全局对象继承自Object.prototype,标示符x在原型链上得到了解析:
Object.prototype.x = 10;
(function() {
alert(x); // 10
})();
活化对象没有原型(对于大部分实现器而言)。在相同的起始条件下,我们可以在内部函数中看到相同的表现。如果我们定义一个局部变量x并声明一个内部函数(FD或者匿名FE),然后在这个内部函数中引用x,那么,正常情况下这个x应该解析到父函数上下文的活化对象中,而不是在Object.prototype中:
Object.prototype.x = 10;
function foo() {
var x = 20;
function bar() {
alert(x);
}
bar(); // 20, from AO(foo)
(function() {
alert(x);
})(); // 20, from AO(foo)
}
foo();
例外的是,一些实现器为活化对象也设置了原型。所以,在黑莓实现器中,上述例子中的x的值解析为10,换句话说,x在内部函数的活化对象的原型链上得到了解析而不是在外部函数的活化对象上:
AO(FD bar | anonymous FE) -> 'x' not found ->
AO(FD bar | anonymous FE).[[Prototype]] -> 'x' found - 10
当内部函数是一个命名的NFE时,在SpiderMoney中也能看到上面的结果。储存FE的函数名值的对象(根据标准)是一个普通的对象——“就像是通过表达式new Object()构造的”,因此它应当继承自Object.prototype。这个特性在SpiderMonkey 1.7以前的版本中能看到,但在其他的实现器(包括新版本的SpiderMonkey)中不会给这个对象添加原型。
微软的ES实现器——JScript(IE内建js引擎)有一些NFE的bug。每一个bug都完全背离ECMAScript-262-3标准,其中一些可能产生严重的错误。
第一个是,JScript打破了FE的主要规则——FE不应该通过函数名储存在变量对象中。FE的可选的函数名应该储存在特殊对象中并且只在函数内部才允许通过函数名访问;而JScript中直接存储在父变量对象上。此外,NFE在JScript中被当做FD处理,换句话说,在进入上下文阶段就被创建,因而可以在定义前的位置上访问到:
testNFE(); // normal : 'testNFE' is not defined; IE: function
(function testNFE() {
alert("NFE");
})();
testNFE(); // normal : 'testNFE' is not defined; IE: function
正如我们所见,完全背离规则。
第二,在将NFE赋值给变量的情况下,JScript创建了2个不同的函数对象,这很难被认为是符合逻辑的(尤其是,事实上函数名在外部应该是完全不可访问的)
//下面的测试结果都是在JScript中的:
var foo = function bar() {
alert('foo');
};
alert(typeof bar); // “function”
alert(foo === bar); // false!
foo.x = 10;
alert(bar.x); // undefined
foo(); // 'foo'
bar(); // 'foo'
然而需要注意的是,如果将NFE创建和赋值给变量分开为两步(例如先通过分组运算符创建),则等性检查将返回true,就像这两个指向的是同一个对象:
//下面的测试结果都是在JScript中的:
(function bar() {});
var foo = bar;
alert(foo === bar); // true
foo.x = 10;
alert(bar.x); // 10
这个结果是可以解释的。事实上在过程中仍然创建了2个对象,只是最后存在的只有1个。由于JScript中将NFE看做是FD,因此在进入上下文阶段中FD ‘bar’就已经创建了。在这之后的代码执行阶段中创建了第二个对象——FE ‘bar’,并且没有保存在任何地方(译者按:在上一个变量赋值和NFE创建合并一起的例子中FE‘bar’赋值给了x,因而和FD ‘bar’不同)。因为没有任何对FE ‘bar’的引用,于是它被移除。于是最后只剩下一个对象——FD ‘bar’,并且它赋值给了变量’x’。
第三个bug是,由于arguments.callee是对函数的直接引用,它引用的是拥有函数活化时调用名称的对象(因为像上面说的,在JScript中NFE实际上有2个函数对象):
//下面的测试结果都是在JScript中的:
var foo = function bar() {
alert([arguments.callee === foo,
arguments.callee === bar]);
};
foo(); // [true, false]
bar(); // [false, true]
第四,由于JScript中像普通FD那样处理NFE,它不受条件运算符规则的影响,换句话说,它在进入上下文阶段中创建并使用代码中最后定义的值:
//下面的测试结果都是在JScript中的:
var foo = function bar() {
alert(1);
};
if (false) {
foo = function bar() {
alert(2);
};
}
bar(); // 2
foo(); // 1
这个结果也有它的“逻辑”解释。在进入上下文阶段,创建了FD ‘bar’(译者按,2个,if语句内的第2个覆盖了上面的第1个)。在接下来的代码执行阶段中,新函数对象FE ‘bar’被创建并赋值给了变量foo(if语句由于条件判断false而没有执行)。这个“逻辑”是清晰的,但是却是建立在相关的背离标准的JScript bug上的,因此逻辑加引号。
第五个bug是关于通过赋值给不合格的标示符(也就是说,没有使用via关键字)创建全局对象的属性的情况。由于NFE在这里被当做FD因而储存在变量对象中,当它赋值给了不合格标示符(不是给变量,而是直接给全局对象的属性),并且和不合格标示符的名称相同时,这个属性不会成为全局属性:
(function() {
foo = function foo() {};
})();
alert(typeof foo); // normal: "function"; JScript: undefined
其中也有它的“逻辑”:在进入上下文阶段,函数声明foo储存到匿名函数的局部上下文的活化对象中。在代码执行阶段,由于局部上下文的AO中已经有了名为‘foo’的属性,因此foo = function…只是更新了AO中该属性的值,而没有为全局对象创建新的属性(与标准相反)。
这类函数因为其特性因此独立于FD和FE来讨论。其主要特性是这类函数的[[Scope]]属性只包含全局对象。
var x = 10;
function foo() {
var x = 20;
var y = 30;
var bar = new Function("alert(x); alert(y)");
bar(); // 10, y is not defined
}
foo();
我们看到函数bar的[[Scope]]中没有包含foo函数上下文的AO——相关AO中的变量声明没有影响到结果。顺便说一句,注意,Function类构造式可以使用也可以不使用new关键字,两者的结果是一样的。
这类函数的另一个特性是关于相等语法产物(Equated Grammar Productions)和联合对象(Joined Object)。这个机制在规范中是作为优化的建议而存在的(然而,实现器有权不使用这些优化)。例如,如果我们有一个100个单元的数组,并通过循环将每一个赋值为函数,那么实现器就可以使用联合对象的机制。其结果是数组内的全部元素都只使用了同一个函数对象:
var a = [];
for (var k = 0; k < 100; k++) {
a[k] = function() {}; // 在这里,可以使用联合对象
}
但是通过函数类构造式创建的函数对象永远不会联合:
var a = [];
for (var k = 0; k < 100; k++) {
a[k] = Function(''); // 永远是100个独立的函数对象
}
另一个关于联合对象的例子:
function foo() {
function bar(z) {
return z*z;
}
return bar;
}
var x = foo();
var y = foo();
这里,实现器也可以对变量x和y使用联合对象,因为它们引用的函数从本质上(包括内部属性[[Scope]])都没有区别。而通过函数类构造式创建的函数总是需要更多的内存。
函数创建的算法的伪代码(除了联合对象的步骤)如下。这个描述有助于理解ES中函数对象的更多细节。这个算法对于各类函数而言都是相同的:
F = new NativeObject();
//属性[[Class]]
F.[[Class]] = "Function";
//属性[[Prototype]]
F.[[Prototype]] = Function.prototype;
//属性[[Call]]
//引用函数本身;被调用表达式F()激活;激活时创建新的执行上下文
F.[[Call]] = <reference to function>;
//属性[[Construct]]
//建立在一般对象的构造式的基础上
//由new关键字激活
//由这个属性分配内存给新对象
//然后调用F.[[Call]]对创建的对象进行初始化并将其作为‘this’的值
F.[[Construct]] = internalConstructor;
//属性[[Scope]]
//当前上下文的作用域链
F.[[Scope]] = currentContext.Scope;
//如果是通过类构造式创建的则是全局上下文的作用域链
F.[[Scope]] = globalContext.Scope;
// length返回形参长度
F.length = countParameters;
//F对象的原型
__objectPrototype = new Object();
__objectPrototype.constructor = F;
F.prototype = __objectPrototype; // {DontEnum}, 不能在循环中枚举
return F;
注意,F.[[Prototype]]是一个函数(构造式)的原型,F.prototype是这个函数创建的对象的原型(许多时候这个属于常常产生混乱,一些文章中声称的F.prototype”是构造式的原型”是不正确的。)
在这一章里,我们讨论了函数的几种类型:函数声明FD,函数表达式FE,以及通过类构造式创建的函数的特性。其中在FE部分我们讨论了它可能的语法结构,函数语句的实现以及有命名的函数表达式NFE的一些特征和在不同浏览器下的情况。最后,我们尝试用伪代码来表示了函数创建的一般算法。
这一章很长,然而,我们将在后面谈到函数作为构造式创建的对象和它们的原型的时候再次涉及到这部分的内容。
这一章的第二部分是关于EMCAScript中的面向对象编程。在第一部分中我们讨论了OOP的基本理论并勾画出和ECMAScript的相似之处。在阅读第二部分之前,如果有必要,我还是建议首先阅读这一章的第一部分.基本理论,因为后面将会用到其中的一些术语。
这一章我们讨论ECMAScript中面向对象编程(object-oriented programming)的几个主要方面。由于这一主题已经在许多文章中谈论过,本章并不打算“老调重弹”,而是试图更多地着眼于这些过程内在的理论方面。尤其是,我们将研究对象创建的算法,看看对象间的关系(包括最基本的关系——继承)是如何实现的,并且给出一些讨论中将用到的准确定义(我希望这样能够打消一些术语和思路上的疑惑以及一些关于Javascript文章中OOP部分的常见的混淆)。
在这一章中我们来谈谈Javascript中被讨论最多的话题之一——关于闭包(closures)。事实上这个主题并不是新鲜的。然而我们在这里将试着更多从理论的角度去分析和理解它,然后我们还会看一下ECMAScript内关于闭包的内容。
在这章里我们讨论ECMAScript中的一个基本对象——函数。我们将会看到不同类型的函数如何影响一个上下文中的变量对象,以及这些函数的作用域链中都包含什么。我们将会回答像下面这样经常被问到的问题:“下面这两种创建函数的方式有什么区别吗(如果有的话,区别是什么呢)?”
正如我们从第二章.变量对象中了解到的,执行上下文的数据(变量,函数声明,函数形参)以变量对象的属性的方式储存。
许多程序员习惯于认为在编程语言中,this关键字是与面向对象编程紧密相关的,而且引用的是由构造式最新创建的对象。在ECMAScript中,这个概念也被实现了,然而我们将看到,在这里它不仅仅指向已创建的对象。
在程序中我们总是声明变量和函数然后用它们来搭建我们的系统。但是解释器(interpreter)是在哪里和以什么方式来找到我们的数据(函数,变量)的呢?
第1章:在这一章里,我们将会讨论ECMAScript中的执行上下文(execution context)以及与它们相关的可执行代码(executable code)的类型。