(翻译) closures-are-not-complicated
总计:读完这篇文章需要20分钟
这篇文章讲解了闭包的一些内容,作者是拿ES5规范中的一些名词来讲的.
所以可能和博客上一篇文章中提到的binding object, (lexical environment,environment record) 有不一样.
主要学习到的地方是,在声明函数时,其实函数内部会有一个[[Scope]]属性,引用了当前的上下文环境的 binding object. 所以其实当这个函数成为闭包(如文中提到的成为了回调函数),在没有被垃圾回收机制释放的时候, 这个[[Scope]]属性指向的上下文环境也不会被释放.
看文章前记得要分清 prototype chain 和scope chain的区别:
Variables are looked up on the scope chain, starting with the current execution context and going up the tree of enclosing execution contexts.
Properties are looked up firstly on the base object, then on that object's [[Prototoype]]
chain (i.e. its internal prototype).
(注: 在不同的ES规范中 术语会有一点不一样, 作者这里使用的是ES5 规范中的一些术语 )
Closures seem to frighten people a bit. Partially I suspect this is down to the academic nature of the term "closure". It sounds like something Californians look to achieve, not something to help you write software. (Disclosure: I mostly grew up in San Francisco, so I'm allowed to poke fun at my fellow Californians. Don't Try This At Home.) I suspect it's also because if you don't know the rules for them, they seem mysterious, and the mysterious is often frightening.
Closures看起来很让人害怕,通常我觉得这是因为这个术语听起来太学术派了,
First off, what is a closure? I'll leave a thorough definition to my academic betters, but let's put it this way for my fellow plodders and I: A closure is a function with data intrinsically bound to it.
首先,什么是closure? 我给出一个定义吧: Closure是一个绑定了一些数据的函数
Consider this code:
function updateDisplay(panel, contentId)
{
var url;
url = 'getcontent?id=' + contentId;
jsonRequest(
url,
function(resp)
{
if (resp.err)
{
panel.addClassName('error');
panel.update(
'Error retrieving
content ID '
+ contentId
+ ' from "'
+ url
+ '", error: '
+ resp.err);
}
else
{
panel.removeClassName('error');
panel.update(resp.json.content);
}
}
);
}
This updates a panel element based on the results of a call to retrieve some JSON data. The call to the jsonRequest function accepts two parameters: The URL that will return the JSON data, and a callback function to trigger when the request completes (one way or the other). If the data was returned successfully, the callback sets the content of the panel from the JSON data and makes sure that the "error" CSS class is not set on the element; if there's an error, it shows details about the error and sets the "error" CSS class. (In this example, we happen to be using Prototype to extend our panel element with the nifty class name and update methods, but that's as much to keep the example simple as anything else.)
上面的代码会根据获取JSON数据的结果来更新panel元素.
jsonRequest的调用接受两个参数,一个是URL地址,还有一个是callback function,如果数据成功返回了, 回调函数会把获取的JSON内容设置到panel中,并确保’error’的css类不在元素上,如果返回报错了,会显示出错误,并给元素置上’err’r的类.
The callback above is an
example of a closure: A function with data bound to it, in this case the panel
and contentId
arguments
we passed into the updateDisplay
function,
and also updateDisplay
's url
local variable.
It's useful to have this information bound to the callback, because the jsonRequest
function
doesn't know anything about panel
or contentId
, it
just knows about triggering the callback — but the callback has the
information it needs to do its job.
上面的回调函数就是一个closure:一个绑定了数据的函数
updateDisplay这个函数传入了panel和contentId.
jsonRequest函数不知道panel和contentId是什么,它只知道触发回调- 回调包含了一些数据
Magic? Nah. I can't speak for closures in other languages, but there's nothing complicated about closures in JavaScript. Seriously. They're dead easy. You need to know, say, three things and you're good to go. Well, four things, but that's only because someone has been misinforming you. Here's a quick list of those four things, after which we'll delve in a bit deeper, and then I'll tell you something at the end that will surprise you for three seconds before you go "Oh, of course":
所以Js中的闭包一点都不复杂,也没什么魔法, 下面我们来更深入的讲解一下:
- In JavaScript, everything is a data structure, even functions, and — critically — even the context in which functions run. One aspect of that context is a "call object" (that's Flanagan's term, the ECMA specification uses "activation object" — I'll use "call object" because I'm lazy and it's less typing I find it clearer).
在js中 一切都是数据结构,function也是一种,function执行的上下文也是,我们称上下文为”call object” (ECMA标准是叫”activation object” 我这么做只是觉得更清楚)
- JavaScript variable names are resolved on the basis of a "scope chain", which includes (among other things) the global object and all of the current call objects in scope.
js变量名称的解析过程是通过”作用域链”这样的一个东西的,包括了当前作用域的call object和global object
- Functions in JavaScript are lexically scoped — for the plodders like me out there, that means that the things a function can access (the things "in scope" for it) are determined by the context in which the function is defined, not the context in which it's executed.
JS中的函数是词法作用域的,意思是说一个函数可以访问的东西,是由函数声明所处的上下文决定的,而不是执行时的上下文决定的
- Closures do not create memory leaks. Someone probably told you that they did at some point (perhaps they thought that's what Microsoft was trying to say here).
闭包不会引起内存泄漏
Okay, let's see how those things come together to make closures easy.
#1: Everything is a object
When we call a JavaScript function, the context of that call to the function — the parameters we've used, the variables within the function itself — are part of an object called the call object (or, again, "activation object" in ECMA parlance). The interpreter creates a call object for this particular execution of the function and sets some properties on that call object before passing it into the function's code. The properties are:
当我们调用一个js函数的时候 会创建一个call object, 里面有一些属性
- A property called arguments which is an array (of sorts, in most implementations it's not actually an Array object) of the actual arguments we called the function with, plus a callee property which is a reference to the function being called.
一个叫arguments的数组( ..多数实现中其实不是一个数组对象
一个 callee属性指向被调用的函数本身
- A property for each of the arguments defined by the function declaration. The values of these properties are set to the values passed into the function. If any arguments weren't specified (because JavaScript lets you call functions with however many arguments you want, regardless of the definition), properties for any missing arguments are still created on the call object — with the value undefined.
入参们
- A property for every declared variable within the function (e.g., every "var" statement). (These start out with the value undefined.)
函数内定义的变量
Let's look again at selected bits of the updateDisplay function from earlier:
function updateDisplay(panel, contentId)
{
var url;
// ...
}
And let's assume we call it with a reference to a 'leftPanel' div and a contentId of 123:
updateDisplay(leftPanel, 123);
That creates a call object for this execution of updateDisplay that essentially looks like this:
call.arguments = [leftPanel, 123]
call.arguments.callee = updateDisplay
call.panel = leftPanel
call.contentId = 123
call.url = undefined
This object is then used within the body of the function when the code uses the arguments panel or contentId, or the local variable url, etc.
这个对象会在函数后面执行的过程中被用到,
"But wait," you say. "I don't refer to an object when I use the arguments or variables in my function, I just use their names." Indeed — the use of the call object is implicit. How do we end up using the call object when we just write "contentId" (for instance)? Because once the call object is created, it's put at the top of the scope chain for the duration of the function call. Which takes us nicely to...
这个对call object的引用是隐式的,当call object创建的时候,函数执行过程中,该call object会被放在scope chain的顶端
#2: Variable names are resolved using a "scope chain"
变量解析通过”scope chain”
If you've written JavaScript in a browser environment, you've probably used the global document object, and perhaps the global navigator object as well. Here's the thing: Those aren't global objects. Those are properties of a global object — in fact, of the global object. The document and navigator properties are set up for you by the browser, but they're just properties of an object.
在浏览器环境中 全局的 document对象和navigator对象其实不是global objects,这些只是global object的 properties, 是浏览器为你设置好的.
So how do you get away with just giving the property name, rather than giving the object name as well? Because of the scope chain, an ordered series (indeed, a chain) of objects. When the JavaScript interpreter sees an unqualified variable reference, it checks the top object on the scope chain to see if it has a property by that name: If so, it gets used; if not, the interpreter checks the next object down the chain. The global object is the bottom of the chain, so if you just type document.writeln("Blah blah blah"), eventually the document property is found on the global object and used.
通过scope chain,当js解释器看到一个未保留的变量引用时,会沿着scope chain 一直找它,global object 在作用域链的最末端,所以当你打 document.writeln..的时候 最后会找到 global object的这个 document属性并开始使用
(The global object is an abstract entity in the generic JavaScript definition, but in the specific case of a web browser you know it by another name: window. In browsers, the window object is the global object; it just also has a property, "window", that refers to itself.)
(global object 是JS定义中的一种抽象实体, 在浏览器中,window是真正的 global object)
So quick: Within a function, how do variable names get resolved? Right! When the function is being executed, the call object with all those nifty properties for the function's arguments and variables is put at the top of the scope chain. So during our call to updateDisplay from earlier, the call object for the call is at the top of the scope chain, followed by the global object, like this:
所以,当函数被执行的时候,call object被创建,并被放在scope chain的顶端:
When the interpreter sees a variable reference, say contentId, it looks on the call object: If there's a contentId property on the call object, it gets used; otherwise, the interpreter looks at the next object in the scope chain, which in this case is the global object.
于是当解释器找ocntentId的时候会先从call object找 如果没有就向上找
(There's more to know about the scope chain than I've described here; the astute reader will be wondering, for instance, how objects and their instance members come into play, or what the with statement does to things. Alas, we can't tackle everything all at once...)
But how can I know when I'm writing updateDisplay that the scope chain will look like that? I mean, doesn't it depend on who's calling the function? Nope. And that brings us to our next point:
#3: Functions in JavaScript are lexically scoped
JS中的函数是词法作用域的
Okay, so we get the concept of the call object, which is created when we call a function; and we get the global object, which sits at the bottom of the scope chain to handle globals. But what's this "lexically scoped" thing? It's just a fancy way of saying that a function's scope is determined by where it's defined (e.g., the text defining it; léxis is Greek for "word" or "speech"), not where it's called.
‘lexically scoped’的意思是说函数的作用域上下文是由声明的地方来决定的 而不是调用来决定的
Let's put that another way: When a function is defined, the code defining it is in some kind of context — a function is running, or the page itself is running if you've done the code at the top level.
当函数被定义的时候,定义那段函数的代码在一段上下文中,因为 一个函数在运行,执行到定义部分了,或是在整个script最外层执行,执行到定义函数的代码部分了.
That context has an active scope chain when the function is defined. So when creating the function object, the interpreter creates a property on it (called [[Scope]] in the ECMA spec, but you can't access it directly) with a reference to the active execution context's scope chain.
那个上下文会有一个 active scope chain ,当创建一个函数的时候,解释器会给函数对象创建一个[[Scope]]属性,指向当前 active execution context 的 scope chain
Even when the context goes away (e.g., the function returns), because the function object created by the definition has a reference to the scope chain, the scope chain isn't garbage collected — it's kept alive by the active reference to it from our function object.
甚至当切换了当前上下文时候(比如函数return了) ,由于声明函数时引用了 scope chain,
sope chain没有被垃圾回收 – 因为我们的函数对象还引用着它 (--译注:接着看下面一段话,..因为我们还引用着这个函数对象,这个函数对象还引用着它
(Assuming that we've kept a reference to the function object, as we did in our example by passing it into the jsonRequest method; otherwise, the function and the scope chain are both garbage-collected since nothing references them.)
( 假设我们有着对我们这个函数对象的引用,就像我们的例子中,将它传给了jsonRequest,否则函数和scope chain 都会被垃圾回收,因为没有东西引用他们了)
Remember our closure at the beginning of this post? It gets a [[Scope]] property pointing to the scope chain in effect when updateDisplay was called with panel = leftPanel, etc.:
记着我们文章开始时提到的闭包吗,它有一个[[Scope]]属性,指向 scope chain ,当updateDisplay 被调用传入panel = leftPanel的时候, 以此类推..
(图片的意思是: 当解释器执行到callback这个closure的时候,上下文scope chain的顶端是updateDisplay创建的call object,底端是global object, callback closure function object内部会有一个[[Scope]]属性指向 这个scope chain
So when we call it, first its scope chain is put in place, then the call object for this particular call to the function is put on top of the scope chain, and the function is executed with this chain:
然后之后就开始调用了,
所以当我们调用的时候,首先scope chain被摆好了是吧,然后call object会被创建并放在 scope chain的最上边,然后函数会沿着下面的chain来执行:
And there we are, the closure can access panel
and all of the other properties of the call object for the call to updateDisplay
because they're on the scope chain. They're on the scope chain because that was stuck onto the closure's function object when it was created. No magic. Just objects, the scope chain, and lexical scoping all working together.
然后closure就能访问panel和updateDisplay调用产生的call object中的所有其它的属性了
#4: Closures don't cause memory leaks
闭包不会创建内存泄漏
So why do people think closures cause memory leaks? A couple of reasons, I suspect, but chief among them being a bug in an issue with Internet Explorer. IE's DOM objects are not garbage-collected in the same way that JavaScript objects are; instead, IE shows its COM roots by using reference counting, a mechanism that is, unfortunately, prey to issues with circular references. Event handlers can easily end up being circular references, and so in IE it's easy to "leak" the memory associated with a DOM element and its event handler because they're referring to each other. (JavaScript's garbage collector doesn't have an issue with circular references; it works on the "reachability" principle rather than reference counting.) This is an issue with event handlers, rather than closures, but since many event handlers are written as closures, people associate it with closures. Fortunately, it's not that hard to kick IE into shape (in this particular way); Crockford talks about the problem and its solution here and many toolkits [like Prototype] will do this for you if you ask them nicely.
为什么人们觉得闭包会引起内存泄漏呢?
主要是以前IE的一个问题,
IE的DOM objects的垃圾回收机制和标准js有一些不一样.
IE使用COM roots 使用 引用计数.
不行的是,会引起循环引用.
Event handlers可能会引起循环即使,
所以ie上 一个DOM元素和它的event handler间 比较容易引起”泄露”
JS的垃圾回收机制就 不会产生循环引用的问题,它是通过”reachability(可达性分析)”这个机制而不是引用计数. 这是一个event handlers的issue ,而不是闭包,但是因为很多event handlers都是闭包,所以人们就把这件事和闭包联系起来了. 幸运的是,修复这件事很简单.有很多工具.
Lest I be accused of Microsoft-bashing, I should point out that IE is not the only browser that sometimes loses track of things; it's just by far the worst.
免得被说我黑微软,我应该得说IE不是唯一出现这种问题的浏览器,它只是最烂的,
Firefox 2 has a bad habit of leaking a bit of memory on XMLHttpRequest calls, which also frequently involve closures. The good news there is that Firefox 3 beta 3 is looking awfully good indeed on this front.
Firefox2 在发XMLHttpRequest的时候也会出这种问题
Separate from browser issues, though, if you're not really clear on how closures work, particularly with regard to the scope chain, you'll miss the fact that a closure keeps a reference to all of the variables and arguments in scope where it's created, not just the ones it uses;
如果你不真正的了解闭包是怎么工作的,像上面我讲的那些,不懂作用域链, 你可能就会不明白 其实闭包保留了它创建环境的所有的变量和参数, 不是只是它使用的那些变量和参数.
and so if you have (say) a massive temporary array allocated in that scope that the closure doesn't use, you might be tempted to say that the closure is causing the array's memory to leak. (The answer there is simple: Clear the array's variable when you're done with it.)
所以如果你有一个巨大的临时的数组,在某个作用域里,虽然闭包实际没有使用它, 泥可能会想说 闭包 导致了数组的内存泄漏( 如果你受不了,你可以直接清除这个数组变量)
In conclusion
结论
There are lots of nifty things you can do with closures. I'll post a follow-up in a couple of days, "Closures By Example", demonstrating a few of them and just generally walking through some seemingly-complicated examples. But until then, I'll just reiterate my theme: Closures in JavaScript are not complicated. They're powerful, but if you understand that 1. Everything is a data structure, 2. Variables — I should probably call them unqualified references — are resolved according to a scope chain, and 3. The scope chain for a function is defined by where the function is defined lexically, you'll be golden.
对于闭包还有很多骚操作.后面我发一些内容来演示. 但是到这儿我我还是重申一下我的主题:JavaScript中的闭包并不复杂.你只需要明白 1.所有的一切都是数据结构 2.变量-或许我应该叫他们unqualified references(未被保留,未被处理的那些引用)- 会被通过scope chain来处理解析 3.一个函数的作用域链是由函数声明定义的位置决定的,是词法的.
Oh! I said I'd tell you something at the end that would surprise you for about three seconds before you said "Oh, of course." Here it is:
All JavaScript functions are closures.
结尾我想告诉你的一个事情是,
所有的JavaScript函数都是闭包
-译注: 这里的意思应该是 因为所有的函数内部都有 bindings, 其实js中的函数本质就是一种object, 调用函数时就会在创建一大堆bindings,符合了作者给闭包的定义, A closure is a function with data intrinsically bound to it.