(翻译) How variables are allocated memory in Javascript? | scope chain | lexicial scope

总结: 阅读下面文章需要15分钟

提问者的问题是JavaScript中内存是怎么分配的,在介绍的过程作者涉及计到了JS Scope Chain和调用函数call生成lexicial environmentenvironment record(被作者合并称为 binding objects)的过程.非常值得一看

 

 

个人翻译:

 

How variables are allocated memory in Javascript?

 

 

It's actually a very interesting area of JavaScript, and there are at least two answers:

  • An answer in terms of what the specification defines, and
  • An answer in terms of what JavaScript engines actually do, which may be optimized (and often is)

 

 

实际上这是JavaScript中非常有意思的一部分内容.具体你可以在规范中查阅,

这里给出两个方向的参考答案:

  1. 就规范而言..是怎么定义的
  2. 就特定的js引擎而言..是怎么做的(这些引擎还通常都包含一些优化的内容)

 

 

In terms of the specification: JavaScript's way of handling local variables is quite different from the way C does it. When you call a function, amongst other things a lexical environment for that call is created, which has something called an environment record. To keep things simple, I'm going to refer to them both together as the "binding object" (there's a good reason they're separate in the specification, though; if you want to get deeper into it, set aside a few hours and read through the spec).

 

就规范而言,Javascript处理局部变量的方式和C语言有一些不一样,当你调用(call)一个函数,

 

会创建一个 lexical environment,

内部会包含一条 environment record,

 

为了简化,我会把他们合并着称为’binding object’( 在 规范中他们是分开的,如果你想更深入的了解,可以自己去看规范.)

 

 

 

The binding object contains bindings  for the arguments to the function, all local variables declared in the function, and all functions declared within the function (along with a couple of other things).

 

binding object 包含了  bindings , 包括函数的 arguments , 函数内部定义的局部变量,和函数内部定义的函数(还包含一些其它东西)

 ---译注: 答者虽然没说,但是我想应该是还包含了this

 

 

binding is a combination of a name (like a) and the current value for the binding (along with a couple of flags we don't need to worry about here).

 

binding 是指 名称 和 当前值的一组绑定

 

 

An unqualified reference within the function (e.g., the foo in foo, but not the foo in obj.foo, which is qualified) is first checked against the binding object to see if it matches a binding on it; if it does, that binding is used.

 

 未找到的引用(比如找直接引用foo,而不是引用obj.foo) 会先从binding object里找,如果有这个binding就会用它

 

 

 

 

 

 

When a closure survives the function returning (which can happen for several reasons), the binding object for that function call is retained in memory because the closure has a reference to the binding object in place where it was created. So in specification terms, it's all about objects.

 

 

如果函数执行完return结束后还保留着一个closure(闭包) , 那个函数调用(call)生成的的binding object 会被保留在内存中,因为closure引用着binding object. 所以就规范而言, 这些变量都是声明在对象中的.(it is all about objects.  --译注: 这里是指object,C/C++中的object 一般情况对象会放在堆内存存储)

 

 

 

 

At first glance, that would suggest that the stack isn't used for local variables; in fact, modern JavaScript engines are quite smart, and may (if it's worthwhile) use the stack for locals that aren't actually used by the closure. They may even use the stack for locals that do get used by the closure, but then move them into an binding object when the function returns so the closure continues to have access to them. (Naturally, the stack is still used for keeping track of return addresses and such.)

 

这么一看,好像局部变量的不是保存在stack中的(因为会持久保留在堆内存中 后面还要一直被引用).实际上,现代的JavaScript引擎很聪明很高级了,而且有可能会使用stack来保存出不被闭包引用的局部变量,但是当函数返回时会将他们移动到binding object中,以便closures可以继续访问他们. (理所当然的,stack仍然用来跟踪函数返回地址等..)

 

 

Here's an example:

function foo(a, b) {
    var c;
 
    c = a + b;
 
    function bar(d) {
        alert("d * c = " + (d * c));
    }
 
    return bar;
}
 
var b = foo(1, 2);
b(3); // alerts "d * c = 9"

 

 

When we call foo, a binding object gets created with these bindings (according to the spec):

  • a and b — the arguments to the function
  • c — a local variable declared in the function
  • bar — a function declared within the function
  • (...and a couple of other things)

 

当调用foo的时候 创建了一个binding object,里面有这些bindings:

  1. a和b -函数的arguments)
  2. c-函数内定义的一个局部变量
  3. bar-函数内定义的一个函数
  4. (…还有一些其他的东西)

 

 

