digdeep

凡是过去,皆是序幕。Read the fucking manual and source code.

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

转自:http://blog.leapoahead.com/2015/09/15/js-closure/

我研究JavaScript闭包(closure)已经有一段时间了。我之前只是学会了如何使用它们,而没有透彻地了解它们具体是如何运作的。那么,究竟什么是闭包?

Wikipedia 给出的解释并没有太大的帮助。闭包是什么时候被创建的,什么时候被销毁的?具体的实现又是怎么样的?

"use strict";
var myClosure = (function outerFunction() {
  var hidden = 1;
  return {
    inc: function innerFunction() {
      return hidden++;
    }
  };
}());
myClosure.inc();  // 返回 1
myClosure.inc();  // 返回 2
myClosure.inc();  // 返回 3

// 相信对JS熟悉的朋友都能很快理解这段代码
// 那么在这段代码运行的背后究竟发生了怎样的事情呢?

现在,我终于知道了答案,我感到很兴奋并且决定向大家解释这个答案。至少,我一定是不会忘记这个答案的。

Tell me and I forget. Teach me and I remember. Involve me and I learn.© Benjamin Franklin

并且,在我阅读与闭包相关的现存的资料时,我很努力地尝试着去在脑海中想想每个事物之间的联系:对象之间是如何引用的,对象之间的继承关系是什么,等等。我找不到关于这些负责关系的很好的图表,于是我决定自己画一些。

我将假设读者对JavaScript已经比较熟悉了,知道什么是全局对象,知道函数在JavaScript当中是“first-class objects”,等等。

作用域链(Scope Chain)

当JavaScript在运行的时候,它需要一些空间让它来存储本地变量(local variables)。我们将这些空间称为作用域对象(Scope object),有时候也称作 Lexical Environment 。例如,当你调用函数时,函数定义了一些本地变量,这些变量就被存储在一个作用域对象中。你可以将作用域函数想象成一个普通的JavaScript对象, 但是有一个很大的区别就是你不能够直接在JavaScript当中直接获取这个对象。你只可以修改这个对象的属性,但是你不能够获取这个对象的引用。

作用域对象的概念使得JavaScript和C、C++非常不同。在C、C++中,本地变量被保存在栈(stack)中。 在JavaScript中,作用域对象是在堆中被创建的(至少表现出来的行为是这样的),所以在函数返回后它们也还是能够被访问到而不被销毁。

正如你做想的,作用域对象是可以有父作用域对象(parent scope object)的。当代码试图访问一个变量的时候,解释器将在当前的作用域对象中查找这个属性。如果这个属性不存在,那么解释器就会在父作用域对象中查找 这个属性。就这样,一直向父作用域对象查找,直到找到该属性或者再也没有父作用域对象。我们将这个查找变量的过程中所经过的作用域对象乘坐作用域链 (Scope chain)。

在作用域链中查找变量的过程和原型继承(prototypal inheritance)有着非常相似之处。但是,非常不一样的地方在于,当你在原型链(prototype chain)中找不到一个属性的时候,并不会引发一个错误,而是会得到 undefined 。但是如果你试图访问一个作用域链中不存在的属性的话,你就会得到一个 ReferenceError

在作用域链的最顶层的元素就是全局对象(Global Object)了。运行在全局环境的JavaScript代码中,作用域链始终只含有一个元素,那就是全局对象。所以,当你在全局环境中定义变量的时候, 它们就会被定义到全局对象中。当函数被调用的时候,作用域链就会包含多个作用域对象。

全局环境中运行的代码

好了,理论就说到这里。接下来我们来从实际的代码入手。

// my_script.js
"use strict";

var foo = 1;
var bar = 2;

我们在全局环境中创建了两个变量。正如我刚才所说,此时的作用域对象就是全局对象。

在上面的代码中,我们有一个执行的上下文( myscript.js 自身的代码),以及它所引用的作用域对象。全局对象里面还含有很多不同的属性,在这里我们就忽略掉了。

没有被嵌套的函数(Non-nested functions)

接下来,我们看这段代码

"use strict";
var foo = 1;
var bar = 2;

function myFunc() {
  //-- define local-to-function variables
  var a = 1;
  var b = 2;
  var foo = 3;
  console.log("inside myFunc");
}

