了解Javascript的变量提升
前言
本文2922字,阅读大约需要8分钟。
总括: 什么是变量提升,使用var,let,const,function,class公告的变量函数类在变量提升的时候都有什么区别。
- 参考文章:Hoisting in Modern JavaScript — let, const, and var
- 公众号:「前台进阶学习」,回复「666」,获取一揽子前台技术书籍
要么庸俗,要么孤独。
正文
Javascript中的变量提升说的是在程序中可以在变量公告之前就进行使用:
console.log(a); // undefinedvar a = 1;可以看到,在变量a公告之前我们可以正常调用a,代码的实际的体现更像是这样的:
var a;console.log(a); // undefineda = 1;但实际上,代码并没有被改变,上面的代码只是我们猜测的,其实Javascript引擎在执行这几行代码的时候并没有移动或者是改变代码的结果。究竟发生了什么呢?
变量提升
在代码的编译期间,即代码真正执行的瞬息之间,引擎会将代码块中所有的变量公告和函数公告都记录下来。这些函数公告和变量公告都会被记录在一个名为词法环境的数据结构中。词法环境是Javascript引擎中一种记录变量和函数公告的数据结构,它会被直接保存在内存中。所以,上面的console.log(a)可以正常执行。
什么是词法环境
所谓词法环境就是一种标识符—变量映射的结构(这里的标识符指的是变量/函数的名字,变量是对实际对象[包含函数和数组类型的对象]或者基础数据类型的引用)。
简单地说,词法环境是Javascript引擎用来存储变量和对象引用的地方。
词法环境的结构用伪代码表示如下:
LexicalEnvironment = { Identifier: <value>, Identifier: <function object>}关于词法环境更多的理解可以看博主之前的译文:了解Javascript中的执行上下文和执行栈。
理解了词法环境接下来让我依次看下使用var,const,let,function,class公告的变量或者函数的情况。
function公告提升
helloWorld(); // 打印 'Hello World!'function helloWorld(){ console.log('Hello World!');}我们已经知道了,函数公告会在编译阶段就会被记录在词法环境中并且保存在内存中,因而我们可以在函数进行实际公告之前对该函数进行访问。
上面函数公告保存在词法环境中像下面这样:
lexicalEnvironment = { helloWorld: < func >}所以在代码执行阶段,当Javascript引擎碰到helloWorld()这行代码,会在词法环境中寻觅,而后找到这个函数并执行它。
函数表达式
注意,只有函数公告才会被直接提升,使用函数表达式公告的函数不会被提升,看下面代码:
helloWorld(); // TypeError: helloWorld is not a functionvar helloWorld = function(){ console.log('Hello World!');}如上,代码报错了。使用var公告的helloWorld是个变量,并不是函数,Javascript引擎只会把它当成普通的变量来解决,而不会在词法环境中给它赋值。
保存在词法环境中像下面这样:
lexicalEnvironment = { helloWorld: undefined}上面的代码要想可以正常运行改写如下就可:
var helloWorld = function(){ console.log('Hello World!');}helloWorld(); // 打印 'Hello World!'var变量提升
看一个使用var公告变量的例子:
console.log(a); // 打印 'undefined'var a = 3;假如按上面function函数公告的方式去了解,这里应该打印3,但实际上打印了undefined。
请记住:所谓的公告提升只是在编译阶段Javascript引擎将函数公告和变量公告存储在词法环境中,但不会给它们赋值。等到了执行阶段,真正执行到赋值那一行的时候,词法环境才会升级。
但上面的代码为什么打印了undefined呢?
Javascript引擎会在编译阶段将使用var公告的变量保存在词法环境中,并将它初始化为undefined。到了执行阶段,等执行到赋值那一行代码的时候,词法环境中变量的值才会被升级。
所以上面代码的词法环境初始化像下面这样:
lexicalEnvironment = { a: undefined}这也解释了为什么前面使用函数表达式公告的函数执行会报错,为什么上面的代码会打印undefined。当代码执行到var a = 3;这行代码的时候,词法环境中a的值就会被升级,此时词法环境会被升级如下:
lexicalEnvironment = { a: 3}let和const变量提升
看一个使用let公告变量的例子:
console.log(a);let a = 3;输出:
Uncaught ReferenceError: Cannot access 'a' before initialization再看一个使用const公告变量的例子:
console.log(b);const b = 1;输出:
Uncaught ReferenceError: Cannot access 'b' before initialization和var不同,相同结构的代码换成let或者是const都直接报错了。
难道使用let和const公告的变量不存在变量提升的情况么?
实际上,在Javascript中所有公告的变量(var,const,let,function,class)都存在变量提升的情况。使用var公告的变量,在词法环境中会被初始化为undefined,但用let和const公告的变量并不会被初始化。
使用let和const公告的变量只有在执行到赋值那行代码的时候才会真正给他赋值,这也意味着在执行到变量公告的那行代码之前访问那个变量都会报错,这就是我们常说的暂时性死区(TDZ)。即在变量公告之前都不能对变量进行访问。
当执行到变量公告的那一行的时候,但是依然没有赋值,那么使用let公告的变量就会被初始化为undefined;使用const公告的变量就会报错; 看实际的例子:
let a;console.log(a); // 输出 undefineda = 5;在代码编译阶段,Javascript引擎会把变量a存储在词法环境中,并把a保持在未初始化的状态。此时词法环境像下面这样:
lexicalEnvironment = { a: <uninitialized>}此时假如尝试访问变量a或者是b,Javascript引擎会在词法环境中找到该变量,但此时变量处于未初始化的状态,因而会抛出一个引用错误。
而后在执行阶段,Javascript引擎执行到赋值(专业点叫词法绑定)那一行的时候,会评估被赋值的值,假如没有被赋值,只是简单的公告,此时就会给let公告的变量赋值为undefined;此时词法环境像下面这样:
lexicalEnvironment = { a: undefined}当执行到a = 5这一行的时候,词法环境再次升级:
lexicalEnvironment = { a: 5}再看下使用const公告代码的情况:
let a;console.log(a);a = 5;const b;console.log(b);输出:
Uncaught SyntaxError: Missing initializer in const declaration上面代码直接报错,a的值也没有打印,直接报错,其实是代码在编译阶段就已经报错了,压根没执行到console.log(a);这一行代码。
注意:在函数中,只需是能在变量公告之后引用该变量就不会报错。
什么意思呢?看如下代码:
function foo () { console.log(a);}let a = 20;foo(); // 打印 20但下面代码就会报错:
function foo () { console.log(a);}foo();let a = 20; // 报错: Uncaught ReferenceError: Cannot access 'a' before initialization这里报错的起因需要结合Javascript中的执行上下文和执行栈才能了解,由于此时全局执行上下文中词法环境中保存的变量a处于未初始化的状态,调用foo函数,创立了一个函数执行上下文,而后函数foo执行过程对全局执行上下文的变量a进行访问,但a还处于未初始化的状态(此时let a = 20还没有执行)。因而报错。
这里需要纠正一个误区,就是let和const公告的变量只有暂时性死区,不存在变量提升,其实是不对的,举个例子证实了解一下:
let a = 1;{ console.log(a); let a = 2;}上面的代码会被报错:
Uncaught ReferenceError: Cannot access 'a' before initialization假如不存在变量提升,理论上不会报错才对。
class公告提升
与let、const相似,使用class公告的类也会被提升,而后这个类公告会被保存在词法环境中但处于未初始化的状态,直到执行到变量赋值那一行代码,才会被初始化。另外,class公告的类一样存在暂时性死区(TDZ)。看例子:
let peter = new Person('Peter', 25); console.log(peter);class Person { constructor(name, age) { this.name = name; this.age = age; }}打印:
Uncaught ReferenceError: Cannot access 'Person' before initialization改写如下即可以正常运行了:
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);console.log(peter);function Person(name, age) { this.name = name; this.age = age;}// Person { name: 'Peter', age: 25 }上面代码正常运行。
类表达式
和函数表达式一样,类表达式也一样会被提升,比方:
let peter = new Person('Peter', 25);console.log(peter);let Person = class { constructor(name, age) { this.name = name; this.age = age; }}报错:
Uncaught ReferenceError: Cannot access 'Person' before initialization要想正常运行,改写如下就可:
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 }也就是说不论是函数表达式还是类表达式遵循的规则和变量公告是一样的。
结论
不论是var,const,let,function,class公告的变量还是函数都存在变量提升的情况。正确了解变量提升有助于我们写更好的代码。整个变量提升的情况总结如下:
var:存在变量提升,在编译阶段会被初始化为undefined;let: 存在变量提升,存在暂时性死区(TDZ),执行阶段,假如没赋值,则初始化为undefined;const: 存在变量提升,存在暂时性死区(TDZ),假如没有赋值,编译阶段就会报错;function:存在变量提升,在变量公告之前可以访问并执行;class: 存在变量提升,存在暂时性死区(TDZ);
能力有限,水平一般,欢迎勘误,不胜感激。
订阅更多文章可关注公众号「前台进阶学习」,回复「666」,获取一揽子前台技术书籍
前台进阶学习
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 了解Javascript的变量提升