您的位置:首页 > Web前端 > JavaScript

JavaScript作用域链和闭包

2009-08-22 10:57 405 查看
译前序:在JavaScript中,作用域链和闭包始终是个重要而高深的话题,在程序员要深入掌握JS的过程中也是无法回避的问题。一般的JS书籍中也鲜有深入的介绍。这段节自犀牛书第五版,我把它翻译成了中文,供学习使用,欢迎转载。

8.8. 函数作用域和闭包

正如第四章提到的,JavaScript函数体是在局部作用域中执行的,而不是全局作用域。这一节会详细解释相关细节,包括作用域问题,还有闭包。

8.8.1. Lexical Scoping 词法作用域

Functions in JavaScript are lexically rather than dynamically scoped. This means that they run in the scope in which they are defined, not the scope from which they are executed. When a function is defined, the current scope chain is saved and becomes part of the internal state of the function. At the top level, the scope chain simply consists of the global object, and lexical scoping is not particularly relevant. When you define a nested function, however, the scope chain includes the containing function. This means that nested functions can access all of the arguments and local variables of the containing function.
JavaScript中函数的作用域是词法上的,而不是动态的。这意味着它们运行在它们被定义时的作用域中,而不是它们被执行时的作用域中。在一个函数被定义时,当前作用域链就被保存并成为函数的内部状态的一部分。在最外层,作用域链仅仅由全局对象组成,词法作用域与此没什么关系。然后当你定义了一个内嵌函数,作用域链就会包括包含函数(外部函数)。这意思是,内嵌函数可以访问到包含函数的所有参数和本地变量。
Note that although the scope chain is fixed when a function is defined, the properties defined in that scope chain are not fixed. The scope chain is "live," and a function has access to whatever bindings are current when it is invoked.
请注意,虽然在定义函数时,作用域链是固定的,但作用域链中定义的属性并不是固定的。作用域链是“鲜活的”,在某个函数被调用时,它能访问到当前作用域链中任何绑定着的内容。

8.8.2. The Call Object 调用对象

When the JavaScript interpreter invokes a function, it first sets the scope to the scope chain that was in effect when the function was defined. Next, it adds a new object, known as the call  object (the ECMAScript specification uses the term activation object) to the front of the scope chain. The call object is initialized with a property named arguments that refers to the Arguments object for the function. Named parameters of the function are added to the call object next. Any local variables declared with the var statement are also defined within this object. Since this call object is at the head of the scope chain, local variables, function parameters, and the Arguments object are all in scope within the function. This also means that they hide any properties with the same name that are further up the scope chain, of course.
当JavaScript解析器调用某个函数时,它首先把作用域设置到函数定义时就存在的作用域链上。然后,它添加了一个新的对象,叫做调用对象(ECMAScript规范叫做激活对象),并加到作用域链的开头。这个调用对象通过一个指向函数的Arguments对象的名叫arguments的属性来进行初始化。然后函数中有名称的参数也被添加到调用对象。任何用var声明的局部变量也被定义在调用对象中。因为调用对象处于作用域链的开头,局部变量/函数参数以及Arguments对象就都处在函数的作用域中了。当然这意味着它们隐藏/覆盖了任何排在作用域链后面具有相同名称的属性的了。
Note that unlike arguments, this is a keyword, not a property in the call object.
请注意,跟arguments不一样,this是个关键字,而不是调用对象的一个属性。

8.8.3. The Call Object as a Namespace 调用对象作为命名空间

