JavaScript中的闭包是如何工作的

avatarplhDigital nomad

JavaScript闭包不是魔法

本篇文章解释闭包,让程序员可以弄懂它们的工作方式。但是本篇文章不适合科学家或者函数式程序员。因为闭包容易发生内存泄漏阿。

一旦核心概念被弄懂,闭包并不难理解。然而,闭包不可能通过阅读论文或者文献来弄懂。

这个文章适合有主流语言的程序员。请看下面的JavaScript代码:

function sayHello(name){
  var text = 'Hello ' + name;
  var say = function(){console.log(text);}
  say();
}
sayHello('Joe');  // Hello Joe

一个关于闭包的例子

两段关于闭包的定义

  • 一个闭包是一种方式去支持函数是一等公民这种概念,这是一个表达式,它可以在他作用域(当他是第一个被声明的时候),被赋值给一个变量,或者作为一个参数被传递给一个函数,或作为一个函数的结果被返回。

  • 或者,一个闭包是一个栈帧,在一个函数开始执行的时候被分配,并且不被释放,在函数被返回的时候(同时一个栈帧)好像在堆上而不是在栈上面被分配。

下面的代码返回了一个对函数的引用

function sayHello2(name){
  var text = 'hello '+name;   // Local variable
  var say = function () { console.log(text); }
  return say;
}
var say2 = sayHello2('Bob');
say2();   //   logs  "Hello Bob"

大多数JavaScript程序员会理解上面代码是如何返回一个函数给say2()的。如果你不懂,回去补习基础。

这里有一个致命的不同点,C的指针指向一个函数和JavaScript引用一个函数。在JavaScript中,你可以认为一个函数声明一个变量作为同时指向一个函数,同时一个隐藏的指针指向闭包。

上面代码有一个闭包,因为匿名函数function() { console.log(text); }是被声明在另一个函数内部,sayHello2()在这个例子中。在JavaScript中,如果你用函数关键字在另一个函数中,你就是在创造一个闭包。

在C和其他大多数语言中,在一个函数返回时候,所有的本地变量都不可再访问,因为栈帧被销毁了。

在JavaScript中,如果你声明一个函数在另一个函数中,本地变量如果被你返回的函数调用的话,他就会保持仍可访问状态。上面的证明了,因为我们调用say2()在沃恩返回了sayHello2()之后,注意这个代码,我们调用sayHello2()返回的函数,它还有引用text变量,这个text就是函数的本地变量

function() {console.log(text);} // Output of say2.toSting();

看着say2.toString()的输出,我们可以看到这个代码引用了变量text。这个匿名函数引用了text。

这个魔法是,JavaScript一个函数引用,同时有一个秘密的引用指向闭包--他自己创建的闭包,相似于对象指针。

更多的例子

更多的理由,当你看到闭包的时候,闭包看起来真的难以理解。但是当你看到一些闭包的例子(这花了我一段时间)。我推荐以下例子我建议你仔细研究以下例子,直到你理解他们是如何工作的。如果你开始使用闭包却没有充分的理解他们的工作原理的话,你可能会遇到一些难以理解的错误。

例子3

这个例子展示了,本地变量不是复制的---他们被保存了,因为被引用了。这是一种类似于保存栈帧在内存种,当外部函数仍然存在时候!

function say667() {
  // 最终在闭包内的局部变量
  var num = 42;
  var say = function () { console.log(num); }
}
var sayNumber = say667();
sayNumber();  // log 43

例子4

所有三个全局函数有一个共同的引用,指向同一个闭包,因为他们都是被声明给一个单一的调用来自setupSomeGlobals()

var gLogNumber;
var gIncreaseNumber;
var gSetNumber;

