许多JavaScript程序员将提升解释为JavaScript将声明(变量和函数)移至其当前作用域(函数或全局)顶部的行为。 好像它们实际上已经移到了代码的顶部一样,事实并非如此。例如:
console.log(a);
var a= 'Hello World!';
他们会说,上面的代码将在提升后转换为以下代码:
var a;
console.log(a);
a ='Hello World!';
尽管这似乎正在发生(因为代码可以正常工作),但实际上并没有发生,您的代码也无处可去。JavaScript引擎实际上并没有移动您的代码,您的代码停留在您键入代码的位置。
那么,什么是提升呢?
在编译阶段,即在代码执行前的几微秒内,将对其进行扫描以查找函数和变量声明。所有这些函数和变量声明都被添加到称为词汇环境(Lexical Environment) 的JavaScript数据结构内部的内存中。这样,即使在源代码中实际声明它们之前也可以使用它们。
什么是词汇环境?
词法环境 是一个数据结构,具有标识符变量映射。(这里的标识符是指变量/函数的名称,而变量是对实际对象[包括函数对象]或原始值的引用)。
词汇环境在概念上是这样的:
LexicalEnvironment = {
Identifier: <value>,
Identifier: <function object>
}
简而言之,词汇环境是程序执行过程中变量和函数存在的地方。
现在我们知道了提升实际上是什么,让我们来看看如何提升发生的函数和变量(var
,let
和const
)声明。
提升函数声明:
helloWorld(); // prints 'Hello World!' to the console
function helloWorld(){
console.log('Hello World!');
}
我们已经知道函数声明是在编译阶段添加到内存中的,因此我们可以在实际函数声明之前在代码中对其进行访问。
因此,上述代码的词法环境如下所示:
lexicalEnvironment = {
helloWorld:<func>
}
因此,当JavaScript引擎遇到对的调用时helloWorld()
,它将查看词法环境,找到该函数并能够执行它。
提升函数表达式
JavaScript仅提升函数声明,不提升函数表达式。例如:此代码将不起作用。
helloWorld(); // TypeError: helloWorld is not a function
var helloWorld = function(){
console.log('Hello World!');
}
由于JavaScript仅提升声明,而不初始化(赋值),因此helloWorld
会被视为变量,而不是函数。因为helloWorld
是var
变量,所以引擎将undefined
在提升过程中为其分配值。
因此,此代码将起作用。
var helloWorld = function(){
console.log('Hello World!'); prints 'Hello World!'
}
helloWorld();
提升var变量:
让我们看一些示例,以了解在var
变量情况下的提升。
console.log(a); // outputs 'undefined'
var a = 3;
我们期望,3
但是得到了undefined
。为什么?
请记住,JavaScript仅是提升声明,而不是初始化。也就是说,在编译期间,JavaScript仅将函数和变量声明存储在内存中,而不存储它们的分配(值)。
但是为什么undefined
呢?
当JavaScript引擎var
在编译阶段找到变量声明时,它将将该变量添加到词法环境中,并使用进行初始化,undefined
并在执行过程中稍后到达代码中实际分配的行时,它将分配该值到变量。
因此,以上代码的初始词汇环境如下所示:
lexicalEnvironment = {
a: undefined
}
因此,我们得到undefined
而不是3
。当引擎到达完成实际分配的行(在执行过程中)时,它将在其词法环境中更新变量的值。因此,赋值后的词法环境将如下所示:
lexicalEnvironment = {
a:3
}
提升let和const变量:
让我们先来看一些例子:
console.log(a);
let a = 3;
输出:
ReferenceError: a is not defined
那么let
和const
变量没有被提升吗?
答案要复杂得多。
所有声明(函数,var,let,const和类)都使用JavaScript进行了提升,而 var 声明用初始化 undefined, 但 let 和 const 声明保持未初始化。
只有在运行时由JavaScript引擎评估其词法绑定(赋值)时,它们才会被初始化。这意味着您无法在引擎在源代码中声明的位置评估其值之前访问该变量。这就是我们所谓的“时间死区”,即从变量创建到其无法访问它们的初始化之间的时间跨度。
如果JavaScript引擎在声明它们的行上仍找不到let
或的值const
,它将为它们分配的值undefined
或返回错误(如果是const
)。
让我们来看更多示例:
let a;
console.log(a); // outputs undefined
a = 5;
在这里,在编译阶段,JavaScript引擎会遇到该变量a
并将其存储在词法环境中,但是由于它是一个let
变量,因此引擎不会使用任何值对其进行初始化。因此,在编译阶段,词法环境将如下所示:
lexicalEnvironment = {
a: <uninitialized>
}
现在,如果我们尝试在声明变量之前访问变量,则JavaScript引擎将尝试从词法环境中获取变量的值,因为该变量未初始化,因此将引发引用错误。
在执行期间,当引擎到达声明该变量的行时,它将尝试评估其绑定(值),因为该变量没有与之关联的值,因此它将对其进行分配undefined
。
因此,执行第一行后,词法环境将如下所示:
lexicalEnvironment = {
a: undefined
}
而undefined
将被记录到控制台,之后5
将被分配给它的词法环境将得到更新,以包含的价值a
,以5
从undefined
。
注意 只要在变量声明之前不执行该代码,我们甚至可以在声明它们之前在代码(例如,函数主体)中引用let
和const
变量。
例如,此代码是完全有效的。
function foo () {
console.log(a);
}
let a = 20;
foo(); // This is perfectly valid
但下面的例子会产生引用错误。
function foo() {
console.log(a); // ReferenceError: a is not defined
}
foo(); // This is not valid
let a = 20;
提升等级声明
正如let
和const
声明,在JavaScript类也提升,同样let
或const
声明,他们仍然未初始化状态的评价。因此,它们也受到“临时死区”的影响。例如:
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 Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let peter = new Person('Peter', 25);
console.log(peter);
// Person { name: 'Peter', age: 25 }
因此,再次在编译阶段,上述代码的词法环境将如下所示:
lexicalEnvironment = {
Person: <uninitialized>
}
当引擎评估了class语句后,它将使用该值初始化该类。
lexicalEnvironment = {
Person: <Person object>
}
提升类表达式
就像函数表达式一样,类表达式也不会被提升。例如,此代码将不起作用。
let peter = new Person('Peter', 25); // ReferenceError: Person is
// not defined
console.log(peter);
let Person = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
正确的方法是这样的:
let Person = class {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let peter = new Person('Peter', 25);
console.log(peter);
// Person { name: 'Peter', age: 25 }
结论
因此,现在我们知道,在提升过程中,JavaScript引擎不会实际移动代码。正确理解提升机制将有助于您避免将来因提升而引起的任何错误和混乱。为避免未定义的变量或参考错误等提升的副作用,请始终尝试在变量的各自作用域顶部声明变量,并始终在声明变量时尝试初始化变量。
网友评论