[译] 透过从新实作来学习参透闭包

原文出处: 连结

话说网路上有许多文章在探讨闭包(Closures)时大多都是简单的带过。大多的都将闭包的定义浓缩成一句简单的解释,那就是一个闭包是一个函数能够保存其竖立时的执行环境。不过究竟是怎么保存的?

别的为什么一个闭包能够一向运用区域变数,即使这些变数在该 scope 内已经不存在了?

为相识开闭包的神奇面纱,我们将要假装 Javascript 没有闭包这东西而且也不能够用嵌套 function 来从新实作闭包。这么做我们将会发现闭包真实的本质是什么以及在底层究竟是怎么运作的。

为了这个练习我们同时也须要假装 Javascript 自身具备了另一个不存在的功用。那就是一个原始的物件当它如果被当成 function 调用的时候是能够执行的。
你能够已经在其他语言中看过这个功用,在 Python 中你能够定义一个 __call__ 要领,在 PHP 则有一个特别的要领叫 __invoke
这些要领(Method)会在当物件被当作 function 调用时执行。如果我们假装 Javascript 也有这个功用,我们能够须要这么实作:

let o = {
  n: 42,
  __call__() {
    return this.n;
  }
};

// 当我们把物件当作 function 一样调用时
o(); // 42, 当然现在你会获得 `TypeError: o is not a function` 的错误

这边我们获得一个一般的物件,我们假装我们能够把它当做 function 来呼唤,然后当我们这个做的同时其实我们是执行一个特别的要领 __call__ 如果你真的要实作记得用 o.__call__()

译者注: 注重! 如果您想实作的时,呼唤 可调用物件 比方上面的 o() 都要换成 o.__call__()

现在让我们先来看看一个简单的闭包范例。

function f() {
  // 下面这个变数是 f() 的区域变数
  // 一般,当我们离开 f 的 scope 时,这个变数 n 就应该要被接纳了
  let n = 42;

  // 嵌套的 function 参考了 n
  function g() {
    return n;
  }

  return g;
}

// 让我们透过 f() 来竖立一个 g 函数
let g = f();

// 理论上这个变数 n 在 f() 执行完毕之后就应该要立即被接纳,对吧?
// 毕竟 f 已经执行完毕了,而且我们也离开了该 scope
// 那为什么 g 能够继续参考一个已经被释放的变数呢?
g(); // 42

外层的 function f 有一个区域变数,然后里面的 function g 参考 f 的区域变数。

接着我们把内层的 g 回传指派给 f scope 外的变数。但我们猎奇的是如果 f 执行完毕被释放了,那为什么 g 依然能够获得已被释放的 f 的区域变数呢?

这个的魔法就是 – 一个闭包不仅仅只是一个 function。它是一个物件,具有建构子和私有资料。然后我们能够它当作 function 来运用。
那如果 Javascript 没有闭包这种用法,我们必须本身实作它呢?这就是我们接下来要看到的。

// 译者注: 这边请先不要纠结 Babel 或其他 Complier 实际编译出来的 ES5 还是用 function
class G {
  constructor(n) {
    this._n = n
  }

  __call__() {
    return this._n;
  }
}

function f() {
  let n = 42;

  // 这就是一个闭包
  // 这个内层的 function 其实不只是一个 function
  // 它其实是一个能够被调用的物件,然后我们传入 n 到它的建构子
  let g = new G(n);


  return g;
}

// 透过呼唤 f() 获得一个能够被调用的物件 g
let g = f();

// 现在就算原来从 f 拿到的区域变数 n 被接纳了也没关系
// 可被调用的物件 g 实际上是参考本身私有的资料
g(); // 42

如果您曾看过 ECMAScript 规范,能够会对实际上是参考本身私有的资料这句话产生一些疑问,先别急着否认。这边不过是试着用别的一个较浅的角度解释。

这边我们把内部的 function g 用一个 G class 的实例物件(即 new 出来的物件) 庖代,然后我们透过把 f 的区域变数 n 传进 G 的建构子,借此将变数储存在新的实例物件私有的资估中。最终我们能够获得 f 的区域变数(n)。

OK! 列位观众这就是一个闭包的行为。闭包就是一个可调用的物件,能够把透过建构子把传入的参数保存在私有的空间中。

让我们再深切一点

