本文会用导图梳理本书的脉络,由于是导读,正文部分只会列举重点内容,非重点内容会简单介绍,欢迎讨论与阅读原文。此外本文适合未读过此书的同学参考是否需要阅读,另外读过此书的同学,可以尝试回答文初的问题及顺着导图回忆本书内容,如果非常流畅那么相信您对书中的知识的理解是过关的。
上一篇我们讲了本书第一章中的作用域,作用域链,简单介绍了引擎,编译器,作用域是怎么合作进行编译的。本篇我们将会介绍本书第一部分的 2 ~ 6 章。
问题
- 说说你对闭包的理解。(面试)
- 说说你对词法作用域的理解,我们如何欺骗词法作用域?
- 函数作用域的作用?如何避免函数作用域污染全局变量?
- JavaScript 除了函数作用域还存在哪些块作用域?
- 变量提升的机制是什么?函数声明和变量什么那个优先被提升?
第一部分作用域与闭包
说说你对闭包的理解? (个人理解,求拍砖,不太了解闭包的同学可以暂时跳过)
闭包是一个绑定了执行环境的函数,闭包和普通函数的区别是携带了执行的环境。
闭包由环境和表达式两部分组成。
- 环境部分
- 环境:函数的词法作用域(执行上下文的一部分)。
- 标识符列表:函数中用到的未声明的变量。
- 表达式部分:函数体。
它被广泛应用,比如在回调函数中定时器,事件监听器,Ajax请求,跨窗口通信……又比如我们了解的循环,还有模块。闭包可以赋予了我们访问与操作上级作用域的能力为开发提供了极大的便利。
如果要说闭包的缺点可能就是维护困难,闭包可以缓存上级作用域的变量,如果闭包又是异步执行一定要搞清楚上级作用域都发生了什么,对代码的运行机制和逻辑要有所了解。
一、词法作用域?
-
作用域分成词法作用域和动态作用域。
词法作用域是一套关于引擎如何寻找变量以及在何处找到变量的规则,词法作用域最重要的特征就是它定义的过程发生在代码的书写阶段(假设没有使用
eval()
orwith
)。动态作用域让作用域在运行时动态确定。
词法阶段就是你说了一句话,然后编译器断句的过程。像不同的人理解一句话不同断句也就不同一样,不同的编译器分词也会有所不同。
-
词法作用域中有环境记录器和外部环境的引用,想要深入理解,我们需要了解执行上下文和执行栈(书上没有提到)
-
查找过程
function foo(a) { var b = "Java" function bar(c) { var b = "你好" console.log(a + b + c) } bar("Script") } foo("你不知道的") 复制代码
“遮蔽效应” 作用域查找会在找到第一个匹配的标识符时停止,我们在多层嵌套的作用域中可以定义同名的标识符,就可能因为遮蔽效应而找不到外部的标识符。
“全局对象” 全局变量会自动成为全局对象的属性,因此可以不直接通过全局对象的词法名称查找。
window.a 复制代码
通过这种方式我们可以访问被遮蔽的全局变量,但是非全局的变量如果被遮蔽了,无论如何也访问不了。
-
改变词法作用域的方法?
词法作用域完全由写代码期间函数声明的位置来定义,怎么可以在运行时修改呢?
改变词法作用域的方法有两种,
eval
与with
JavaScript 引擎会在编译阶段进行数项的性能优化,其中有些优化依赖于词法的静态分析,必须预先确定所有变量和函数的定义位置,一但在引擎中发现了上述的两种方法,那么这些优化都是无效的。
所以这两种方式不建议使用,想看的同学可以查资料或者翻阅本书。
二、 函数作用域与块作用域
-
函数作用域是什么?
每声明一个函数,就会为自身创建一个作用域,在这个作用域中,属于函数的全部变量都可以在整个函数的范围内(包括嵌套在内的作用域)使用及复用。
-
函数作用域的作用?
隐藏,在当前函数作用域内声明的变量或函数都会绑定在当前的函数作用域中,这样在全局作用域中就访问不到它们了,我们可以叫这个函数叫包装函数。
这符合软件设计中应该最小限度得暴露必要内容,将其他内容都“隐藏”起来的原则,比如模块或对象API设计。
规避冲突,避免同名标识符之间的冲突。
-
立即执行函数表达式的作用?
(function foo() { var a = "zhengyang"; console.log(a); })(); 复制代码
由于函数被包含在一对()中就成为了一个表达式,通过在末尾加一个()可以立即执行这个函数。它可以避免 foo 函数名称污染全局作用域,并且也不需要通过函数名 foo()来调用。
社区称它为LIFE,并且我们往往使用匿名函数,即省略掉 foo,因为函数表达是可以匿名的。
-
let的机制?
let关键字可以将变量绑定到所在的任意作用域中(通常是在{}中),可以说let为其声明的变量隐式得劫持了所在的块作用域。(表面上 JavaScript不具有块作用域的相关功能,但其实 with ,try/catch 是可以创建块作用域的)
三、 提升
-
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
-
函数声明优先于变量声明。
我们把
var a = 2
看做一个声明,事实上这个表达式可以拆成var a;
和a = 2
,第一部分是变量声明发生在编译阶段,第二部分的赋值会被留在原地等待执行阶段。console.log(zy) // ƒ zy() {} function zy() { } console.log(zy); // ƒ zy() {} var zy = 2 console.log(zy); // 2 复制代码
在上面的代码中在编译阶段会进行声明的提升,首先提升函数声明
function zy(){}
然后再提升var zy
,然后逐条执行,所以一个console.log(zy)
输出了函数 , 然后 第二个console.log(zy)
输出了函数,最后我们用留在原地的zy = 2
把 2 赋给了 zy 所以最后一个console.log(zy)
输出了 2。
四、 循环与闭包
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000);
}
复制代码
不难理解以上的代码输出是 5 个 5,因为 setTimeout
是异步的所以会在for循环结束后才会执行。但是我们想要得到的却是每循环一次输出一次,结果是 1 , 2 , 3 , 4 , 5。
使用闭包的方法解决问题。
for (var i = 1; i <= 5; i++) {
(function () {
var j = i;
setTimeout(() =>{
console.log(j)
}, j * 1000)
})();
}
复制代码
在上面的代码中我们的环境是全局作用域,函数体是一个立即执行函数,每次的循环 i 的值都因为闭包被保留了下来传递给 j 然后通过 setTimeout 输出一次值。
for (var i = 1; i <= 5; i++) {
(function (j) {
setTimeout(() =>{
console.log(j)
}, j * 1000)
})(i);
}
复制代码
我们也可以用 let
。
for (let i = 1; i <= 5; i++){
setTimeout(() =>{
console.log(i)
}, i*1000)
}
复制代码
后言
闭包这块我读到这里已然对它有了一个全新的认识,但是仍然存在不少问题,比如具体编译的过程;JavaScript引擎如何做优化;同步异步的理解还要有待挖掘,对于 let 的实现也不是很了解。