第一章:作用域是什么
1.1 编译原理
- 编译过程
- 分词 / 词法分析:将一个语句分解为有意义的代码块,即:词法单元
- 比如:
let a = 2;被分解为:let、a、=、2、;5 个词法单元 - 分词:无状态解析规则,词法分析:有状态解析规则
- 比如:
- 解析 / 语法分析:词法单元 -> 抽象语法树(Abstract Syntax Tree,AST)
- 代码生成:AST -> 可执行代码
- 分词 / 词法分析:将一个语句分解为有意义的代码块,即:词法单元
- JS 代码在执行前都需要编译
1.2 理解作用域
- 编译过程重要参与对象
- 引擎:负责 JS 编译和执行过程
- 编译器:负责编译过程
- 作用域:收集并维护所有声明的标识符(变量),通过规则限制代码对标识符的访问权限
- 变量赋值的两个操作
- 在当前作用域中声明一个变量(如果之前存在则忽略)
- 运行时引擎在作用域中查找该变量,找到了就赋值
- 编译器基础术语
- 赋值操作使用 LHS,获取目标变量的值使用 RHS
- LHS:从左侧查找,即:查找某个容器本身
- 比如:
a = 2,需要找到为 = 2 赋值的目标
- 比如:
- RHS:从右侧查找,即:查找某个变量的值(retrieve his source value)
- 比如:
console.log(a);,这里 a 没有被赋值,但需要找到 a 对应的 value
- 比如:
- LHS:从左侧查找,即:查找某个容器本身
- 赋值操作使用 LHS,获取目标变量的值使用 RHS
1.3 作用域嵌套
- 在当前作用域找不到变量,就向上层作用域查找,直到找到该变量,或者到最外层作用域(全局作用域)为止
1.4 异常
- 如果在 RHS 在任何作用域都找不到,会抛出
ReferenceError异常 - 非严格模式下,如果在全局作用域也找不到,会在全局作用域创建一个该名称的变量
- 严格模式禁止自动隐式或自动创建全局变量
- 如果 RHS 找到一个变量,但操作不合理,比如引用 null ,会抛出
TypeError异常 ReferenceError说明作用域判断异常,TypeError说明作用域判断成功但对值的操作失败了
第二章:词法作用域
2.1 词法阶段
- 词法作用域由变量和块作用于写在哪里决定
- 作用域查找会在匹配第一个标识符时停止
2.2 欺骗词法
- 欺骗词法作用域会导致性能下降
eval([str])函数:- 通常被用来执行动态创建的代码
- 字符串作为参数,内容视未好行书写时就存在于程序中的位置
- 可以修改词法作用域
- with 关键字
-
重复引用同一个对象中多个属性的快捷方式
-
将一个对象的引用当作作用域来处理, 将对象的属性当作作用域中的标识符来处理,创建了一个新的词法作用域
-
非严格模式,with 会造成变量泄漏到全局作用域,因为非严格模式会隐式创建全局变量
var obj = { a: 1, b: 2 } function foo(obj) { with (obj) { a = 2; } } console.log(foo(obj.a)); // undefined console.log(foo(a)); // 2- with 根据传递的对象凭空创建了一个全新的词法作用域
-
2.3 性能
- JS 引擎在编译阶段对静态代码优化时,并不能确认 eval 和 with 内部的代码,最糟糕的情况是优化的代码可能完全是无效的
第三章:函数作用域和块作用域
3.1 函数中的作用域
- 函数的全部变量在函数内部都可以被访问,从外部则无法访问
3.2 隐藏内部实现
- 最小特权原则:软件设计中,应该最小限度的暴露必要的内容,比如内部函数或者内部类
- 隐藏作用域的变量和函数可以避免同名标识符的冲突
- 避免全局变量的使用
- 全局命名空间:通过对象实现,将对外暴露的功能都作为这个对象的属性
- 模块管理:通过管理器将库的标识符显式导入另一个特定的作用域中
3.3 函数作用域
- 函数表达式
- 声明函数的 function 不在第一个位置
var a = 2; (function foo() { var a = 3; console.log( a ); })(); console.log( a ) // 2 - 函数表达式可以将函数隐藏在自己的作用域,外部无法访问,不会污染外部作用域
- 声明函数的 function 不在第一个位置
- 匿名函数:省略函数名称
setTimeout( function() { console.log( 1 ); }, 1000); - 具名函数:声明函数名称
- 立即执行函数表达式(IIFE,Immediately Invoked Function Expression)
- 函数表达式末尾加上一个
()让函数立即执行 - 可以当作函数调用并传递参数
var a = 2; (function foo( global) { var a = 3; console.log( a ); // 3 console.log( global.a ); // 2 })(window);
- 函数表达式末尾加上一个
3.4 块作用域
- 使用 var 申明变量会属于外部作用域
- try ... catch 的 catch 部分会创建块作用域
- let 关键字
- 将变量绑定到所在的作用域中,外部变量无法访问
- let 声明的代码在 let 之前无法被访问
{ console.log( bar ); // ReferenceError let bar = 2; }
- const 关键字
- 可以创建块作用域变量,但值是固定的
第四章:提升
var a = 2;,var 2属于编译阶段任务,a = 2属于解释阶段任务- 提升过程:现有声明,再有赋值。所有声明都被提升到各自作用域顶端
- var 的变量声明会被提升
- 函数的声明会被提升,函数表达式的声明不会被提升
- 因为函数表达式的变量赋值会被提升,但还是 undefined,变量的函数操作就属于 TypeError
第五章:作用域闭包
- 当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // 2- bar() 的词法作用域能够访问 foo() 内部作用域,将 bar() 函数本身作为一个值类型进行传递
- 在自己定义的词法作用域以外的地方执行,依然持有对该作用的引用,这个引用就是闭包
- 函数可以记住并访问所在的词法作用域, 即使函数是在当前词法作用域之外执行,这时就产生了闭包
- 闭包使得函数可以继续访问定义时对词法作用域
- 闭包应用示例:setTimeout
function wait(message) { setTimeout( function timer() { console.log( message ); }, 1000); } wait("hello message");- timer 函数具有涵盖 wait 作用域的闭包,因此还有对变量 message 的引用
- 循环和闭包
-
循环输出问题
for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000); }-
以上代码会连续输出 5 个 6
-
延迟函数的回调会在循环结束时执行,即使延迟时间为 0
-
问题原因:i 是共享的全局作用域,所以即使循环了 5 次,也相当于只有一个变量
-
解决方案
- 使用 IIFE 立即执行函数, 并且需要一个变量来存储每个迭代中的 i (即传入的参数)
for (var i = 1; i <= 5; i++) { (function (j) { setTimeout(function timer() { console.log(j); }, i * 1000); })(i); } - 使用 let 关键字
- 将一个块转换成一个可以被关闭的作用域
for (let i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, i * 1000); }
- 使用 IIFE 立即执行函数, 并且需要一个变量来存储每个迭代中的 i (即传入的参数)
-
-
- 模块
- 返回一个含有属性引用的对象将函数传递到词发作用域外部
- 类似 React 和 Vue 的 hook 模式
function useState() { var someThing = "wujie"; function getSomeThing() { return someThing; } return { getSomeThing: getSomeThing, }; } var state = useState(); console.log(state.getSomeThing()); // wujie - 模块模式的两个必备条件
- 必须有外部封闭函数,且该函数至少被调用一次(注:每次调用都会创建新的模块)
- 封闭函数必须返回至少一个内部函数(闭包)