聪明的读者已经发现还有一些行为还没解释清晰或许说我们的模拟实作是有破绽的。让我们来观察其他的闭包范例

function f() {
  let n = 42;

  // 内部函数获得变数 n
  function get() {
    return n;
  }

  // 别的一个内部函数也同时存取 n
  function next() {
    return n++;
  }

  return { get, next };
}

let o = f();
o.get(); // 42
o.next();
o.get(); // 43

在这个范例中,我们获得两个闭包同时参考变数 n 。个中一个函数的操纵变数会影响别的一个变数获得得值。
但如果 Javascript 没有闭包,单靠我们上面的实作和 JS 的行为将不会一样。

class Get {
  constructor(n) {
    this._n = n;
  }

  __call__() {
    return this._n;
  }
}

class Next {
  constructor(n) {
    this._n = n;
  }

  __call__() {
    this._n++;
  }
}

function f() {
  let n = 42;

  // 这边的闭包我们一样换成可调用的物件
  // 它们能够将参数传入建构子,进而将值保存起来
  let get = new Get(n);
  let next = new Next(n);

  return { get, next };
}

let o = f();
o.get(); // 42
o.next();
o.get(); // 42

跟上面一样,我们庖代了内部 function getnext 的部份改成运用物件。它们是透过将值保存在物件内部进而获得 f 的区域变数,每一个物件具有本身私有的资料。同时我们也注重到个中一个可调用物件 操纵 n 并不会影响别的一个。这是因为它们是传 n 的值 value而不是传址 reference/address。白话文就是复制了一分资料。并不是操纵变数自身。

为了要解释为什么 Javascript 的闭包会参考到雷同的 n 即记忆体位置是一样的。我们须要解释变数自身。在底层,Javascript 的区域变数跟我们从其他语言明白的观念并不雷同,它们是负责动态分派与计算参考(reference)的物件的属性,称为 LexicalEnvironment 物件。Javascript 的闭包其实会有一个参考指向到整个 执行环境, 上下文, Context 的 LexicalEnvironment 物件,而不是特定的变数。

如果您对于 scope 与 context 还不是很相识强烈建议您观赏这篇

让我们来修正我们的可调用物件让其能够获得一个 lexical environment 而不是 n

class Get {
  constructor(lexicalEnvironment) {
    this._lexicalEnvironment = lexicalEnvironment;
  }

  __call__() {
    return this._lexicalEnvironment.n;
  }
}

class Next {
  constructor(lexicalEnvironment) {
    this._lexicalEnvironment = lexicalEnvironment;
  }

  __call__() {
    this._lexicalEnvironment.n++;
  }
}

function f() {
  let lexicalEnvironment = {
    n: 42
  }

  // 现在这个可调用变数是透过一个参考 lexical environment 来改变 n
  // 所以现在变更的是统一个 n 了
  let get = new Get(lexicalEnvironment);
  let next = new Next(lexicalEnvironment);
  return { get, next }
}

// 现在我们实作的物件行为跟 javascript 一致了
// 还是请注重如果您要时作,记得 o.get() 要换成 o.get.__call__() 喔
let o = f();
o.get(); // 42
o.next();
o.get(); // 43

上面实作我们将区域变数 n 换成 lexicalEnvironment 物件,然后具有一个属性 n
这时 GetNext 的物件实例所存取的就是统一个参考(reference)即 lexical environment 物件。
所以现在修正的就是雷同的处所了。基本上这就是一个闭包的行为。

结论

闭包是一个物件而且当它们是函数时我们能够直接调用。而事实上任何一个 Javascript 中的函数都是一个可被调用的物件也称作 function object 或许 functor 当它们被执行或许说被实例化时会带有一个私有的 lexical environment 物件。而想要更相识关于这个物件的看官们能够参考Lexical environment的定义。
在 Javascript 不是 function 创造闭包,function 自身就是一个闭包。

老实说译者自身还是比较喜欢明白 context 与 variable object 的说明,接着用 一个闭包是一个函数能够保存其竖立时的执行环境 这句话来记忆。虽然翻译了这篇文章但让小弟对于闭包有更深切明白是这篇解读ECMAScript[1]——实行环境、作用域及闭包。不过原作者从这个角度来解释的确是能够大要的明白整个运作机制,愿望这篇文章能让你有所收获。

    原文作者:andyyu0920
    原文地址: https://segmentfault.com/a/1190000004486514
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