It is sometimes useful to define a function simply to create a call object that acts as a temporary namespace in which you can define variables and create properties without corrupting the global namespace. Suppose, for example, you have a file of JavaScript code that you want to use in a number of different JavaScript programs (or, for client-side JavaScript, on a number of different web pages). Assume that this code, like most code, defines variables to store the intermediate results of its computation. The problem is that since this code will be used in many different programs, you don't know whether the variables it creates will conflict with variables used by the programs that import it.
有时候简单地定义一个函数来创建一个调用对象当作临时命名空间来使用是有点用处的,这样你可以定义变量并创建属性,而不会跟全局命名空间里的变量属性相冲突。比如,你有个javascript文件,想把这个代码用在很多不同的JavaScript程序中(或者是许多不同web页面的客户端 javascript中)。假设这段代码跟大多数代码一样,会定义变量来存储计算的中间结果。但问题是,因为这段代码会用在很多不同的程序中,你不知道它创建的那些变量会不会跟引入了这段代码的那些程序中的变量相冲突。
The solution, of course, is to put the code into a function and then invoke the function. This way, variables are defined in the call object of the function:
当然解决方案就是把这代码放到一个函数中,然后调用这个函数。这样,变量就定义在了函数的调用对象中了。
function init() {
   // Code goes here.
    // Any variables declared become properties of the call
    // object instead of cluttering up the global namespace.
}
init(); // But don't forget to invoke the function!
The code adds only a single property to the global namespace: the property "init", which refers to the function. If defining even a single property is too much, you can define and invoke an anonymous function in a single expression. The code for this JavaScript idiom looks like this:
这段代码为全局命名空间增加了一属性:“init”,它指向了这个函数。如果还觉得定义一个属性也很麻烦,你可以在一个表达式中,直接定义并执行一个匿名函数。这段JavaScript中的惯常用法看起来是这样的:
(function() { // This function has no name.
    // Code goes here.
    // Any variables declared become properties of the call
    // object instead of cluttering up the global namespace.
})();     // end the function literal and invoke it now.
Note that the parentheses around the function literal are required by JavaScript syntax.
请注意函数外面的圆括号是必须的,这由JavaScript语法决定的。

8.8.4. Nested Functions as Closures 内嵌函数作为闭包

