let-var 作用域链问题
发布于 9 年前 作者 magicdawn 6557 次浏览 最后一次编辑是 8 年前 来自 问答

之前看过汤姆大叔的博客翻译的js the core

对for循环中出现的问题可以做出解释

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}

为什么直接data[k] = function(){ alert(k) }这样做k总是3, 因为闭包 data[0] data[1] data[2]的 [[scope]]属性是一样的, 他们是在同一个上下文中创建出的闭包, 他们

  • data[0].scope = [ data0Context.VO, globalContextVO ]
  • data[1].scope = [ data1Context.VO, globalContextVO ]
  • data[2].scope = [ data2Context.VO, globalContext.VO ] 可以看到除了第一个是它们自己的VO外, 后面都是一样的, 他们引用的k不存在于他们自己的VO对象中, 由于外面k修改, 他们都跟着变了.

为什么加一层自执行函数可以.

for (var k = 0; k < 3; k++) {
  (function temp(k){
    data[k] = function () {
      alert(k);
    };
  })(k);
}

此时他们的scope属性如下

  • data[0].scope = [ data0Context.VO, tempContext.VO={ k:0}, globalContext.VO ]
  • data[1].scope = [ data1Context.VO, tempContext.VO={ k:1}, globalContext.VO ]
  • data[2].scope = [ data2Context.VO, tempContext.VO={ k:2}, globalContext.VO ] 可以看到他们的scope chain中引用到k的地方是没有重合的, 后修改for循环中的k是不会影响到他们scope chain中的k值的.

问题来了 http://dmitrysoshnikov.com/ecmascript/javascript-the-core/comment-page-3/#comment-173907

var data = [];
for (let k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}

data[0](); // 0
data[1](); // 1
data[2](); // 2

阮一峰对ES6 let类似的问题有个解释, 块级作用域, 这个我也知道, 但解决不了我的问题 http://es6.ruanyifeng.com/#docs/let

我的想法是:

  • data[0].scope = [ data0Context.VO, blockContext.VO={ k }, globalContext.VO ]
  • data[1].scope = [ data1Context.VO, blockContext.VO={ k }, globalContext.VO ]
  • data[2].scope = [ data2Context.VO, blockContext.VO={ k }, globalContext.VO ]

k存在于 let-for的块级作用域中, 而且是公共的, 就是引用的同一个k, 结果应该也是与 var 相同, 但是测试结果不是这样的, 遂求解答?

15 回复

若有所思… 此时的blockContext 应该与 前面的 tempContext 一样, 不重合~

看了下原文说的Evaluation context部分, 应该是for循环每次都会创建, blockContext中的k 值每次都不一样, 就起到了跟var+自执行函数一样的效果

简单地说, 如果for括号里面有let或者const, 那么forbody每次执行的时候都会新建一个词法环境, 并且把letconst的声明的变量名和值绑定到这个新的词法环境, 所以有上面的结果.

不是很明白,for let应该给每个循环一个单独的作用域,虽然没看过标准,但感觉标准应该做符合直觉的事情。来填坑了无数人的坑。

另外,现在各种环境的es6支持称不上完美。for of基本在任何平台对异步异常处理都不太对劲。最好babel转下……

然而最近好像碰到了个babel的错误。。。

@William17

yeah, 类似用var加一个自执行函数每次都创建一个作用域…

@reverland

for of基本在任何平台对异步异常处理都不太对劲

怎么说?

我问JavaScript the core的作者这个问题有回复了 http://dmitrysoshnikov.com/ecmascript/javascript-the-core/comment-page-3/#comment-173985

Per ES6 let is a block-scoped. So every time we enter the block of the for loop, a new environment is created, and the function created inside the block, > is created relatively to this environment.

Roughly it’s similar to (although at engine level much more optimized than this):

var data = [];
for (var k = 0; k < 3; k++) {
  (function(x) {
    data[x] = function () {
      alert(x);
    };
  })(k);
}

@magicdawn 建一个函数的话,会进入一个新的执行上下文。 而含有词法声明的for只会新建一个词法环境对象。

@magicdawn 指这个例子,不知道表述有什么问题没。只是想说珍爱生命,慎用原生es6。

参见:https://hacks.mozilla.org/2015/07/es6-in-depth-generators-continued/

function setup() {
  console.log("setup")
}

function cleanup() {
  console.log("cleanup")
}

function work(value) {
  console.log(value)
}

function* produceValues() {
  setup();
  try {
    yield "work1";
    yield "work2";
    // ... yield some values ...
  } finally {
    cleanup();
  }
}

for (var value of produceValues()) {
  work(value);
  break;
}

这个例子,我这里node v5.0.0

➜  test  node forof.js    
setup
work1

firefox for ubuntu 43.0.4

setup
work1

babel转换后的

➜  test  cat forof-es5.js 
"use strict";
require("babel-polyfill");


var _marked = [produceValues].map(regeneratorRuntime.mark);

function setup() {
  console.log("setup");
}

function cleanup() {
  console.log("cleanup");
}

function work(value) {
  console.log(value);
}

function produceValues() {
  return regeneratorRuntime.wrap(function produceValues$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case 0:
          setup();
          _context.prev = 1;
          _context.next = 4;
          return "work1";

        case 4:
          _context.next = 6;
          return "work2";

        case 6:
          _context.prev = 6;

          cleanup();
          return _context.finish(6);

        case 9:
        case "end":
          return _context.stop();
      }
    }
  }, _marked[0], this, [[1,, 6, 9]]);
}

var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;

try {
  for (var _iterator = produceValues()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
    var value = _step.value;

    work(value);
    break;
  }
} catch (err) {
  _didIteratorError = true;
  _iteratorError = err;
} finally {
  try {
    if (!_iteratorNormalCompletion && _iterator.return) {
      _iterator.return();
    }
  } finally {
    if (_didIteratorError) {
      throw _iteratorError;
    }
  }
}
➜  test  node forof-es5.js          
setup
work1
cleanup

@reverland

Note that the .return() is not called automatically by the language in all contexts, only in cases where the language uses the iteration protocol. So it is possible for a generator to be garbage collected without ever running its finally block.

这个不就是在用这个 iteration protocol么, 为啥没有调用 return? 发现node v4的generator没有return… 怎么回事

@magicdawn 不知道怎么回事。。。

哈哈,不明觉厉…

回到顶部