💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
![](https://img.kancloud.cn/39/6e/396eb6fba29c47803f3524c342d288b3_469x314.png) ![](https://img.kancloud.cn/eb/4e/eb4e42959777480fdab8a4a3a591293f_683x129.png) ![](https://img.kancloud.cn/08/c2/08c2db58fa6d69facbfb50727b0901d8_616x417.png) ![](https://img.kancloud.cn/f4/83/f4831954354850936d1619e16f73fb5d_721x269.png) ![](https://img.kancloud.cn/3d/c3/3dc39a93cc5bb4df6ac7eb62721e935a_844x292.png) ![](https://img.kancloud.cn/26/25/2625d02a2ae6f426b45e4d7de6aef645_763x226.png) 作用域和闭包是前端面试中,最可能考查的知识点。例如下面的题目: > 题目:现在有个 HTML 片段,要求编写代码,点击编号为几的链接就`alert`弹出其编号 ~~~ <ul> <li>编号1,点击我请弹出1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> ~~~ 一般不知道这个题目用闭包的话,会写出下面的代码: ~~~ var list = document.getElementsByTagName('li'); for (var i = 0; i < list.length; i++) { list[i].addEventListener('click', function(){ alert(i + 1) }, true) } ~~~ 实际上执行才会发现始终弹出的是`6`,这时候就应该通过闭包来解决: ~~~ var list = document.getElementsByTagName('li'); for (var i = 0; i < list.length; i++) { list[i].addEventListener('click', function(i){ return function(){ alert(i + 1) } }(i), true) } ~~~ 要理解闭包,就需要我们从「执行上下文」开始讲起。 ### 执行上下文 先讲一个关于**变量提升**的知识点,面试中可能会遇见下面的问题,很多候选人都回答错误: > 题目:说出下面执行的结果(这里笔者直接注释输出了) ~~~ console.log(a) // undefined var a = 100 fn('zhangsan') // 'zhangsan' 20 function fn(name) { age = 20 console.log(name, age) var age } console.log(b); // 这里报错 // Uncaught ReferenceError: b is not defined b = 100; ~~~ 在一段 JS 脚本(即一个`<script>`标签中)执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个**全局执行上下文**环境,先把代码中即将执行的(内部函数的不算,因为你不知道函数何时执行)变量、函数声明都拿出来。变量先暂时赋值为`undefined`,函数则先声明好可使用。这一步做完了,然后再开始正式执行程序。再次强调,这是在代码执行之前才开始的工作。 我们来看下上面的面试小题目,为什么`a`是`undefined`,而`b`却报错了,实际 JS 在代码执行之前,要「全文解析」,发现`var a`,知道有个`a`的变量,存入了执行上下文,而`b`没有找到`var`关键字,这时候没有在执行上下文提前「占位」,所以代码执行的时候,提前报到的`a`是有记录的,只不过值暂时还没有赋值,即为`undefined`,而`b`在执行上下文没有找到,自然会报错(没有找到`b`的引用)。 另外,一个函数在执行之前,也会创建一个**函数执行上下文**环境,跟**全局上下文**差不多,不过**函数执行上下文**中会多出`this``arguments`和函数的参数。参数和`arguments`好理解,这里的`this`咱们需要专门讲解。 总结一下: * 范围:一段`<script>`、js 文件或者一个函数 * 全局上下文:变量定义,函数声明 * 函数上下文:变量定义,函数声明,`this`,`arguments` ### `this` 先搞明白一个很重要的概念 ——**`this`的值是在执行的时候才能确认,定义的时候不能确认!**为什么呢 —— 因为`this`是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。看如下例子 ~~~ var a = { name: 'A', fn: function () { console.log(this.name) } } a.fn() // this === a a.fn.call({name: 'B'}) // this === {name: 'B'} var fn1 = a.fn fn1() // this === window ~~~ `this`执行会有不同,主要集中在这几个场景中 * 作为构造函数执行,构造函数中 * 作为对象属性执行,上述代码中`a.fn()` * 作为普通函数执行,上述代码中`fn1()` * 用于`call``apply``bind`,上述代码中`a.fn.call({name: 'B'})` 下面再来讲解下什么是作用域和作用域链,作用域链和作用域也是常考的题目。 > 题目:如何理解 JS 的作用域和作用域链 ### 作用域 ES6 之前 JS 没有块级作用域。例如 ~~~ if (true) { var name = 'zhangsan' } console.log(name) ~~~ 从上面的例子可以体会到作用域的概念,作用域就是一个独立的地盘,让变量不会外泄、暴露出去。上面的`name`就被暴露出去了,因此,**JS 没有块级作用域,只有全局作用域和函数作用域**。 ~~~ var a = 100 function fn() { var a = 200 console.log('fn', a) } console.log('global', a) fn() ~~~ ![](https://img.kancloud.cn/34/a4/34a4ef0ad0a122d9b6d642f6ce12e7b6_251x147.png) 全局作用域就是最外层的作用域,如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样的坏处就是很容易撞车、冲突。 ~~~ // 张三写的代码中 var data = {a: 100} // 李四写的代码中 var data = {x: true} ~~~ 这就是为何 jQuery、Zepto 等库的源码,所有的代码都会放在`(function(){....})()`中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。 附:ES6 中开始加入了块级作用域,使用`let`定义变量即可,如下: ~~~ if (true) { let name = 'zhangsan' } console.log(name) // 报错,因为let定义的name是在if这个块级作用域 ~~~ ### 作用域链 首先认识一下什么叫做**自由变量**。如下代码中,`console.log(a)`要得到`a`变量,但是在当前的作用域中没有定义`a`(可对比一下`b`)。当前作用域没有定义的变量,这成为**自由变量**。自由变量如何得到 —— 向父级作用域寻找。 ~~~ var a = 100 function fn() { var b = 200 console.log(a) console.log(b) } fn() ~~~ 如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是**作用域链**。 ~~~ var a = 100 function F1() { var b = 200 function F2() { var c = 300 console.log(a) // 自由变量,顺作用域链向父作用域找 console.log(b) // 自由变量,顺作用域链向父作用域找 console.log(c) // 本作用域的变量 } F2() } F1() ~~~ ### 闭包 讲完这些内容,我们再来看一个例子,通过例子来理解闭包。 ~~~ function F1() { var a = 100 return function () { console.log(a) } } var f1 = F1() var a = 200 f1() ~~~ ![](https://img.kancloud.cn/6a/bb/6abbde1359c0f513765e563dda6dd7df_261x150.png) 自由变量将从作用域链中去寻找,但是**依据的是函数定义时的作用域链,而不是函数执行时**,以上这个例子就是闭包。闭包主要有两个应用场景: * **函数作为返回值**,上面的例子就是 * **函数作为参数传递**,看以下例子 ~~~ function F1() { var a = 100 return function () { console.log(a) } } function F2(f1) { var a = 200 console.log(f1()) } var f1 = F1() F2(f1) ~~~ ![](https://img.kancloud.cn/4b/ea/4bea5fbd9089c2c6183829ad72b70229_289x195.png) 至此,对应着「作用域和闭包」这部分一开始的点击弹出`alert`的代码再看闭包,就很好理解了。