The facts that JavaScript allows nested functions, allows functions to be used as data, and uses lexical scoping interact to create surprising and powerful effects. To begin exploring this, consider a function g defined within a function f. When f is invoked, the scope chain consists of the call object for that invocation of f followed by the global object. g is defined within f, so this scope chain is saved as part of the definition of g. When g is invoked, the scope chain includes three objects: its own call object, the call object of f, and the global object.
JavaScript支持内嵌函数,这样函数就可以被当成数据来使用,并使用词法作用域交互来创建令人惊奇和强大的效果。开始前,我们先假设一个函数f中定义了另一个函数g。在f调用时,作用域链包含了因为调用f而产生的调用对象,接着是全局对象。g是在f中定义的,因此这个作用域链作为g的定义的一部分被保存下来。在g调用时,作用域链就包含了三个对象:它自己的调用对象,f的调用对象,还有全局对象。
Nested functions are perfectly understandable when they are invoked in the same lexical scope in which they are defined. For example, the following code does not do anything particularly surprising:
如果内嵌函数调用时的作用域就是它们定义时所处的词法作用域,那就很好理解了。比如,下面的代码没有什么稀奇的东西:
var x = "global";
function f() {
    var x = "local";
    function g() { alert(x); }
    g();
}
f(); // Calling this function displays "local"
In JavaScript, however, functions are data just like any other value, so they can be returned from functions, assigned to object properties, stored in arrays, and so on. This does not cause anything particularly surprising either, except when nested functions are involved. Consider the following code, which includes a function that returns a nested function. Each time it is called, it returns a function. The JavaScript code of the returned function is always the same, but the scope in which it is created differs slightly on each invocation because the values of the arguments to the outer function differ on each invocation. (That is, there is a different call object on the scope chain for each invocation of the outer function.) If you save the returned functions in an array and then invoke each one, you'll see that each returns a different value. Since each function consists of identical JavaScript code, and each is invoked from exactly the same scope, the only factor that could be causing the differing return values is the scope in which the functions were defined:
然而在JavaScript中,函数跟其他元素一样都是数据,因为它们也可以是函数的返回值,赋值给对象的属性,甚至存储在数组中等等。这也不会有什么意想不到的,但涉及到内嵌函数,事情就不一样了。思考一下下面的代码,它包括一个能够返回内嵌函数的函数。每次调用这个函数,它都会返回另一个函数。返回函数的JavaScript的代码总是一样的,但每次调用时,它被创建时所处的作用域有些不同,因为每次调用时传给外部函数的参数值是不一样的。(也就是说,每次外部函数调用时,作用域链中的调用对象是不同的)如果你把返回的函数放到一个数组中,然后依次执行,你会发现每个都返回不同的数值。因为每个函数都是由相同的javascript代码组成的,而且都是从同一个作用域中被调用的,那唯一可能导致返回值不同的因素就是函数所定义的作用域了:
// This function returns a function each time it is called
// The scope in which the function is defined differs for each call
function makefunc(x) {
    return function() { return x; }
}
// Call makefunc() several times, and save the results in an array:
var a = [makefunc(0), makefunc(1), makefunc(2)];
// Now call these functions and display their values.
// Although the body of each function is the same, the scope is
// different, and each call returns a different value:
alert(a[0]()); // Displays 0
alert(a[1]()); // Displays 1
alert(a[2]()); // Displays 2
The results of this code are exactly what you would expect from a strict application of the lexical scoping rule: a function is executed in the scope in which it was defined. The reason that these results are surprising, however, is that you expect local scopes to cease to exist when the function that defines them exits. This is, in fact, what normally happens. When a function is invoked, a call object is created for it and placed on the scope chain. When the function exits, the call object is removed from the scope chain. When no nested functions are involved, the scope chain is the only reference to the call object. When the object is removed from the chain, there are no more references to it, and it ends up being garbage collected.
这段代码执行的结果会跟你所期望的遵循词法作用域规则的程序运行的完全一样:一个函数在它所被定义的作用域中执行。然而之所以这些结果会出其不意,是因为你会以为局部作用域会在定义它的函数退出时也不再存在了。而这,也的确是这样的。当函数被调用时,关于它的一个调用对象会被创建出来并放到作用域链中。在函数退出时,调用对象就会从作用域链中去掉。如果不涉及到内嵌函数,这个作用域链是唯一引用到调用对象的。当对象从链中去掉时,对象就不再有别的引用,然后就会被垃圾回收。
But nested functions change the picture. If a nested function is created, the definition of that function refers to the call objects because that call object is the top of the scope chain in which the function was defined. If the nested function is used only within the outer function, however,  the only reference to the nested function is in the call object. When the outer function returns, the nested function refers to the call object, and the call object refers to nested function, but there are no other references to either one, and so both objects become available for garbage collection.
但内嵌函数改变了这一切。如果创建了内嵌函数,那么函数的定义就指向了调用对象,因为调用对象在作用域链的最开头,而函数是在作用域链中定义的。如果内嵌函数只是在外部函数中使用到,那么对内嵌函数的唯一引用就是调用对象自己。当外部函数返回时,内嵌函数指向调用对象,调用对象也指向内嵌函数,但没有任何其他的引用来引用二者了,所以两者都会被垃圾回收。
Things are different if you save a reference to the nested function in the global scope. You do so by using the nested function as the return value of the outer function or by storing the nested function as the property of some other object. In this case, there is an external reference to the nested function, and the nested function retains its reference to the call object of the outer function. The upshot is that the call object for that one particular invocation of the outer function continues to live, and the names and values of the function arguments and local variables persist in this object. JavaScript code cannot directly access the call object in any way, but the properties it defines are part of the scope chain for any invocation of the nested function. (Note that if an outer function stores global references to two nested functions, these two nested functions share the same call object, and changes made by an invocation of one function are visible to invocations of the other function.)
但如果你在全局作用域上保存了对内嵌函数的引用,事情就不一样了。你可以把内嵌函数作为外部函数的返回值,或者把内嵌函数存成某个其他对象的属性,来达到这点。这种情况下,就会存在一个指向内嵌函数的外部引用,同时内嵌函数也保留着指向外部函数的调用对象的引用。结果就是,外部函数的某次调用产生的调用对象会继续存货,而函数参数的名和值以及本地变量都存在这个对象中。javascript代码无法以任何方式访问到这个调用对象,但它定义的属性却是作用域链的一部分,以备内嵌函数的调用。(请注意,如果外部函数保存了全局引用指向两个内嵌函数,那么这两个内嵌函数就会共享同一个调用对象,而一个函数的调用所带来的变化对另外一个函数的调用也是可见的。)
JavaScript functions are a combination of code to be executed and the scope in which to execute them. This combination of code and scope is known as a closure in the computer science literature. All JavaScript functions are closures. These closures are only interesting, however, in the case discussed above: when a nested function is exported outside the scope in which it is defined. When a nested function is used in this way, it is often explicitly called a closure.
JavaScript函数是要执行的代码已经执行时所处的作用域的结合体。代码和作用域的结合,成就了计算机科学文化中闭包的概念。所有的 JavaScript函数都是闭包。然后这些闭包只在上面讨论的这些情形下才显得有趣:当某个内嵌函数被暴露在它所被定义的作用域外时。当某个内嵌函数被这样使用时,它通常就被称作闭包。
Closures are an interesting and powerful technique. Although they are not commonly used in day-to-day JavaScript programming, it is still worth working to understand them. If you understand closures, you understand the scope chain and function call objects, and can truly call yourself an advanced JavaScript programmer.
闭包是个有趣又强大的技术。虽然它们在日复一日的JavaScript编程中并不常用到,但仍然值得去理解它们。如果你理解了闭包,你就理解了作用域链和函数调用对象,你也就可以称得上是个高级JavaScript程序员了。