console.log("outside");
//-- and then, call it:
myFunc();

myFunc 被定义的时候, myFunc 的标识符(identifier)就被加到了当前的作用域对象中(在这里就是全局对象),并且这个标识符所引用的是一个函数对象(function object)。函数对象中所包含的是函数的源代码以及其他的属性。其中一个我们所关心的属性就是内部属性 [[scope]][[scope]] 所指向的就是当前的作用域对象。也就是指的就是函数的标识符被创建的时候,我们所能够直接访问的那个作用域对象(在这里就是全局对象)。

“直接访问”的意思就是,在当前作用域链中,该作用域对象处于最底层,没有子作用域对象。

所以,在console.log("outside")被运行之前,对象之间的关系是如下图所示。

对象之间的关系

温习一下。myFunc所引用的函数对象其本身不仅仅含有函数的代码,并且还含有指向其被创建的时候的作用域对象。这一点非常重要!

myFunc函数被调用的时候,一个新的作用域对象被创建了。新的作用域对象中包含myFunc函数所定义的本地变量,以及其参数(arguments)。这个新的作用域对象的父作用域对象就是在运行myFunc时我们所能直接访问的那个作用域对象。

所以,当myFunc被执行的时候,对象之间的关系如下图所示。

对象之间的关系(函数执行后)

现在我们就拥有了一个作用域链。当我们试图在myFunc当中访问某些变量的时候,JavaScript会先在其能直接访问的作用域对象(这里就是myFunc() scope)当中查找这个属性。如果找不到,那么就在它的父作用域对象当中查找(在这里就是Global Object)。如果一直往上找,找到没有父作用域对象为止还没有找到的话,那么就会抛出一个ReferenceError

例如,如果我们在myFunc中要访问a这个变量,那么在myFunc scope当中就可以找到它,得到值为1

如果我们尝试访问foo,我们就会在myFunc() scope中得到3。只有在myFunc() scope里面找不到foo的时候,JavaScript才会往Global Object去查找。所以,这里我们不会访问到Global Object里面的foo

如果我们尝试访问bar,我们在myFunc() scope当中找不到它,于是就会在Global Object当中查找,因此查找到2。

很重要的是,只要这些作用域对象依然被引用,它们就不会被垃圾回收器(garbage collector)销毁,我们就一直能访问它们。当然,当引用一个作用域对象的最后一个引用被解除的时候,并不代表垃圾回收器会立刻回收它,只是它现在可以被回收了

所以,当myFunc()返回的时候,再也没有人引用myFunc() scope了。当垃圾回收结束后,对象之间的关系变成回了调用前的关系。

对象之间的关系恢复

接下来,为了图表直观起见,我将不再将函数对象画出来。但是,请永远记着,函数对象里面的[[scope]]属性,保存着该函数被定义的时候所能够直接访问的作用域对象。


嵌套的函数(Nested functions)

正如前面所说,当一个函数返回后,没有其他对象会保存对其的引用。所以,它就可能被垃圾回收器回收。但是如果我们在函数当中定义嵌套的函数并且返回,被调用函数的一方所存储呢?(如下面的代码)

function myFunc() {
  return innerFunc() {
    // ...
  }
}

var innerFunc = myFunc();

你已经知道的是,函数对象中总是有一个 [[scope]] 属性,保存着该函数被定义的时候所能够直接访问的作用域对象。所以,当我们在定义嵌套的函数的时候,这个嵌套的函数的 [[scope]] 就会引用外围函数(Outer function)的当前作用域对象。

如果我们将这个嵌套函数返回,并被另外一个地方的标识符所引用的话,那么这个嵌套函数及其 [[scope]] 所引用的作用域对象就不会被垃圾回收所销毁。

"use strict";

function createCounter(initial) {
  var counter = initial;

  function increment(value) {
    counter += value;
  }

  function get() {
    return counter;
  }

  return {
    increment: increment,
    get: get
  };
}

var myCounter = createCounter(100);

console.log(myCounter.get());   // 返回 100
myCounter.increment(5);
console.log(myCounter.get());   // 返回 105

当我们调用 createCounter(100) 的那一瞬间,对象之间的关系如下图

调用createCounter(100)时对象间的关系

