前沿
大家对变量提升(hoisting)一定不陌生了,这篇文章希望可以建立一个系统的规则,以后碰到变量提升,可以重新看这篇文章,根据文中的规则找到答案。最终,我们可以记住这个规则,很自然的理解变量提升。
例子
相信大家对这个例子不陌生了:
a = 2;
var a;
console.log( a ); /// 2
为什么这段话a 在声明之前就可以赋值了呢?
再看下这个例子:
console.log( a ); //undefined
var a = 2;
为什么这个又是输出undefined呢?
别担心,看完整个文章你就会知道运用怎样的规则来推断结果了。
作用域浅析
在这里我们对作用域不做详细的讲解,作用域可以想象成可访问对象的集合。
打个通俗的比方,现在有个公寓管理公司,底下有多个公寓。高级管理员A, 他有所有公寓所有房间的钥匙,所以他能打开所有公寓所有房间,并能拿出所有房间的物品。中级管理员B,只有某一栋公寓所有房间的钥匙,他能拿出所有此公寓所有房间的物品,但是不能拿出其他公寓的物品。原因就是管理员B在拿物品时(程序执行代码时), 并没用其他公寓的信息(其他作用域内信息)。
为了简单,本篇文章只涉及同一个作用域下,不同作用域的交互之后几篇会讲到
大家可以想像成进入到全局作用域或者某个函数作用域时,引擎会产生一个json object 当作资料库, 之后代码执行的时候会从这个json 中找值。
pseudoContext = {
}
JavaScript解析器
一般来说,大家可能觉得JavaScript解析器会在 run-time运行时一行一行的来解析代码。
事实上当解释器到达一个作用域后,会先编译代码,然后再一行一行解析。
当JavaScript引擎运行到某个作用域后 ( 在第一个例子中,作用域是 global 全局作用域 )
它会有两个步骤
- 初始化阶段 ( Creation Stage) [当进入一个作用域,逐行运行代码之前]
- 创建
var变量,function函数和函数的arguments参数
- 创建
- 代码执行阶段 (Activation/Code Execution Stage)
- 给变量和函数赋值,以及执行代码
初始化阶段 ( Creation Stage)
在初始化阶段,进入一个作用域时会发生:
- 如果作用域是函数内部,把函数参数放进前面的
context json中- 扫描当前作用域寻找函数:
- 每发现一个函数,就把名字和函数指针放进前面的json中
- 如果函数名已经存在,覆盖之前的函数指针
- 扫描当前作用域寻找变量:
- 每发现一个变量
var,就把名字放进前面的json中,并把值设>成undefined- 如果变量名已经存在,不会覆盖,忽略然后继续扫描
代码执行阶段 (Activation/Code Execution Stage)
逐行执行代码,并且赋值之前为
undefined的变量var
例子1
我们回到之前的例子:
a = 2;
var a;
console.log( a ); /// 2
我们把之前讲的规则拿来套用:
- 首先进入全局作用域,初始化一个空的模拟作用域
json
pseudoContext = {
}
-
逐步执行代码之前,执行初始化阶段
i. 作用域不是函数内部,没用函数参数,忽略
ii. 扫描未发现函数,忽略
iii. 扫描发现变量
var a, 放进json里并设置成undefined -
此时我们的
json:
pseudoContext = {
variables: {
a = undefined
}
}
- 逐行执行代码
a = 2
扫描我们的作用域,发现pseudoContext里存在a,赋值成2
pseudoContext = {
a = 2
}
下一步:
console.log( a );
在作用域中找到 a, 发现有值,输出2
例子2
console.log( a ); //undefined
var a = 2;
- 首先进入全局作用域,初始化一个空的模拟作用域
json
pseudoContext = {
}
-
逐步执行代码之前,执行初始化阶段
i. 作用域不是函数内部,没用函数参数,忽略
ii. 扫描未发现函数,忽略
iii. 扫描发现变量
var a, 放进json里并设置成undefined -
此时我们的
json:
pseudoContext = {
variables: {
a = undefined
}
}
- 逐行执行代码
console.log( a );
json里a是undefined, 所以输出undefined
更复杂的例子3
console.log(typeof foo); // function pointer
console.log(typeof bar); // undefined
var foo = 'hello',
bar = function() {
return 'world';
};
function foo() {
return 'hello';
}
- 首先进入全局作用域,初始化一个空的模拟作用域
json
pseudoContext = {
}
-
逐步执行代码之前,执行初始化阶段
i. 作用域不是函数内部,没用函数参数,忽略
ii. 扫描发现函数
foo(第9行), 声明并赋值
pseudoContext = {
foo = function pointer
}
- 扫描发现变量
var foo, 因为foo名字已经存在了,依照之前规则 "如果变量名已经存在,不会覆盖,忽略然后继续扫描", 忽略 - 扫描发现变量
var bar, 赋值成undefined
引擎会先函数扫描,再变量扫描
- 此时我们的
json:
pseudoContext = {
foo = function pointer,
bar = undefined
}
- 逐行执行代码
console.log(typeof foo);
json里foo是function pointer,返回function
下一步:
console.log(typeof bar);
json里bar是undefined,输出undefined
let 和 const
希望以上讲的大家都能理解,再来说说let和const,这两个和var不同 他们在所谓的 时间静止区 temporal dead zone (TDZ)(不知道谁取的这么中二的名字)。
- 当进入一个作用域时,我们不会把它加在我们
json里 - 一开始
get或者set的时候就会报错ReferenceError - 逐行执行时,如果有
letconst声明,就会在作用域json里创建,如果赋值了,就会在作用域json里赋值
b // Uncaught ReferenceError: b is not defined
let b =2
总结
希望大家看了这篇文章对变量提升有更深的理解,变量提升只是表象,只是一个js解析器和作用域共同产生的一个结果。之后会更加详细的讲解一下作用域
参考:
What is the Execution Context & Stack in JavaScript? by David Shariff
You-Dont-Know-JS/ch4.md at master · getify/You-Dont-Know-JS · GitHub