8.8.4.1. Closure examples 闭包示例

Occasionally you'll want to write a function that can remember a value across invocations. The value cannot be stored in a local variable because the call object does not persist across invocations. A global variable would work, but that pollutes the global namespace. In Section 8.6.3., I presented a function named uniqueInteger() that used a property of itself to store the persistent value. You can use a closure to take this a step further and create a persistent and private variable. Here's how to write a function without a closure:
有时候,你会想要编写一个函数能在跨调用时记住某个值。这个值不能保存在局部变量中,因为调用对象并不会在跨调用时保存下来。全局变量也许可以,但这回污染了全局命名空间。在8.6.3节,我提供了一个名叫uniqueInteger()的方法,使用它自己的一个属性来保存这个值。现在你也可以使用一个闭包来完成这个任务,并创建一个保存及私有的值。下面是不是用闭包时编写的函数:
// Return a different integer each time we're called
uniqueID = function() {
     if (!arguments.callee.id) arguments.callee.id = 0;
     return arguments.callee.id++;
};
The problem with this approach is that anyone can set uniqueID.id back to 0 and break the contract of the function that says that it never returns the same value twice. You can prevent that by storing the persistent value in a closure that only your function has access to:
这个方法的问题在于,任何人都可以设置uniqueID.id为0,从而打破函数永远不会返回两次相同值的约定。现在你可以在闭包中保存这个值,而只有你的函数能访问到它:
uniqueID = (function() { // The call object of this function holds our value
    var id = 0;                // This is the private persistent value
    // The outer function returns a nested function that has access
    // to the persistent value. It is this nested function we're storing
    // in the variable uniqueID above.
    return function() { return id++; }; // Return and increment
})(); // Invoke the outer function after defining it.
Example 8-6 is a second closure example. It demonstrates that private persistent variables like the one used above can be shared by more than one function.
示例8-6是第二个闭包示例。它演示了像上面那样私有保存的值可以被不止一个函数访问到。
Example 8-6. Private properties with closures
// This function adds property accessor methods for a property with
// the specified name to the object o. The methods are named get
// and set. If a predicate function is supplied, the setter
// method uses it to test its argument for validity before storing it.
// If the predicate returns false, the setter method throws an exception.
//
// The unusual thing about this function is that the property value
// that is manipulated by the getter and setter methods is not stored in
// the object o. Instead, the value is stored only in a local variable
// in this function. The getter and setter methods are also defined
// locally to this function and therefore have access to this local variable.
// Note that the value is private to the two accessor methods, and it cannot
// be set or modified except through the setter.
function makeProperty(o, name, predicate) {
    var value; // This is the property value
    // The setter method simply returns the value.
    o["get" + name] = function() { return value; };
    // The getter method stores the value or throws an exception if
    // the predicate rejects the value.
    o["set" + name] = function(v) {
        if (predicate && !predicate(v))
            throw "set" + name + ": invalid value " + v;
        else
            value = v;
    };
}
// The following code demonstrates the makeProperty() method.
var o = {}; // Here is an empty object
// Add property accessor methods getName and setName()
// Ensure that only string values are allowed
makeProperty(o, "Name", function(x) { return typeof x == "string"; });
o.setName("Frank"); // Set the property value
print(o.getName()); // Get the property value
o.setName(0); // Try to set a value of the wrong type
The most useful and least contrived example using closures that I am aware of is the breakpoint facility created by Steve Yen and published on http://trimpath.com as part of the TrimPath client-side framework. A breakpoint is a point in a function at which code execution stops and the programmer is given the opportunity to inspect the value of variables, evaluate expressions, call functions, and so on. Steve's breakpoint technique uses a closure to capture the current scope within a function (including local variables and function arguments) and combines this with the global eval() function to allow that scope to be inspected. eval() evaluates a string of JavaScript code and returns its result (you can read more about it in Part III). Here is a nested function that works as a self-inspecting closure:
我能想到的最有用也最自然的使用闭包的例子是由Steve Yen 编写的断点工具,他也发布在了http://trimpath.com作为TrimPath客户端框架的一部分。断点是函数执行停止所处的点,从而让程序员有机会查看变量的值、表达式的值、调用函数等等。Steve的断点技术使用了一个闭包来在函数内(包括局部变量和函数参数)捕捉当前作用域,并将其与全局的eval()函数结合来查看这个作用域。eval()函数会计算一段JavaScript代码字符串,并返回结果(你可以在本书第三部分阅读到更多相关内容)。这里是一个内嵌函数,可以当作自检查闭包来运行:
// Capture current scope and allow it to be inspected with eval()
var inspector = function($) { return eval($); }
This function uses the uncommon identifier $ as its argument name to reduce the possibility of naming conflicts with the scope it is intended to inspect.
这个函数使用不寻常的标识符$来作为它的参数名称,从而减少与它要检查的作用域差生命名冲突的可能
You can create a breakpoint within a function by passing this closure to a function like the one in Example 8-7.
你可以像示例8-7这样的函数中传入这个闭包来创建断点。
Example 8-7. Breakpoints using closures
// This function implements a breakpoint. It repeatedly prompts the user
// for an expression, evaluates it with the supplied self-inspecting closure,
// and displays the result. It is the closure that provides access to the
// scope to be inspected, so each function must supply its own closure.
//
// Inspired by Steve Yen's breakpoint() function at
// http://trimpath.com/project/wiki/TrimBreakpoint2
function inspect(inspector, title) {
    var expression, result;
   // You can use a breakpoint to turn off subsequent breakpoints by
    // creating a property named "ignore" on this function.
    if ("ignore" in arguments.callee) return;
    while(true) {
        // Figure out how to prompt the user
        var message = "";
        // If we were given a title, display that first        
        if (title) message = title + "/n";
        // If we've already evaluated an expression, display it and its value
        if (expression) message += "/n" + expression + " ==> " + result + "/n";
        else expression = "";
        // We always display at least a basic prompt:
        message += "Enter an expression to evaluate:";
        // Get the user's input, displaying our prompt and using the
        // last expression as the default value this time.
        expression = prompt(message, expression);
        // If the user didn't enter anything (or clicked Cancel),
        // they're done, and so we return, ending the breakpoint.
        if (!expression) return;
        // Otherwise, use the supplied closure to evaluate the expression
        // in the scope that is being inspected.
        // The result will be displayed on the next iteration.
        result = inspector(expression);
    }
}
Note that the inspect() function of Example 8-7 uses the Window.prompt() method to display text to the user and allow him to enter a string (see Window.prompt() in Part IV for further details).
请注意,示例8-7的inspect()函数使用了Window.prompt()方法来向用户显示文本,并允许用户输入一个字符串。
Here is a function for computing factorials that uses this breakpointing technique:
下面是使用断点技术计算阶乘的函数:
function factorial(n) {
     // Create a closure for this function
    var inspector = function($) { return eval($); }
    inspect(inspector, "Entering factorial()");
    var result = 1;
    while(n > 1) {
        result = result * n;
        n—;
        inspect(inspector, "factorial() loop");
    }
    inspect(inspector, "Exiting factorial()");
    return result;
}

