前沿
大家对变量提升(hoisting)一定不陌生了。这篇文章希望可以建立一个系统的规则。以后碰到变量提升。可以重新看这篇文章。根据文中的规则找到答案。最终。我们可以记住这个规则。很自然的理解变量提升。
例子
相信大家对这个例子不陌生了:
a = 2;var a;console.log( a ); /// 2
为什么这段话a 在声明之前就可以赋值了呢?
再看下这个例子:
console.log( a ); //undefinedvar a = 2;
为什么这个又是输出undefined呢?
别担心。看完整个文章你就会知道运用怎样的规则来推断结果了。
作用域浅析
在这里我们对作用域不做详细的讲解。作用域可以想象成可访问对象的集合。
打个通俗的比方。现在有个公寓管理公司。底下有多个公寓。高级管理员A。 他有所有公寓所有房间的钥匙。所以他能打开所有公寓所有房间。并能拿出所有房间的物品。中级管理员B。只有某一栋公寓所有房间的钥匙。他能拿出所有此公寓所有房间的物品。但是不能拿出其他公寓的物品。原因就是管理员B在拿物品时(程序执行代码时)。 并没用其他公寓的信息(其他作用域内信息)。
为了简单。本篇文章只涉及同一个作用域下。不同作用域的交互之后几篇会讲到
大家可以想像成进入到全局作用域或者某个函数作用域时。引擎会产生一个json object 当作资料库。 之后代码执行的时候会从这个json 中找值。
var pseudoContext = {}
JavaScript解析器
一般来说。大家可能觉得JavaScript解析器会在 run-time 运行时一行一行的来解析代码。
事实上当解释器到达一个作用域后。会先编译代码。然后再一行一行解析。
当JavaScript引擎运行到某个作用域后 ( 在第一个例子中。作用域是 global 全局作用域 )
它会有两个步骤
1. 初始化阶段 ( Creation Stage) [当进入一个作用域。逐行运行代码之前]
+ 创建 var 变量。 function 函数和函数的arguments 参数
2. 代码执行阶段 (Activation/Code Execution Stage)
+ 给变量和函数赋值。以及执行代码
初始化阶段 ( Creation Stage)
在初始化阶段。进入一个作用域时会发生:
1. 如果作用域是函数内部。把函数参数放进前面的context json 中
2. 扫描当前作用域寻找函数:
+ 每发现一个函数。就把名字和函数指针放进前面的json中
+ 如果函数名已经存在。覆盖之前的函数指针 3. 扫描当前作用域寻找变量:
+ 每发现一个变量 var。就把名字放进前面的json中。并把值设>成 undefined
+ 如果变量名已经存在。不会覆盖。忽略然后继续扫描
代码执行阶段 (Activation/Code Execution Stage)
逐行执行代码。并且赋值之前为undefined的变量var
例子1
我们回到之前的例子:
a = 2;var a;console.log( a ); /// 2
我们把之前讲的规则拿来套用:
首先进入全局作用域。初始化一个空的模拟作用域 json逐步执行代码之前。执行初始化阶段作用域不是函数内部。没用函数参数。忽略扫描未发现函数。忽略扫描发现变量var a。 放进json里并设置成undefined此时我们的 json:
pseudoContext = { a = undefined}a = 2
4.扫描完成。逐行执行代码
a = 2
5.扫描我们的作用域。发现pseudoContext里存在a。赋值成2
pseudoContext = { a = 2}
6.下一步:
console.log( a );
7.在作用域pseudoContext 中找到 a。 发现有值。输出2
例子2
console.log( a ); //undefinedvar a = 2;
首先进入全局作用域。初始化一个空的模拟作用域json逐步执行代码之前。执行初始化阶段作用域不是函数内部。没用函数参数。忽略扫描未发现函数。忽略扫描发现变量var a。 放进json里并设置成undefined此时我们的json:
pseudoContext = { variables: { a = undefined }}
4.扫描完成。逐行执行代码
console.log( a );
json里a是undefined。 所以输出undefined
更复杂的例子3
console.log(typeof foo); // function pointerconsole.log(typeof bar); // undefinedvar foo = ‘hello’。 bar = function() { return ‘world’; };function foo() { return ‘hello’;}
1.首先进入全局作用域。初始化一个空的模拟作用域json
2.逐步执行代码之前。执行初始化阶段
3.作用域不是函数内部。没用函数参数。忽略
4.扫描发现函数foo(第9行)。 声明并赋值
5.此时的 json:
pseudoContext = { foo = function pointer}
6.扫描发现变量var foo。 因为foo名字已经存在了。依照之前规则 “如果变量名已经存在。不会覆盖。忽略然后继续扫描”。 忽略
7.扫描发现变量 var bar。 赋值成undefined
引擎会先函数扫描。再变量扫描
8.此时我们的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 definedlet b =2
总结
希望大家看了这篇文章对变量提升有更深的理解。变量提升只是表象。只是一个js解析器和作用域共同产生的一个结果。之后会更加详细的讲解一下作用域
关注我的头条号。分享更多的技术学习文章。我自己是一名从事了多年开发的web前端老程序员。目前辞职在做自己的web前端私人定制课程。今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货。各种框架都有整理。送给每一位前端小伙伴。想要获取的可以关注我的头条号并在后台私信我:前端。即可免费获取。