function setupSomeGlobals() {
	// 局部变量,终止于闭包内
	var num = 42;
	// 多函数做一些引用,存储于全局变量
	gLogNumber = function (){
		console.log(num);
	}
	gIncreaseNumber = function (){
		num++;
	}
	gSetNumber = function (x){
		num = x;
	}
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber();  // 43
gSetNumber(445);
gLogNumber();  // 445

var oldLog = gLogNumber;

setupSomeGlobals();
gLogNumber(); // 42

oldLog()

上面三个函数可以共同访问一个闭包 -- 这个本地变量setupSomeGlobals(),当你带哦有那个他的时候,三个函数被定义。

注意上面例子,如果你再次调用setupSomeGlobals(),那样的话一个新的闭包(栈帧)被创建。这个老的gLogNumbergIncreaseNumbergSetNumber变量将会被具有新的闭包的函数所覆盖。(在JavaScript中,每当你声明一个函数在另一个函数里面的时候,每当外部函数被调用,里面的函数都会被再创造一次)。

例子5

这个例子展示了闭包包含一些局部变量,他们被声明在外部函数,在他退出之前。注意,这个变量alice是真正的被声明在匿名函数之后。这个匿名函数被第一个声明;并且当这个函数被调用,它能接受alice变量,因为alice是在相同作用域内(JavaScript执行变量提升)。同时sayAlice()()只是直接调用函数sayAlice()返回的引用---他是基本上和上一个例子4相同的,但是没有临时变量。

提示:注意say变量同时也在闭包内,并且它可以通过另一个函数被接受,这个变量被声明在sayAlice(),内部,或者它可以作为参数被传递。

例子6

对于很多人来说,这是一个真的陷阱。要非常小心,当你在循环中定义函数。闭包中的局部变量可能不会像你想的那样发生作用。

你需要明白变量提升功能,在js中,为了明白下面例子。

function buildList(list) {
	var result = [];
	for (var i = 0; i < list.length; i++) {
		var item = 'item' + i;
		result.push(function (){
			console.log(item+ ' '+ list[i])
		})
	}
	return result;
}

function testList(){
	var fnlist = buildList([1,2,3]);
	// 只使用j
	for (var j = 0; j < fnlist.length; j++) {
		fnlist[j]();
	}
}

testList()
好吧,辣鸡英语,看不懂,go next

最终的观点

  • 每当你用function在另一个函数中,一个闭包就被创建了。

  • 每当你用eval()在一个函数中,一个闭包被使用了。本文你的eval可以被调用在本地变量来自函数,在eval中,你甚至可以创建新的局部变量,通过eval('var foo = ...')

  • 当你使用new Function(...)创建在一个函数中,这样不会创建闭包,(new Function 不能引用来自外部函数的局部变量)。

  • 一个闭包在JavaScript中,是保存一个局部变量的副本,就像函数退出时候的那样。

  • 最好认为闭包总是只创建一个函数的入口,并且将局部变量添加到闭包中。

  • 每次调用一个闭包的时候,都会创建一系列新的局部变量(假设函数内部包含一个函数声明,并且引用的内部函数同时返回或者要么被返回,要么以某种方式保留外部引用)。

  • 两个函数也许看起来有相同的关键字,但是有着完全不同的行为方式因为他们所隐藏的闭包。我不认为JavaScript代码可以真正的找出一个函数引用是否有闭包。

  • 如果你正在尝试做一些动态代码修改(例如:myFunction=Function(myFunction.toString().replace(/Hello/,'hello'));),这个不会成功运行,如果myFunction是一个闭包的话(当然,你甚至不会去想做一些在运行中将源码替换的动作。。)

  • 可以在函数内的函数声明中获取函数声明 - 并且可以在多个级别获得闭包。

  • 我认为通常闭包是函数和捕获的变量的术语。请注意,我在本文中没有使用该定义!

  • 我怀疑JavaScript中的闭包与函数式语言中的闭包有所不同。

最后

我想补充的一些例子 下面的代码中,func()函数每执行一次,都会产生不一样的变量nun,这个是原始引用。

const func = (num = 0) => () => num++;
var test1 = func();
var test2 = func();
console.log(
  test1(),   // 0
  test1(),   // 1
  test1(),   // 2
  test2(),   // 0
  test2(),   // 1
  test2(),   // 2
)

但是再次查看下面例子又不一样了,this.num作为函数内部属性,无论func()函数执行多少次,都依然是之前的变量,这个说明,函数还是之前的函数,但其中声明的局部变量却不是之前的局部变量了。

const func = (function() {
  this.num = 0;
  return ()=>{
    return num++
  }
})
var test1 = func();
var test2 = func();
console.log(
  test1(),   // 0
  test1(),   // 1
  test1(),   // 2
  test2(),   // 3
  test2(),   // 4
  test2(),   // 5
)

函数内部是否将一个函数作为返回变量十分重要。

const func = function() {
  this.num = 0;
  function a(){
    console.log(this.num)
  }
  const b = ()=>{
    console.log(this.num++)
  }
  return num++
  // return ()=>{
  //   return num++
  // }
}
var test1 = func();
var test2 = func();
console.log(
  new func(),   // 0
  func(),    // 0
  test1,     // 0
)

当你返回的是一个函数的时候,就不能做构造函数了。

const func = function() {
  this.num = 0;
  function a(){
    console.log(this.num)
  }
  const b = ()=>{
    console.log(this.num++)
  }
  // return num++
  return ()=>{
    return num++
  }
}
var test1 = func();
var test2 = func();
console.log(
  new func(),   // [Function]
  func(),   // [Function]
  test1,    // [Function]
)

函数每次执行的时候都会返回一个指针指向内存中新创建的一个函数

const func = function() {
  this.num = 0;
  function a(){
    console.log(this.num)
  }
  const b = ()=>{
    console.log(this.num++)
  }
  // return num++
  return ()=>{
    return num++
  }
}
var test1 = func();
var test2 = func();
console.log(
  test1 == test2,  // false
  test1 == func(), // false
  test1 == test1,  // true
)

继续前面翻译的例子,没有解释清楚的继续再解释一次,清晰解释,你也许会认为sayAlery和say2()指向同一个函数,但是,在外部打印sayAlert的时候,他是未定义的,这和我了解的变量提升不一样的啊。首先sayHello2函数返回的变量每次都不是同一个函数。从这不知道你能不能看出,为什么闭包稍微不注意就会发生内存泄漏。。sayHello仍然是同一个函数

function sayHello2(name) {
    var text = 'Hello ' + name; // Local variable
    function sayAlert() { 
        alert(text); 
    }
    return sayAlert;
}
say1 = sayHello2('Bob');
say2 = sayHello2('Bob');
say3 = sayHello2;
say4 = sayHello2;
say2(); // alerts "Hello Bob"
console.log(
  say1 == say2, // false
  sayHello2 == sayHello2,   // true
  sayHello2() == sayHello2(),   // false
  say3() == say4(),   // false
  say3 == say4,   // false
  sayAlert,  // sayAlert is not defined
)

当然,仍然可以使用这句经典的话

在JavaScript中,你可以认为一个函数的指针变量同时拥有两个指针。一个指向这个函数,另一个隐藏的指针指向一个闭包。

Reference

stackoverflow高分回答