DS.Lab筆記 - ECMA-262-3: 闭包

85 篇文章 0 订阅
58 篇文章 0 订阅

原文链接:ECMA-262-3 in detail. Chapter 6. Closures.



=============================================================


ECMA的闭包实现


var x = 10;
 
function foo() {
  console.log(x);
}
 
(function (funArg) {
 
  var x = 20;
 
  // variable "x" for funArg is saved statically
  // from the (lexical) context, in which it was created
  // therefore:
 
  funArg(); // 10, but not 20
 
})(foo);


ECMA用的是静态作用域,不是动态作用域。

不论函数有没有被真正执行过,它的作用域链都会在创建时被保存。

有些引擎可能会优化没有被访问到的自由变量(free variable)。

有些引擎实现了__parent__,允许直接访问作用域,但是Chrome和Firefox都没有。


=============================================================


一[[Scope]]永逸

在同一个上下文里创建的不同闭包(就是内函数对象),它们保存的变量是指向同一个对象的,通过某个内函数闭包对其进行的修改会影响到所有其余的闭包。


var firstClosure;
var secondClosure;
 
function foo() {
 
  var x = 1;
 
  firstClosure = function () { return ++x; };
  secondClosure = function () { return --x; };
 
  x = 2; // affection on AO["x"], which is in [[Scope]] of both closures
 
  console.log(firstClosure()); // 3, via firstClosure.[[Scope]]
}
 
foo();
 
console.log(firstClosure()); // 4
console.log(secondClosure()); // 3


演示这个问题的一个经典面试题是:

var data = [];
 
for (var k = 0; k < 3; k++) {
  data[k] = function () {
    console.log(k);
  };
}
 
data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2


这是由于闭包共享和变量提升导致的一个常见陷阱!


上述代码创建的闭包的情况是:

activeContext.Scope = [
  ... // higher variable objects
  {data: [...], k: 3} // activation object
];
 
data[0].[[Scope]] === Scope;
data[1].[[Scope]] === Scope;
data[2].[[Scope]] === Scope;

解决办法是通过函数表达式给每次循环里创建一个新作用域,并在新作用域里用一个新变量保存每次循环里k的值,这些新作用域被独立保存在每个数组元素的函数的闭包里,互不影响。


如下:

var data = [];
 
for (var k = 0; k < 3; k++) {
  data[k] = (function _helper(x) {
    return function () {
      console.log(x);
    };
  })(k); // pass "k" value
}
 
// now it is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2


它创建的闭包的情况是:

data[0].[[Scope]] === [
  ... // higher variable objects
  AO of the parent context: {data: [...], k: 3},
  AO of the _helper context: {x: 0}
];
 
data[1].[[Scope]] === [
  ... // higher variable objects
  AO of the parent context: {data: [...], k: 3},
  AO of the _helper context: {x: 1}
];
 
data[2].[[Scope]] === [
  ... // higher variable objects
  AO of the parent context: {data: [...], k: 3},
  AO of the _helper context: {x: 2}
];


作者也介绍了另外一个方案:

var data = [];
 
for (var k = 0; k < 3; k++) {
  (data[k] = function () {
    console.log(arguments.callee.x);
  }).x = k; // save "k" as a property of the function
}
 
// also everything is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2

疑问:我印象中有一本书建议不要使用arguments.callee,所以这个方案或许不值得推荐。


另外,ES6里面的let关键字解决了这个问题,在《YDtKJS:Scope & Closure》里也交代了。


=============================================================


函数参数和return

其实我没有太看懂这一节。


作者讨论的是从语义上,return的行为。似乎作者的意思是在ECMA里,return的职责很简单,就仅仅是返回控制流程到呼叫者。

function getElement() {
 
  [1, 2, 3].forEach(function (element) {
 
    if (element % 2 == 0) {
      // return to "forEach" function,
      // but not return from the getElement
      console.log('found: ' + element); // found: 2
      return element;
    }
 
  });
 
  return null;
}
 
console.log(getElement()); // null, but not 2

并且与用try-catch的情况做了比对:

var $break = {};
 
function getElement() {
 
  try {
 
    [1, 2, 3].forEach(function (element) {
 
      if (element % 2 == 0) {
        // "return" from the getElement
        console.log('found: ' + element); // found: 2
        $break.data = element;
        throw $break;
      }
 
    });
 
  } catch (e) {
    if (e == $break) {
      return $break.data;
    }
  }
 
  return null;
}
 
console.log(getElement()); // 2


=============================================================


理论版本(theory versions)


作者重新归纳了跟闭包的定义有关的内容。重新定性地描述何为闭包?


实践上来讲,被作为返回值被返回的函数是闭包,访问了自由变量(free variable)的函数是闭包。


=============================================================


闭包的实际用途


  • 应用于数组,用来遍历数组元素的函数,比如sort(),map(),forEach()。
  • apply与call,它们是受启发于函数编程里面的函数参数。
    (function () {
      console.log([].join.call(arguments, ';')); // 1;2;3
    }).apply(this, [1, 2, 3]);
  • 延时调用,比如setTimeout()。
  • 还有回调函数,比如Ajax里面的回调方法。


最常见的用法当然还是用来实现封装:

var foo = {};
 
// initialization
(function (object) {
 
  var x = 10;
   
  object.getX = function _getX() {
    return x;
  };
 
})(foo);
 
console.log(foo.getX()); // get closured "x" – 10





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值