felix-cao / Blog

A little progress a day makes you a big success!
31 stars 4 forks source link

JavaScript 闭包(closure) #96

Open felix-cao opened 5 years ago

felix-cao commented 5 years ago

为了理解闭包的概念,请先阅读以下几篇文章并理解其概念:

虽然 JavaScript 是一门完整的面向对象的编程语言,但这门语言同时也拥有许多函数式语言的特性。 函数式语言的鼻祖是 LISPJavaScript 在设计之初参考了 LISP 两大方言之一的 Scheme,引入了Lambda 表达式、闭包、高阶函数等特性。使用这些特性,我们经常可以用一些灵活而巧妙的方式来编写 JavaScript 代码。本篇主要聊一聊闭包。

一、闭包的概念

一般情况下,在函数调用结束后,函数内定义的变量会被自动销毁,但是闭包例外

闭包的概念,在国内文献资料中很少有阐述清楚明确的,

在《你不知道的JavaScript 上卷》第一版中用了极其重要(incredibly important)又难以理解(but persistently elusive)近似神话(almost mythological), 却又无处不在(closure is all around you in JavaScript)。闭包是基于词法作用域书写代码时所产生的自然结果。但第一版没有闭包的概念的定义。

《你不知道的JavaScript 上卷》第二版中有这样的定义:

Closure is observed when a function uses variable(s) from outer scope(s) even while running in a scope where those variable(s) wouldn't be accessible.

这个定义有三个重要的部分:

而在维基百科中的关于闭包的概念,我觉得描述的比较到位:

闭包(closure),是词法闭包(Lexical Closure)或函数闭包(function closures)的简称,是引用了自由变量的表达式(函数)。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

自由变量的概念请移步《JavaScript 自由变量(free variable)》

JavaScript 拥有自动的垃圾回收机制,垃圾回收机制,有一个重要的行为,那就是,当一个值,在内存中失去引用时,垃圾回收机制会根据特殊的算法找到它,并将其回收,释放内存。详情请移步《JavaScript 的垃圾回收机制

而我们知道,函数的执行上下文,在执行完毕之后,生命周期结束,那么该函数的执行上下文就会失去引用。其占用的内存空间很快就会被垃圾回收器释放。可是闭包的存在,会阻止这一过程。

本质上,闭包是将函数内部和函数外部连接起来的桥梁。 闭包的核心思想是函数调用完成之后,其执行上下文环境不会被销毁。**

二、代码解析

2.1 函数作为返回值的闭包


function foo() {
var max = 10;
return function bar(x) {
if(x> max) {
console.log(x);
}
}  
}

var f1 = foo(); f1(15);

`bar` 函数作为返回值,赋值给 `f1` 变量, 这时候 `bar` 函数作用域和 `foo` 函数作用域都被保留了下来了,(`foo` 函数中通过 `return` 语句实现“越狱”)  执行 `f1(15)` 时,用到了 `foo` 作用域下的 `max` 变量的值。

#### 2.2 函数作为参数被传递的闭包
```js
var max = 10;
var foo = function(x) {
    if(x > max) {
           console.log(x);
        }
}

(function(f) {
    var max = 100;
    f(15)
})(fn);

fn函数作为一个参数被传递进入另一个函数,赋值给f参数。执行f(15)时,max变量的取值是10,而不是100。

三、图示

图1

图2:第一步,代码执行前生成全局上下文环境,并在执行时对其中的变量进行赋值。此时全局上下文环境是活动状态。

图3:第二步,执行第17行代码时,调用 fn(),产生 fn() 执行上下文环境,压栈,并设置为活动状态。

图4:第三步,执行完第17行,fn() 调用完成。按理说应该销毁掉 fn() 的执行上下文环境,但是这里不能这么做。注意,重点来了:因为执行 fn() 时,返回的是一个函数。函数的特别之处在于可以创建一个独立的作用域。而正巧合的是,返回的这个函数体中,还有一个自由变量 max 要引用 fn 作用域下的fn() 上下文环境中的 max。因此,这个 max不能被销毁,销毁了之后 bar 函数中的 max 就找不到值了。

因此,这里的 fn() 上下文环境不能被销毁,还依然存在与执行上下文栈中。

——即,执行到第18行时,全局上下文环境将变为活动状态,但是 fn() 上下文环境依然会在执行上下文栈中。另外,执行完第18行,全局上下文环境中的 max 被赋值为100。如下图:

图5:第四步,执行到第20行,执行 f1(15),即执行 bar(15),创建 bar(15) 上下文环境,并将其设置为活动状态。

执行 bar(15) 时,max 是自由变量,需要向创建 bar 函数的作用域中查找,找到了 max 的值为10。这个过程在作用域链一节已经讲过。

这里的重点就在于,创建 bar 函数是在执行 fn() 时创建的。fn() 早就执行结束了,但是 fn() 执行上下文环境还存在与栈中,因此 bar(15) 时,max 可以查找到。如果 fn() 上下文环境销毁了,那么 max 就找不到了。

使用闭包会增加内容开销,现在很明显了吧!

第五步,执行完20行就是上下文环境的销毁过程,这里就不再赘述了。

Reference