原文
ECMA-262-3 in detail. Chapter 5. Functions.
简介
在这篇文章中,我们将讨论一个ESCMAScript对象,函数。我们将讨论不同类型的函数,每个类型是如何影响环境中的变量对象(variables object)以及内部的作用域的。我们将回答以下经常会出现的问题,“下面的函数有什么不同吗?”[译者注:当进入一个函数时,会创建一个对象,里面保存了函数运行需要的各种变量]
var foo = function () {
...
};
function foo() {
...
}
(function () {
...
})();
函数类型
函数声明
函数声明(Funcation Declaration),缩写FD,是一个函数:
必须拥有一个名字
在源码中的位置,要么在程序级别,要么在其他函数体的体内
在进入上下文时创建
会影响变量对象
用以下方式声明
function exampleFunc() {
...
}
这个类型的函数的主要特点是它影响变量对象(它们被存在环境的变量对象中)。这导致了第二个重要的点,在代码执行期间,他们已经被用了,因为当一进入环境后,函数声明就被保存在环境的变量对象中,在代码开始执行之前。例如函数可以被调用在它被声明之前,从源码的角度看过去
foo();
function foo() {
console.log('foo');
}
// function can be declared:
// 1) directly in the global context
function globalFD() {
// 2) or inside the body
// of another function
function innerFD() {}
}
函数声明要么出现在全局环境中,要么出现在其他函数体内。
函数表达式
函数表达式(Function Expression),缩写FE,是一个函数
在源码中可以在表达式位置被定义
可以选择不要名字
对环境的变量对象没有影响
在代码执行的阶段被创建
常见的赋值表达式
var foo = function () {
...
};
也可以给函数个名字
var foo = function _foo() {
...
};
在这值得注意的是,在函数表达式的外面通过变量foo访问函数,而在函数内部,例如递归调用,用_foo访问。函数表达式总在表达式的位置,例如以下的例子都是函数表达式
// in parentheses (grouping operator) can be only an expression
(function foo() {});
// in the array initialiser – also only expressions
[function bar() {}];
// comma also operates with expressions
1, function baz() {};
函数表达式在代码执行的阶段才会创建,并不会被存在环境变量对象中
// FE is not available neither before the definition
// (because it is created at code execution phase),
console.log(foo); // "foo" is not defined
(function foo() {});
// nor after, because it is not in the VO
console.log(foo); // "foo" is not defined
为什么需要函数表达式?是为了不污染环境变量对象,同时作为其他函数的参数。
function foo(callback) {
callback();
}
foo(function bar() {
console.log('foo.bar');
});
foo(function baz() {
console.log('foo.baz');
});
函数表达式被赋值给了个变量,我们可以通过该变量来访问它
var foo = function () {
console.log('foo');
};
foo();
还可以创建封闭的作用域,对外隐藏内部数据。
var foo = {};
(function initialize() {
var x = 10;
foo.bar = function () {
console.log(x);
};
})();
foo.bar(); // 10;
console.log(x); // "x" is not defined
我们可以看到foo.bar通过[[Scope]]可以访问函数initialize内部变量x,与此同时,x不能被外界直接访问。这个策略经常被用来创建私有变量和隐藏辅助实体。通常initialize函数表达式的名字是被忽略的。
(function () {
// initializing scope
})();
这有个函数表达式,根据运行情况来创建,且不污染环境变量对象。
var foo = 10;
var bar = (foo % 2 == 0
? function () { console.log(0); }
: function () { console.log(1); }
);
bar(); // 0
注意,ES5中有bind函数,锁定this的值。
var boundFn = function () {
return this.x;
}.bind({x: 10});
boundFn(); // 10
boundFn.call({x: 20}); // still 10
这通常在事件监听,或延迟函数(setTimeout)中被使用。
括号问题
根据规范,表达式声明(expression statement),不能以{
开始,这会被当作块,也不能以关键字function
开始,会被当作函数声明。所以,如果我们想定义一个函数,用下面的方式(以function
关键字开头)立马调用。
function () {
...
}();
// or even with a name
function foo() {
...
}();
我们处理函数声明,同时会产生解析错误。如果我们把这样的定义放在全局代码中,解析器会把函数当作声明,因为它以function
关键字开头,在第一种情况中,我们会得到SyntaxError,因为我们缺少函数名。在第二个情况中,我们确实有了函数名,函数声明应该被正常的创建。但是我们有另一个语法错误,一个组操作符中没有表达式。所以在这个场合下,括号只是函数声明后面的组操作符,而不是函数调用。
// "foo" is a function declaration
// and is created on entering the context
console.log(foo); // function
function foo(x) {
console.log(x);
}(1); // and this is just a grouping operator, not a call!
foo(10); // and this is already a call, 10
// function declaration
function foo(x) {
console.log(x);
}
// a grouping operator
// with the expression
(1);
// another grouping operator with
// another (function) expression
(function () {});
// also - the expression inside
("foo");
// etc
如果我们在语句中有函数声明,也会报错
if (true) function foo() {console.log(1)}
我们如何创建一个函数立刻调用它?它应该是个函数表达式,创建函数表达式最简单的操作就是组操作符。如此,一个函数会在执行时创建,调用,移除,如果没有引用指向它。
(function foo(x) {
console.log(x);
})(1); // OK, it's a call, not a grouping operator, 1
注意在下面的例子中,括号已经不需要了,因为函数已经在表达式的位置了,解析器知道把它当作函数表达式,在执行的时候被创建。
var foo = {
bar: function (x) {
return x % 2 != 0 ? 'yes' : 'no';
}(1)
};
console.log(foo.bar); // 'yes'
正如我们所见,foo.bar是一个字符串而不是函数,函数在初始化属性时就被调用了。括号是需要的,如果我们想立刻调用函数在创建它后,而函数并不在表达式的位置,如果函数已经在表达式的位置,括号就不需要了。除了括号,还有其他转换函数表达式的方法
1, function () {
console.log('anonymous function is called');
}();
// or this one
!function () {
console.log('ECMAScript');
}();
// and any other manual
// transformation
(function () {})();
(function () {}());
实现扩展: Function Statement
if (true) {
function foo() {
console.log(0);
}
} else {
function foo() {
console.log(1);
}
}
foo(); // 1 or 0 ? test in different implementations
这里要说的是,根据规范,这种语法构造是不正确的。因为函数声明不能出现在代码块中,而这里有if/else的代码块,函数声明只能出现在程序级别(program level)或其他函数体内。然而在规范的错误处理中,允许了这种实现扩展。但是有各自不同的实现方式。if/else是希望我们能根据运行时的情况,来创建函数,这暗示了把它们当作函数表达式,实际上,主要的实现中,在进入上下文时,就会创建函数声明,因为它们同名,所以最后一个会被调用,所以打印1。然后spidermonkey(一种js引擎)以不同的方式实现这个。
有名字的函数表达式的特点 (NFE)
如果函数表达式有名字(named function expression),缩写NFE。由定义可知,函数表达式并不影响环境的变量对象。然后,函数表达式有时候需要在递归中调用自己。
(function foo(bar) {
if (bar) {
return;
}
foo(true); // "foo" name is available
})();
// but from the outside, correctly, is not
foo(); // "foo" is not defined
当解释器在代码执行的过程中遇到有名字的函数表达式,在创建函数表达式之前,解释器创建了一个辅助特殊的对象,把它加在当前的作用域链前面。然后它创建函数表达式,函数获得[[Scope]]属性。在那之后,有名字的函数表达式被作为一个属性,添加到了特殊的对象上,对象的值是函数表达式的引用。最后一步是从父作用域链中移除特殊的对象。
specialObject = {};
Scope = specialObject + Scope;
foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
delete Scope[0]; // remove specialObject from the front of scope chain
NFE and SpiderMonkey
[译者注:讲述SpiderMonkey(Mozilla火狐c/c++)引擎如何处理有名函数表达式,不译了]
[译者注:讲述Rhino(Mozilla java)引擎如何处理有名函数表达式,不译了]
NFE and JScript
[译者注:讲述JScript(微软)引擎如何处理有名函数表达式,不译了]
通过函数构造器创建函数
由函数构造器构造的函数,它的[[Scope]]只包括全局对象。
var x = 10;
function foo() {
var x = 20;
var y = 30;
var bar = new Function('console.log(x); console.log(y);');
bar(); // 10, "y" is not defined
}
函数创建算法
F = new NativeObject();
// property [[Class]] is "Function"
F.[[Class]] = "Function"
// a prototype of a function object
F.[[Prototype]] = Function.prototype
// reference to function itself
// [[Call]] is activated by call expression F()
// and creates a new execution context
F.[[Call]] = <reference to function>
// built in general constructor of objects
// [[Construct]] is activated via "new" keyword
// and it is the one who allocates memory for new
// objects; then it calls F.[[Call]]
// to initialize created objects passing as
// "this" value newly created object
F.[[Construct]] = internalConstructor
// scope chain of the current context
// i.e. context which creates function F
F.[[Scope]] = activeContext.Scope
// if this functions is created
// via new Function(...), then
F.[[Scope]] = globalContext.Scope
// number of formal parameters
F.length = countParameters
// a prototype of created by F objects
__objectPrototype = new Object();
__objectPrototype.constructor = F // {DontEnum}, is not enumerable in loops
F.prototype = __objectPrototype
return F
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。