When foo executes the statement c = a + b;, it's referencing the ca, and b bindings on the binding object for that call to foo.

 

When foo returns a reference to the bar function declared inside it, bar survives the call to foo returning. Since bar has a (hidden) reference to the binding object for that specific call to foo, the binding object survives (whereas in the normal case, there would be no outstanding references to it and so it would be available for garbage collection).

 

当foo执行到c =a+b;的时候

引用了binding object中的c,a,b 这些binding

 

当foo 返回一个 bar的引用的时候 ,bar被保留了,因为bar有着对binding object的引用,所以binding object也被保留了(在正常情况下 没有被引用的话就要被垃圾回收了)

 

 

Later, when we call bar, a new binding object for that call is created with (amongst other things) a binding called d — the argument to bar. That new binding object gets a parent binding object: The one attached to bar. Together they form a "scope chain".

Unqualified references within bar are first checked against the binding object for that call to bar, so for instance, d resolves to the d binding on the binding object for the call to bar.

 

然后,当我们调用bar的时候,一个新的binding object生成了,其中一个binding叫d – bar的参数,这个binding object的父亲就是原来的binding object,他们一起形成了 “scope chain” 作用域链. bar函数内会开始查找没有被保留的引用, 举例 d 被解析成了 d binding,但是

 

But an unqualified reference that doesn't match a binding on that binding object is then then checked against its parent binding object in the scope chain, which is the binding object for the call to foo that created bar. Since that has a binding for c, that's the binding used for the identifier c within bar. E.g., in rough terms:

 

但是在binding object中找不到的引用,会接下来在它作用域链父亲的binding object上面找,foo的 binding object里有一个c,于是于是那个c binding就被bar里面c标识符使用了,粗略的说:

 

 

+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
|   global binding object   |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| ....                      |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
             ^
             | chain
             |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| `foo` call binding object |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| a = 1                     |
| b = 2                     |
| c = 3                     |
| bar = (function)          |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
             ^
             | chain
             |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| `bar` call binding object |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+
| d = 3                     |
+−−−−−−−−−−−−−−−−−−−−−−−−−−−+

Fun fact: This scope chain is how global variables work in JavaScript. Note the "global binding object" in the above. So in a function, if you use an identifier that isn't in the binding object for that function call, and isn't in any of the other binding objects between that and the global binding object, if the global binding object has a binding for it, the global binding is used. Voilà, global variables.

 

这个作用域链就是JavaScript里全局变量的工作方式 被标记了”globall binding object”

 

在一个函数里如果你用了一个标识符但是不在函数call产生的binding object里,就会一直向上找,直到global binding object.

 

 

 

(ES2015 made this a bit more interesting by having two layers to the global binding object: A layer used by old-fashioned global declarations like var and function declarations, and a layer used by newer ones like letconst, and class.

The difference is that the older layer also creates properties on the global object, which you kind of access via window on browsers, but the newer layer doesn't. So a global let declaration doesn't create a window property, but a global var declaration does.)

 

ES2015 通过向global binding object上添加了两层 让这一部分更有趣了一些

一层是使用旧风格的全局声明如var和 function declarations, 另一层使用更新的像let const class.

区别在于旧的层会在global object上创建 properties,你可以通过浏览器里的window来访问, 但新的一层不可以

所以,一个全局的let声明不会在window下创建,但是一个全局的var声明会在window下创建一个属性

 

 

 

 

Implementations are free to use whatever mechanism they want under the covers to make the above seem to happen.

It's impossible to get direct access to the binding object for a function call, and the spec makes clear that it's perfectly fine if the binding object is just a concept, rather than a literal part of the implementation.

具体的实现会根据引擎的实现机制而不同

不能直接拿到函数调用时产生的binding object

并且规范说了其实有binding object这种概念就可以,并没有具体涉及到实现的细节机制

 

A simple implementation may well just literally do what the spec says; a more complicated one may use a stack when there are no closures involved (for the speed benefit), or may always use a stack but then "tear off" the binding object needed for a closure when popping the stack. The only way to know in any specific case is to look at their code. :-)

一个简单的实现会逐字的照规范来做,更复杂的会为了优化,当涉及到closures时引入栈.. 除非你去看这些引擎的代码.

 

 

 

More about closures, the scope chain, etc. here:

 

 

 

 

资料:规范中关于lexial environment的一段截图

 

posted @ 2019-10-02 13:00  hh9515  阅读(216)  评论(0编辑  收藏  举报