8.8.4.2. Closures and memory leaks in Internet Explorer 闭包及IE中的内存泄漏

Microsoft's Internet Explorer web browser uses a weak form of garbage collection for ActiveX objects and client-side DOM elements. These client-side objects are reference-counted and freed when their reference count reaches zero. This scheme fails when there are circular references, such as when a core JavaScript object refers to a document element and that document element has a property (such as an event handler) that refers back to the core JavaScript object.
微软的IE浏览器对ActiveX对象和客户端DOM元素的垃圾回收使用了一种弱形式。这些客户端对象使用引用计数机制,在引用计数为0时就被释放。这种模式在有闭环引用存在时就会有问题,比如某个核心JavaScript对象引用了一个文档元素,而这个文档元素有个属性(比如event hanler)又引用回这个核心JavaScript对象。
This kind of circular reference frequently occurs when closures are used with client-side programming in IE. When you use a closure, remember that the call object of the enclosing function, including all function arguments and local variables, will last as long as the closure does. If any of those function arguments or local variables refer to a client-side object, you may be creating a memory leak.
这种闭环应用通常发生在在IE客户端编程中使用闭包的时候。当你使用闭包时,记得包含函数的调用对象,包括所有的函数参数和本地变量,会有跟闭包一样的存活时期。如果任意一个函数参数或者局部变量引用了客户端对象,你就导致了内容泄漏。
关于这个问题的完整讨论已经超出本书的范围,请阅读http://msdn.microsoft.com/library/en-us/IETechCol/dnwebgen/ie_leak_patterns.asp for details。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: