JS 高程 闭包问题小解

JS 高程 闭包问题小解

keyword

  • 闭包
  • 垃圾回收(引用计数)
  • 块级作用域

我先插播一个自己以前写过的话。

自己对闭包的理解

maintains a reference to the scope where the variable was defined.

⬆️ 这句话很重要很重要,闭包一个重要的理解。在接下来的文章慢慢体会吧。
PS: 这是 Object-Oriented JavaScript 里的一句话。


一切要从小鲍可的一个问题说起:

function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = function() {
      return i;
    };
  }
  return result;
}

console.log(createFunctions()[1]());  // 10
function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = (function(num) {
      return function() {
        return num;
      };
    })(i);
  }
  return result;
}

console.log(createFunctions()[1]()); // 1

为啥子这俩东西的结果还不一样哟ヾ(⌒(´・ㅅ・`)


我们跳出来先说点别的,先把 scope 这件事提一下。

for (var i = 0; i < 10; i++) {}

console.log(i);   // 10

在上面这个例子中呢,按我们想,i 这个变量只存在于 for 循环中,循环一结束应该被销毁的,但是在之后的 console.log 中不仅还留着,而且还是保留着 10 这个值。

也就是说,实际上这个 var i 看似在 for 中定义了,实际上也会存在于 for 外层的作用域中。


const a = 1;

const foo = () => {
  console.log(a);
}

foo();  // 1

另外再来看上面这个例子,假如在自己的 scope 找不到所需的变量,会到父级的 scope 中去寻找相应的变量。


然后简单说一下垃圾回收这件事。

按引用计数来说,只要一个变量引用计数还不为零,就不会被回收,所以闭包才得以保持变量。


那上面这些东西跟我们说的问题又啥关系呐?

回到我们的问题:

function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = function() {
      return i;
    };
  }
  return result;
}

console.log(createFunctions()[1]());  // 10

当执行 createFunctions()[1]() 会执行 function() { return i; } 这个函数,

但是当前作用域中,并没有 i 这个变量。所以会到上级作用域中寻找,诶,找到了诶,还真有 i 。

但是我们要知道 ES5 中,只有 function scope 而没有 block scope 的,所以实际上每次执行 for () { 共用同一个 i }。

共用这点很重要

还记得上面说的么?

maintains a reference to the scope where the variable was defined.

也就是说当我执行闭包的时候,因为一次一次的循环递增,return i; 所返回的这个 i 在闭包真正调用的时候,值已经变成 10 了。

因为闭包返回的并不是那个值,而是一个指向,指向一个值真正被定义的地方。访问的是那个变量的本身,而不是当时的那个值。


function createFunctions() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = (function(num) {
      return function() {
        return num;
      };
    })(i);
  }
  return result;
}

那为啥这个例子又能正确返回每一次循环的 i 的值了呢?

其实这里动了点小手脚。这里使用了立即执行函数 (function() {})()。(立即执行函数是啥子咧,字面意思,详细的这里不作介绍)

也就是说,在之后执行闭包的函数时,我们访问到的实际上是 num 这个变量,而不是 i

在每次循环中,都创建了 function(num) { return num;} 这个 function scope。

这个 function scope 中,num 接受了 i 传进来的值,并因为引用计数不为零,没有被垃圾回收机制回收,保持了每次循环的 i 的值。所以在之后执行闭包的函数时,就能返回每次循环的 i。



这大概就是 JS ES5 各种知识结合之后的奇妙之处吧,说起来也挺麻烦的。

我当时事发突然,没来得及跟鲍可解释清楚,现在再来解释这一遍,希望都能多一分理解吧。

ヽ(●´∀`●)ノ


后话

其实…

到了 ES6 (ES next) 的时代,因为新关键词 let, const 实现了 block scope,事情已经没那么复杂了。

function createFunctions() {
  var result = [];
  for (let i = 0; i < 10; i++) {
    result[i] = function() {
      return i;
    };
  }
  return result;
}

console.log(createFunctions()[1]());  // 1

注意这里 for (let i = 0; i < 10; i++) 的 let 的关键词。

实现了块级作用域之后,每次循环的 i 其实并不像 var 定义的那样是同一个 i(不知道我讲的有没有错)。

每次循环,都会在相应的 block scope 重新定义一个 i,所以之后闭包调用的时候,能访问到不同的 i 。


最后,还是类似传教一般再重复一遍…

maintains a reference to the scope where the variable was defined.

晚安…

This blog is under a CC BY-NC-SA 4.0 Unported License
Link to this article: http://nhh.ink/2017/11/26/var-closures/