开始执行脚本时,执行脚本的第一步是编译代码,然后再开始执行代码,如图
另外,在编译优化方面来说,最开始时也并不是全部编译好脚本,而是当函数执行时,才会先编译,再执行脚本,如图
编译阶段:经历了词法分析,语法分析生成 AST,以及代码生成。并且在此阶段,它只会扫描并且抽出环境中的声明变量,声明函数以便准备分配内存,所有的函数声明和变量声明都会被添加到名为 Lexical Environment 的 JavaScript 内部数据结构内的内存中。因此,它们可以在源代码中实际声明之前使用。但是,Javascript 只会存储函数声明和变量声明在内存,并不会存储他们的值
执行阶段:给变量 x 赋值,首先询问内存你这有变量 x 吗,如果有,则给变量 x 赋值,如果没有则创建变量 x 并且给它赋值。
变量提升
如下图,左边灰色块区域,是演示函数执行前的编译阶段,先抽出所有声明变量和声明函数,并进行内存分配。然后再开始执行代码,在执行第一行代码的时候,若是变量 a 存在于内存中,则直接给变量 a 赋值。而执行到第二行时,变量 b 并没有在内存中,则会创建变量 b 并给它赋值。
Lexical enviroment 是一种包含标识符变量映射的数据结构
LexicalEnviroment = {
Identifier: <value>,
Indentifier: <function object>
}
简而言之,Lexical enviroment 就是程序执行过程中变量和函数存在的地方。
let,const 变量
console.log(a)
let a = 3;
输出
ReferenceError: a is not defined
所以 let 和 const 变量并不会被提升吗?
这个答案会比较复杂。所有的声明 (function, var, let, const and class) 在 JavaScript 中都会被提升,然而 var 声明被 undefined 值初始化,但是 let 和 const 声明的值仍然未被初始化。
它们仅仅只在 Javascript 引擎运行期间它们的词法绑定被执行在才会被初始化。这意味着引擎在源代码中声明它的位置计算其值之前,你无法访问该变量。这就是我们所说的时间死区,即变量创建和初始化之间的时间,我们无法访问该变量。
如果 JavaScript 引擎仍然无法在声明它们的行中找到 let 或者 const 的值,它将为它们分配 undefined 值或返回错误值(在 const 的情况下会返回错误值)。
6a9a50532bf60f5fac6b3c.png](evernotecid://F2BCA3B5-CC5A-4EB3-BD61-DD865800F342/appyinxiangcom/10369121/ENResource/p1163)
let a;
console.log(a); // outputs undefined
a = 5;
在编译阶段,JavaScript 引擎遇到变量 a 并将它存储在 lexical enviroment,但是因为它是一个 let 变量,所以引擎不会为它初始化任何值。所以,在编译阶段,lexical enviroment 看起来像下面这样。
// 编译阶段
lexicalEnvironment = {
a: <uninitialized>
}
现在如果我们尝试在声明它之前访问该变量,JavaScript 引擎将会尝试从词法环境中拿到这个变量的值,因为这个变量未被初始化,它将抛出一个引用错误。
在执行期间,当引擎到达了变量声明的行,它将试图执行它的绑定,因为该变量没有与之关联的值,因此它将为其赋值为 unedfined
// 执行阶段
lexicalEnviroment = {
a: undefined
}
之后,undefined 将会被打印到控制台,然后将值 5 赋值给变量 a,lexical enviroment 中变量 a 的值也会从 undefined 更新为 5
functionn foo() {
console.log(a)
}
let a = 20;
foo();
function foo() {
console.log(a): // ReferenceError: a is not defined
}
foo();
let a = 20;
Class Declaration
就像 let 和 const 声明一样,class 在 JavaScript 中也会被提升,并且和 let,const 一样,知道执行之前,它们都会保持 uninitialized。因此它们同样会受到 Temporal Deal Zone(时间死区)的影响。例如
let peter = new Person(‘Peter’, 25); // ReferenceError: Person is not defined
console.log(peter);
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
因此要访问 class,必须先声明它
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let peter = new Person(‘Peter’, 25);
console.log(peter);
// Person {name: ‘Peter’, age: 25}
所以在编译阶段,上面代码的 lexical environment(词法环境)将如下所示:
lexicalEnvironment = {
Person: <uninitialized>
}
当引擎执行 class 声明时,它将使用值初始化类。
lexicalEnvironment = {
Person: <Person object>
}
提升 Class Expressions
let peter = new Person(‘Peter’, 25);
console.log(peter);
let Person = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let peter = new Person(‘Peter’, 25);
console.log(peter);
var Person = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
所以现在我们知道在提升过程中我们的代码并没有被 JavaScript 引擎实际移动。正确理解提升机制将有助于避免因变量提升而产生的任何未来错误和混乱。为了避免像未定义的变量或引用错误一样可能产生的副作用,请始终尝试将变量声明在各自作用域的顶部,并始终尝试在声明变量时初始化变量。
Hoisting in Modern JavaScript — let, const, and var