掘金上关于作用域和作用域链的讨论非常多, 但少有人来讲清楚 JS 中相关的机制, 这里我就捡一些大佬们看剩的知识, 来讲讲理解作用域之前的准备. 带着这些问题看文章:
JavaScript 是如何编译执行的?
查找作用域时是如何一层层往上查询的?
作用域链的本质是?
想直接看解析的请跳到: 2. JavaScript 是如何执行的?
还有速记口诀: 作用域链口诀
1. 理解前的普及: 编译原理
1.1 分词 / 词法解析
这些代码块被称为词法单元(token) , 这些词法单元组成了词法单元流数组
- var sum = 30;
- // 词法分析后的结果
- [
- "var" : "keyword",
- "sum" : "identifier",
- "=" : "assignment",
- "30" : "integer",
- ";" : "eos" (end of statement)
- ]
1.2 语法分析
把词法单元流数组转换成一个由元素逐级嵌套所组成的代表程序语法结构的树, 这个树被称为 "抽象语法树" (Abstract Syntax Tree, 简称 AST).
1.3 代码生成
将抽象语法树 (AST) 转换为一组机器指令, 也就是可执行代码, 简单说, 就是用来创建一个变量 a, 并将 3 这个值储存在 a 中.
1.4 JavaScript 编译过程的不同处
JavaScript 大部分情况下编译发生在代码执行前的几微秒 (甚至更短!) 的时间内
JavaScript 引擎用尽了各种办法 (比如 JIT, 可以延 迟编译甚至实施重编译) 来保证性能最佳
2. JavaScript 是如何执行的?
核心重点: 变量和函数在内的所有声明都会在任何代码被执行前首先 被处理.
函数运行的瞬间, 创建一个 AO (Active Object 活动对象)运行载体.
2.1 例子一
- function a(age) {
- console.log(age);
- var age = 20
- console.log(age);
- function age() {
- }
- console.log(age);
- }
- a(18);
2.1.1 分析阶段
函数运行的瞬间, 创建一个 AO (Active Object 活动对象)
AO (Active Object 活动对象) 相当于载体
AO = {}
第一步, 分析函数参数:
形式参数: AO.age = undefined
实参: AO.age = 18
第二步, 分析变量声明:
- // 第 3 行代码有 var age
- // 但此前第一步中已有 AO.age = 18, 有同名属性, 不做任何事
即 AO.age = 18
第三步, 分析函数声明:
- // 第 5 行代码有函数 age
- // 则将 function age(){
- }付给 AO.age
- AO.age = function age() {
- }
函数声明特点: AO 上如果有与函数名同名的属性, 则会被此函数覆盖.
因为函数在 JS 领域, 也是变量的一种类型
分析阶段最终结果是:
AO.age = function age() {}
2.1.2 执行阶段
2.2 例子二
- function a(age) {
- var age = function () {
- console.log('25');
- }
- console.log(age);
- }
- a(18);
2.2.1 分析阶段
第一步, 分析函数参数:
形式参数: AO.age = undefined
实参: AO.age = 18
第二步, 分析变量声明:
- // 第 3 行代码有函数表达式 var age = function () {
- console.log('25');
- }
- // 但此前第一步中已有 AO.age = 18, 有同名属性, 不做任何事
即 AO.age = 18
第三步, 分析函数声明(无)
分析阶段最终结果是:
AO.age = 18
2.2.2 执行阶段
2.3 例子三
- function a(age) {
- console.log(age);
- var age = function () {
- console.log(age);
- }
- age();
- }
- a(18);
2.3.1 分析阶段
第一步, 分析函数参数: AO.age = 18
第二步, 分析变量声明: 有同名属性, 不做任何事 AO.age = 18
第三步, 分析函数声明(无)
分析阶段最终结果是:
AO.age = 18
2.3.2 执行阶段
到这里, 很多人会犯迷糊: age(); 不是应该输出 18 吗?
代码执行到 age(); 时, 其实又会再分析 & 执行.
2.3.3 age()的分析 & 执行
// 分析阶段
创建 AO 对象, AO = {}
第一步, 分析函数参数(无)
第二步, 分析变量声明(无)
第三步, 分析函数声明(无)
分析阶段最终结果是: AO = {}
当 age() 自己的 AO 对象, 即 age.AO 是个空对象时, 它会往上调用.
上一级的 AO 对象是 a, 即 a.AO, a.AO 下有个执行完后得到的
a.AO.age = function(){console.log(age);}
输出
ƒ () { console.log(age); }
`
2.4 执行总结: 何为作用域链
JavaScript 上每一个函数执行时, 会先在自己创建的 AO 上找对应属性值. 若找不到则往父函数的 AO 上找, 再找不到则再上一层的 AO, 直到找到大 boss:Windows(全局作用域). 而这一条形成的 "AO 链" 就是 JavaScript 中的作用域链.
3.LHS 和 RHS 查询: 作用域链的两大利器
LHS,RHS 这两个术语就是出现在引擎对变量进行查询的时候. 在《你不知道的 JavaScript(上)》也有很清楚的描述. 在这里, 我想引用 freecodecamp 上面的回答来解释:
LHS = 变量赋值或写入内存. 想象为将文本文件保存到硬盘中. RHS = 变量查找或从内存中读取. 想象为从硬盘打开文本文件. Learning JavaScript, LHS RHS
3.1 两者的特性
都会在所有作用域中查询
严格模式下, 找不到所需的变量时, 引擎都会抛出 ReferenceError 异常.
非严格模式下, LHR 稍微比较特殊: 会自动创建一个全局变量
查询成功时, 如果对变量的值进行不合理的操作, 比如: 对一个非函数类型的值进行函数调用, 引擎会抛出 TypeError 异常
3.2 拿书中的例子来讲
- function foo(a) {
- var b = a;
- return a + b;
- }
- var c = foo( 2 );
直接看执行查找:
- LHS(写入内存):
- c=, a=2(隐式变量分配), b=
RHS(读取内存):
读 foo(2), = a, a ,b
(return a + b 时需要查找 a 和 b)
按 写入 / 读取内存来理解, 是不是比书中的好理解多了?
3.3 关于 LHS 和 RHS 抛错
拿两个最简单的例子将:
3.3.1 不合理的操作
LHS 执行查询阶段, 原本查询成功, 但将 a 作用函数调用 a();, 故引擎会抛出 TypeError 异常.
3.3.2 LHS 抛错
LHS 比较少见的情况是: 很多时候我们都没开启严格模式, 即:"use strict". 你们可以现在打开 Chrome 调试工具, 分别试下以下代码严格 / 非严格模式的输出:
- "use strict"
- function init(a){
- b=a+3;
- }
- init(2);
- console.log(b);
3.3.3 RHS 抛错
4. 作用域链口诀
这里我们拿《你不知道的 JavaScript(上)》中的一张图解释:
我也总结了一个作用域链口诀, 教你快速找到输出:
分析阶段创 AO, 参数看完找变量, 变量不顶函数顶, 顶完之后定乾坤.
执行阶段看 LR, 内层不行找外层, 翻遍楼层找不到, 抛个异常连连看.
感悟:
这几天摸爬滚打的找了很多资料, 发现很多都讲得语焉不详. 要么非常复杂, 讲得贼深奥. 要么就是粗略概括, 没有系统介绍. 这也是为啥这么多将作用域与作用域链, 却没一个彻底看明白的原因(大概率也是因为菜)
文章链接
「从源码中学习」彻底理解 vue 选项 Props
「Vue 实践」项目升级 vue-cli3 的正确姿势
「从源码中学习」Vue 源码中的 JS 骚操作
求一份深圳的内推
目前本人在准备跳槽, 希望各位大佬和 HR 小姐姐可以内推一份靠谱的深圳前端岗位!
微信: huab119
邮箱: 454274033@qq.com
来源: https://juejin.im/post/5c8efeb1e51d45614372addd