从运行时角度理解JS闭包

从运行时角度理解JS闭包

难度:容易

前言

闭包是JS中非常重要的内容,理解闭包有利于我们深入掌握JS语言以及学习其它前端框架源代码,同时闭包也是面试中必考的知识点。我在学习闭包时,花了不少功夫,看了许多大佬的博客,一开始死记硬背,看的时候以为掌握了,然后过几天又忘了,反反复复,耗时费力。本文尝试从js代码运行时的角度,来解释闭包的基本原理,希望看完之后可以将闭包知识刻进记忆里。

定义

我们先看下闭包的定义:

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。—— 摘自MDN

定义比较抽象,看完疑窦丛生,闭包是组合!?那什么是词法环境?什么又是引用捆绑?什么又是函数的作用域?

我们再来看看大佬们怎么说:

闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。——摘自某位财经博主

看完大佬说法更晕了,闭包是函数!?函数内部的函数!?

算了,姑且不论谁对谁错,我们先从一个例子出发,看看JS引擎咋说。

举例

我们先看第一个例子:

function makeFunc() {
    var stevenx911_name = "StevenX911";
    function displayName() {
        console.log(stevenx911_name);
    }
    return displayName;
}

var myFunc = makeFunc();
myFunc();

我们将断点放在console.log(stevenx911_name);这一行,观察Chrome DevTools调试面板,结果如下:

image-20201022175713205

从调试面板中我们可以清晰的看到,在Scope下显示着Closure (makeFunc),这就是我们的闭包(Closure)所在!同时在这个闭包中,我们也可以看到在执行内部函数displayName时访问到了定义在外层函数makeFunc中定义的变量stevenx911_name,换句话说,闭包Closure(makeFunc)在内部函数执行时的作用域(Scope)链上,所以,对比上述的两种定义,不难看出,MDN的定义更为严谨、更为靠谱!(此处没有diss大佬之意~)

我们再来看一个例子:

// 这是一道经典的面试题,结果输出什么?怎么改进?
function test() {
    for (var i = 0; i < 10; i++) {
        setTimeout(function () {
            console.log(i);
        })
    }
}
test();

我们将断点放置在console.log(i)这一行,观察调试面板,结果如下:

image-20201022182707235

从调试面板上我们可以看到结果都是10,这里的闭包是由setTimeout回调函数和外层函数组合形成的,当主线程循环结束时,闭包中的变量i=10,所以定时器触发的回调函数只能拿到10,因此10次EventLoop的结果是一样的。至于怎么改进就留给各位看官了。

接着,我们再来看一个例子:

// 这也是经常会考到面试题,高阶函数
function Add(x) {
    return function (y) {
        return function (z) {
            console.log(x + y + z);
        }
    }
}

Add(1)(2)(3);

我们将断点放在console.log(x + y + z);这一行,然后运行它,打开调试面板,我们看到结果如下:

image-20201022184317355

从上图可以看到最内层的函数一直可以通过Scope(Closure)链访问到最外层的参数变量,并最终输出结果。除此之外,我们还发现:

  1. 闭包是可以嵌套的
  2. 形成闭包的函数不一定需要具名,匿名也可以

通过上述三个小例子,相信诸位对闭包一定有了一个感性的认识,但是我们还是很难用自己的话对闭包下一个准确的定义,这里还是搬出MDN的定义,再看上一遍:

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。—— 摘自MDN

闭包的应用

几乎所有的框架都用了闭包,所以只有深刻理解并掌握闭包,才能看懂框架、用好框架:

  1. 包装私有变量/方法
  2. 回调函数(含异步)
  3. 函数Currying柯里化
  4. ...

常见闭包面试题

  • 什么是闭包?
  • 闭包的应用有哪些?
  • 闭包的变量存在栈上?还是堆上?
  • JS为什么要设计闭包?
  • 阅读代码给出输出结果,尝试给出两种以上的改进方案
function test() {
	var arr = [];
	for(var var_i = 0; var_i < 10; var_i++){
		arr[var_i] = function () {
			console.log(var_i + " ");
		}
    }
    // 改进方案1 var -> let 利用ES6新增的块级作用域
    // for(let let_i = 0; let_i < 10; let_i++){
	  // 	arr[let_i] = function () {
	  // 		console.log(let_i + " ");
	  // 	}
    // }

    // 改进方案2 利用立即执行函数形成新的闭包
    // for(var var_i = 0; var_i < 10; var_i++){
    //     (function(f_var_i){
    //         arr[f_var_i] = function () {
    //             console.log(f_var_i + " ");
    //         }
    //     })(var_i)
    // }
    
	return arr;
}
 
var myArr = test();
for(var j = 0; j < 10; j++) {
	myArr[j]();
}

参考

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures

http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html

https://developers.google.com/web/tools/chrome-devtools/memory-problems?hl=zh-cn

风清洋

风清洋

保持原动力,迎接每一天

评论