注意 incrementget 函数都存有指向 createCounter(100) scope 的引用。如果 createCounter(100) 没有任何返回值,那么 createCounter(100) scope 不再被引用,于是就可以被垃圾回收。但是因为 createCounter(100) 实际上是有返回值的,并且返回值被存储在了 myCounter 中,所以对象之间的引用关系变成了如下图所示

所以, createCounter(100) 虽然已经返回了,但是它的作用域对象依然存在,可以 且仅只能 被嵌套的函数( incrementget )所访问。

让我们试着运行 myCounter.get() 。刚才说过,函数被调用的时候会创建一个新的作用域对象,并且该作用域对象的父作用域对象会是当前可以直接访问的作用域对象。所以,当 myCounter.get() 被调用时的一瞬间,对象之间的关系如下。

myCounter.get() 运行的过程中,作用域链最底层的对象就是 get() scope ,这是一个空对象。所以,当 myCounter.get() 访问 counter 变量时,JavaScript在 get() scope 中找不到这个属性,于是就向上到 createCounter(100) scope 当中查找。然后, myCounter.get() 将这个值返回。

调用 myCounter.increment(5) 的时候,事情变得更有趣了,因为这个时候函数调用的时候传入了参数。

正如你所见, increment(5) 的调用创建了一个新的作用域对象,并且其中含有传入的参数 value 。当这个函数尝试访问 value 的时候,JavaScript立刻就能在当前的作用域对象找到它。然而,这个函数试图访问 counter 的时候,JavaScript无法在当前的作用域对象找到它,于是就会在其父作用域 createCounter(100) scope 中查找。

我们可以注意到,在 createCounter 函数之外,除了被返回的 getincrement 两个方法,没有其他的地方可以访问到 value 这个变量了。 这就是用闭包实现“私有变量”的方法

我们注意到 initial 变量也被存储在 createCounter() 所创建的作用域对象中,尽管它没有被用到。所以,我们实际上可以去掉 var counter = initial; ,将 initial 改名为 counter 。但是为了代码的可读性起见,我们保留原有的代码不做变化。

需要注意的是作用域链是不会被复制的。每次函数调用只会往作用域链下面新增一个作用域对象。所以,如果在函数调用的过程当中对作用域链中的任何一个作用域对象的变量进行修改的话,那么同时作用域链中也拥有该作用域对象的函数对象也是能够访问到这个变化后的变量的。

这也就是为什么下面这个大家都很熟悉的例子会不能产出我们想要的结果。

"use strict";

var elems = document.getElementsByClassName("myClass"), i;

for (i = 0; i < elems.length; i++) {
  elems[i].addEventListener("click", function () {
    this.innerHTML = i;
  });
}

在上面的循环中创建了多个函数对象,所有的函数对象的 [[scope]] 都保存着对当前作用域对象的引用。而变量 i 正好就在当前作用域链中,所以循环每次对 i 的修改,对于每个函数对象都是能够看到的。

“看起来一样的”函数,不一样的作用域对象

现在我们来看一个更有趣的例子。

"use strict";

function createCounter(initial) {
  // ...
}

var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);

myCounter1myCounter2 被创建后,对象之间的关系为

在上面的例子中, myCounter1.incrementmyCounter2.increment 的函数对象拥有着一样的代码以及一样的属性值( namelength 等等),但是它们的 [[scope]] 指向的是 不一样的作用域对象

这才有了下面的结果

var a, b;
a = myCounter1.get();   // a 等于 100
b = myCounter2.get();   // b 等于 200

myCounter1.increment(1);
myCounter1.increment(2);

myCounter2.increment(5);

a = myCounter1.get();   // a 等于 103
b = myCounter2.get();   // b 等于 205

作用域链和 this

this 的值不会被保存在作用域链中, this 的值取决于函数被调用的时候的情景。

译者注:对这部分,译者自己曾经写过一篇更加详尽的文章,请参考 《用自然语言的角度理解JavaScript中的this关键字》 。原文的这一部分以及“ this 在嵌套的函数中的使用”译者便不再翻译。

总结

让我们来回想我们在本文开头提到的一些问题。

  • 什么是闭包?闭包就是同时含有对函数对象以及作用域对象引用的最想。实际上,所有JavaScript对象都是闭包。
  • 闭包是什么时候被创建的?因为所有JavaScript对象都是闭包,因此,当你定义一个函数的时候,你就定义了一个闭包。
  • 闭包是什么时候被销毁的?当它不被任何其他的对象引用的时候。

专有名词翻译表

本文采用下面的专有名词翻译表,如有更好的翻译请告知,尤其是加 * 的翻译

  • *全局环境中运行的代码:top-level code
  • 参数:arguments
  • 作用域对象:Scope object
  • 作用域链:Scope Chain
  • 栈:stack
  • 原型继承:prototypal inheritance
  • 原型链:prototype chain
  • 全局对象:Global Object
  • 标识符:identifier
  • 垃圾回收器:garbage collector

著作权声明

本文经授权翻译自 How do JavaScript closures work under the hood

译者对原文进行了描述上的一些修改。但在没有特殊注明的情况下,译者表述的意思和原文保持一致。

-----------------------------------------------------------附上原文----------------------------------------------------

http://dmitryfrank.com/articles/js_closures

How do JavaScript closures work under the hood

You're reading the original article in English. You can as well read the translation in Russian.

If you have translated the article to different language, please leave a comment or write me an email, so that I can update the list. Thank you.


I've been using closures for quite some time already. I learned how to use them, but I didn't have clear understanding of how closures actually work, and what's going on behind the scenes when I use them. What the closure is exactly, to begin with? Wikipedia doesn't help very much. When it is created and deleted? What the implementation should look like?

"use strict";
 
var myClosure = (function outerFunction() {
 
  var hidden = 1;
 
  return {
    inc: function innerFunction() {
      return hidden++;
    }
  };
 
}());
 
myClosure.inc();  // returns 1
myClosure.inc();  // returns 2
myClosure.inc();  // returns 3
 
// Ok, very nice. But how is it implemented,
// and what's going on behind the scenes?

And when I eventually got it, I felt excited and decided to explain it: at least, I will definitely not forget it now. You know,

Tell me and I forget. Teach me and I remember. Involve me and I learn.
© Benjamin Franklin

And, after all, while I was reading existing explanations of closures, I tried hard to imagine visually how things relate to each other: which object references others, which one inherits from another, etc. I failed to find such useful illustrations, so, I decided to draw my own.

I assume that the reader is already familiar with JavaScript, knows what is a Global Object, knows that functions in JavaScript are “first-class objects”, etc.

Scope chain

When any JavaScript code is executing, it needs some place to store its local variables. Let's call this place as a scope object (some refer to it as a LexicalEnvironment). For example, when you invoke some function, and function defines local variables, these variables are saved on the scope object. You can think of it as a regular JavaScript object, with the notable difference that you can't refer to the whole object directly. You can only modify its properties, but you can't refer to the scope object itself.

This concept of scope object is very different from, say, C or C++, where local variables are stored on stack. In JavaScript, scope objects are allocated in heap instead (or at least they behave like this), so they might stay allocated even if function already returned. More on that later.

As you might expect, scope object might have parent. When the code tries to access some variable, interpreter looks for the property of current scope object. If the property doesn't exist, interpreter moves to the parent scope object, and looks there. And so on, until the value is found, or there's no more parent. Let's call this sequence of scope objects as a scope chain.

The behavior of resolving a variable on scope chain is very similar to that of prototypal inheritance, with, again, one notable difference: if you try to access some non-existing property of regular object, and prototype chain doesn't contain this property either, it's not an error: undefined is silently returned. But if you try to access non-existing property on the scope chain (i.e. access non-existing variable), then ReferenceError occurs.

The last element in the scope chain is always the Global Object. In top-level JavaScript code, scope chain contains just a single element: the Global Object. So, when you define some variables in top-level code, they are defined on Global Object. When some function is invoked, scope chain consists of more than one object. You might expect that if function is called from top-level code, then scope chain is guaranteed to contain exactly 2 scope objects, but this is not necessarily true! There might be 2 or more scope objects; it depends on the function. More on that later.

Top-level code

Ok, enough theory, let's try something concrete. Here is a very simple example:

my_script.js
"use strict";
 
var foo = 1;
var bar = 2;

We just create two variables in top-level code. As I mentioned above, for top-level code, scope object is a Global Object:

In the diagram above, we have execution context (which is just top-level my_script.js code), and the scope object referenced by it. Of course, in real world, Global Object contains lots of standard- and host-specific stuff, but it's not reflected in the diagrams.

Non-nested functions

Now, consider this script:

my_script.js
"use strict";
var foo = 1;
var bar = 2;
 
function myFunc() {
  //-- define local-to-function variables
  var a = 1;
  var b = 2;
  var foo = 3;
 
  console.log("inside myFunc");
}
 
console.log("outside");
 
//-- and then, call it:
myFunc();

When the function myFunc is defined, myFunc identifier is added to the current scope object (in this case, Global object), and this identifier refers to the function object. The function object holds function's code as well as other properties. One of the properties that we're interested in is an internal property [[scope]]; it refers to the current scope object, that is, the scope object that was active when the function is defined (again, in this case, Global object).

So, by the time the console.log(“outside”); is executed, we have the following arrangement:

Again, take a moment to reflect on it: the function object referred by myFunc variable not only holds the function code, but also refers to the scope object which was in effect when the function is defined. This is very important.

And when the function is invoked, new scope object is created, which keeps local variables for myFunc (as well as its argument values), and this new scope object inherits from the scope object referenced by the function being invoked.

So, when myFunc is actually invoked, the arrangement becomes as follows:

What we have now is a scope chain: if we try to access some variable inside myFunc, JavaScript will try to find it on the first scope object: myFunc() scope. If the lookup fails, then go to the next scope object in the chain (here, it's Global object), and look there. If requested property is not a part of all scope objects in the chain, then ReferenceError occurs.

For example, if we access a from myFunc, we get value 1 from the first scope object myFunc() scope. If we access foo, we get value 3 from the same myFunc() scope: it effectively hides the foo property of the Global object. If we access bar, we get value 2 from Global object. It works pretty much like prototypal inheritance.

It's important to note that these scope objects are persisted as long as there are references to them. When the last reference to some particular scope object is dropped, this scope object will be garbage-collected on occasion.

So, when myFunc() returns, nobody references myFunc() scope anymore, it gets garbage-collected, and we end up with previous arrangement again:

From now on, I won't include explicit function objects in the diagrams, since diagrams become too overloaded otherwise. You already know: any reference to a function in JavaScript refers to function object, which, in turn, has a reference to the scope object.

Always keep this in mind.

Nested functions

As we've seen from the previous discussion, when function returns, nobody else references its scope object, and so, it gets garbage-collected. But what if we define nested function and return it (or store somewhere outside)? You already know: function object always refers to the scope object in which it was created. So, when we define nested function, it gets reference to the current scope object of outer function. And if we store that nested function somewhere outside, then the scope object won't be garbage collected even after outer function returns: there are still references to it! Consider this code:

my_script.js
"use strict";
 
function createCounter(initial) {
  //-- define local-to-function variables
  var counter = initial;
 
  //-- define nested functions. Each of them will have
  //   a reference to the current scope object
 
  /**
   * Increments internal counter by given value.
   * If given value is not a finite number or is less than 1, then 1 is used.
   */
  function increment(value) {
    if (!isFinite(value) || value < 1){
      value = 1;
    }
    counter += value;
  }
 
  /**
   * Returns current counter value.
   */
  function get() {
    return counter;
  }
 
 
  //-- return object containing references
  //   to nested functions
  return {
    increment: increment,
    get: get
  };
}
 
//-- create counter object
var myCounter = createCounter(100);
 
console.log(myCounter.get());   //-- prints "100"
 
myCounter.increment(5);
console.log(myCounter.get());   //-- prints "105"

When we call createCounter(100);, we have the following arrangement:

Notice that createCounter(100) scope is referenced by nested functions increment and get. If createCounter() returned nothing, then, of course, these inner self-references wouldn't be counted, and the scope would be garbage-collected anyway. But since createCounter() returns object containing references to these functions, we have the following:

Take some time to reflect on it: the function createCounter(100) already returned, but its scope is still there, accessible by the inner functions, and only by these functions. It is truly impossible to access createCounter(100) scope object directly, we can only call myCounter.increment() or myCounter.get(). These functions have unique private access to the scope of createCounter.

Let's try to call, for example, myCounter.get(). Recall that when any function is called, new scope object is created, and scope chain that is referenced by the function is augmented with this new scope object. So, when myCounter.get() is called, what we have is:

The first scope object in the chain for function get() is the empty object get() scope. So, when get() accesses counter variable, JavaScript fails to find it on the first object in the scope chain, moves to the next scope object, and uses counter variable on createCounter(100) scope. And get() function just returns it.

You may have noticed that myCounter object is additionally given to the function myCounter.get() as this (denoted by red arrow on the diagram). This is because this is never a part of the scope chain, and you should be aware of it. More on that later.

Calling increment(5) is a bit more interesting, since this function has an argument:

As you see, the argument value is stored in the scope object that was created just for a single call increment(5). When this function accesses variable value, JavaScript immediately locates it on the first object in the scope chain. When, however, the function accesses counter, JavaScript fails to find it on the first object in the scope chain, moves to the next scope object, and locates it there. So, increment() modifies the counter variable on createCounter(100) scope. And virtually nobody else can modify this variable. This is why closures are so powerful: the myCounter object is impossible to compromise. Closures are very appropriate place to store private things.

Notice that the argument initial is also stored in the scope object of createCounter(), even though it is not used. So, we can save a bit of memory if we get rid of explicit var counter = initial;, rename initial to counter, and use it directly. But, for clarity, we have explicit argument initial and var counter.

It is important to highlight that bound scopes are “live”. When function is invoked, current scope chain is not copied for this function: the scope chain is just augmented with new scope object, and when any scope object in the chain is modified by any function, this change is immediately observable by all functions that have this scope object in their scope chains. When increment() modifies counter value, the next call to get() will return updated value.

This is why this well-known example doesn't work:

"use strict";
 
var elems = document.getElementsByClassName("myClass"), i;
 
for (i = 0; i < elems.length; i++) {
  elems[i].addEventListener("click", function () {
    this.innerHTML = i;
  });
}

Multiple functions are created in the loop, and all of them have reference to the same scope object in their scope chains. So, they use exactly the same variable i, not private copies of it. For further explanation of this particular example, see this link: Don't make functions within a loop.

Similar function objects, different scope objects

Now, let's try to expand a bit our counter example and have more fun. What if we create more than one counter objects? It doesn't hurt:

my_script.js
"use strict";
 
function createCounter(initial) {
  /* ... see the code from previous example ... */
}
 
//-- create counter objects
var myCounter1 = createCounter(100);
var myCounter2 = createCounter(200);

When both myCounter1 and myCounter2 are created, we have the following:

Be sure to remember that each function object has a reference to the scope object. So, in the example above, myCounter1.increment and myCounter2.increment refer to function objects that have exactly the same code and the same property values (name, length, and others), but their [[scope]] refer to different scope objects.

Diagram does not contain separate function objects, for the sake of simplicity, but they are still there.

Some examples:

var a, b;
a = myCounter1.get();   // a equals 100
b = myCounter2.get();   // b equals 200
 
myCounter1.increment(1);
myCounter1.increment(2);
 
myCounter2.increment(5);
 
a = myCounter1.get();   // a equals 103
b = myCounter2.get();   // b equals 205

So, this is how it works. The concept of closures is very powerful.

Scope chain and "this"

Like it or not, this is not saved as a part of the scope chain at all. Instead, value of this depends on the function invocation pattern: that is, you may call the same function with different values given as this.

Invocation patterns

This topic pretty much deserves its own article, so I won't go deeply inside, but as a quick overview, there are four invocation patterns. Here we go:

Method invocation pattern

"use strict";
 
var myObj = {
  myProp: 100,
  myFunc: function myFunc() {
    return this.myProp;
  }
};
myObj.myFunc();  //-- returned 100

If an invocation expression contains a refinement (a dot, or [subscript]), the function is invoked as a method. So, in the example above, this given to myFunc() is a reference to myObj.

Function invocation pattern

"use strict";
 
function myFunc() {
  return this;
}
myFunc();   //-- returns undefined

When there's no refinement, then it depends on whether the code runs in strict mode:

  • in strict mode, this is undefined
  • in non-strict mode, this points to Global Object

Since the code above runs in strict mode thanks to “use strict”;, myFunc() returns undefined.

Constructor invocation pattern

"use strict";
 
function MyObj() {
  this.a = 'a';
  this.b = 'b';
}
var myObj = new MyObj();

When function is called with new prefix, JavaScripts allocates new object which inherits from the function's prototype property, and this newly allocated object is given to the function as this.

Apply invocation pattern

"use strict";
 
function myFunc(myArg) {
  return this.myProp + " " + myArg;
}
 
var result = myFunc.apply(
  { myProp: "prop" },
  [ "arg" ]
);
//-- result is "prop arg"

We can pass arbitrary value as this. In the example above, we use Function.prototype.apply() for that. Beyond that, see also:

In the examples that follow, we will primarily use Method invocation pattern.

Usage of "this" in nested functions

Consider:

"use strict";
 
var myObj = {
 
  myProp: "outer-value",
  createInnerObj: function createInnerObj() {
 
    var hidden = "value-in-closure";
 
    return {
      myProp: "inner-value",
      innerFunc: function innerFunc() {
        return "hidden: '" + hidden + "', myProp: '" + this.myProp + "'";
      }
    };
 
  }
};
 
var myInnerObj = myObj.createInnerObj();
console.log( myInnerObj.innerFunc() );

Prints: hidden: 'value-in-closure', myProp: 'inner-value'

By the time myObj.createInnerObj() is called, we have the following arrangement:

And when we call myInnerObj.innerFunc(), it looks as follows:

From the above, it's clear that this given to myObj.createInnerObj() points to myObj, but this given to myInnerObj.innerFunc() points to myInnerObj: both functions are called with Method invocation pattern, as explained above. That's why this.myProp inside innerFunc() evaluates to “inner-value”, not “outer-value”.

So, we can easily trick innerFunc() into using different myProp, like this:

/* ... see the definition of myObj above ... */
 
var myInnerObj = myObj.createInnerObj();
var fakeObject = {
  myProp: "fake-inner-value",
  innerFunc: myInnerObj.innerFunc
};
console.log( fakeObject.innerFunc() );

Prints: hidden: 'value-in-closure', myProp: 'fake-inner-value'

Or with apply() or call():

/* ... see the definition of myObj above ... */
 
var myInnerObj = myObj.createInnerObj();
console.log(
  myInnerObj.innerFunc.call(
    {
      myProp: "fake-inner-value-2",
    }
  )
);

Prints: hidden: 'value-in-closure', myProp: 'fake-inner-value-2'

Sometimes, however, inner function actually needs access to this given to outer function, independently of the way inner function is invoked. There is a common idiom for that: we need to explicitly save needed value in the closure (that is, in the current scope object), like: var self = this;, and use self in inner function, instead of this. Consider:

"use strict";
 
var myObj = {
 
  myProp: "outer-value",
  createInnerObj: function createInnerObj() {
 
    var self = this;
    var hidden = "value-in-closure";
 
    return {
      myProp: "inner-value",
      innerFunc: function innerFunc() {
        return "hidden: '" + hidden + "', myProp: '" + self.myProp + "'";
      }
    };
 
  }
};
 
var myInnerObj = myObj.createInnerObj();
console.log( myInnerObj.innerFunc() );

Prints: hidden: 'value-in-closure', myProp: 'outer-value'

This way, we have the following:

As you see, this time, innerFunc() has access to the value given as this to the outer function, through the self stored in the closure.

Conclusion

Let's answer a couple of questions from the beginning of the article:

  • What the closure is? - It is the object that refers both to function object and the scope object. Actually, all JavaScript functions are closures: it's impossible to have the reference to function object without scope object.
  • When is it created? - Since all JavaScript functions are closures, it's obvious: when you define a function, you actually define a closure. So, it is created when the function is defined. But make sure you distinguish between closure creation and new scope object creation: the closure (function + reference to the current scope chain) is created when the function is defined, but new scope object is created (and used to augment the closure's scope chain) when the function is invoked.
  • When is it deleted? - Just like any regular object in JavaScript, it is garbage-collected when there are no references to it.

Further reading:

  • JavaScript: The Good Parts by Douglas Crockford. It's surely nice to understand how do closures work, but it's probably even more important to understand how to use them correctly. The book is very concise, and it contains lots of great and useful patterns.
  • JavaScript: The Definitive Guide by David Flanagan. As the title suggests, this book explains the language in a very detailed way.
posted on 2015-09-17 17:49  digdeep  阅读(869)  评论(0编辑  收藏  举报
不懂数据库和Web安全的架构师不是一个好的程序员。