【JS】变量提升的本质


在开始之前,我们先来看一看如下几个现象:

console.log(str); //undefined
console.log(fn); //ƒ fn() {}

var str = "fantasy";
function fn() {}

如果对上述输出结果不感到陌生,那么你或许对变量提升有了一定了了解。

没错,这就是JS的"特性"之一。但与其说是一种特性,其实更是一种JS最初的设计缺陷。这种令人费解的特性时常会产生某些迷惑的行为。而JS为了解决这种缺陷,在ES6引入了const和let。

那const和let是否真的就不存在变量提升了?接下来会从底层出发,介绍变量提升的本质,帮助你对其有更深刻的理解。

变量提升的根本原因

首先我们需要了解,什么是变量提升?

变量提升并不是说某个变量会被真的提升到代码块的顶部,而是指 在代码执行之前,变量被提前创建于内存之中,这个现象就叫变量提升(对变量提升的其中一种理解)

JS对一个变量声明语句的执行,可以分为以下三部分:创建,初始化,赋值。

创建

创建,就是将变量创建至内存的步骤。

在执行JS文件的过程中,解析引擎的方式是 先编译,后执行(在执行之时存在"边执行边解析"的JIT解析模式,与内容无关,不在此展开)。而解释,就是将JS代码生成一个AST抽象语法树,然后生成二进制编码的过程。在此过程中,就会将声明语句的变量,添加至进程所分配的内存当中,这也就对应着 "创建"。

也就是说,对于以下任意变量声明语句,在编译的过程中,都会将变量存放于内存当中:

var a = 10;
let b = 10;
const c = 10;

初始化

初始化介于声明与赋值之间,其也是const、let与var 在声明之前使用变量却存在不同现象的主要原因。

我们通过下述差异现象来区别分析var和let:

console.log(a); //undefined
var a = 10;

console.log(b); //Uncaught ReferenceError: Cannot access 'b' before initialization
let b = 10;

对于var声明的a变量,在其声明语句之前使用a并不会报错,而会获得一个undefined值;但是对于let声明的b变量,却直接报了ReferenceError。为什么同样存在被创建过的两个变量,会存在如此差异呢?

首先在JS引擎对文件的编译阶段,二者都会将变量创建至内存之中。对于var声明语句,还会还编译的过程中执行一个 "初始化" 的操作,将其初始化一个undefined的值;而对于let,则不会在编译过程中执行这一步,let的初始化会在执行过程中,真正的执行到了这一行代码才会进行初始化,也就是发生在执行阶段。

而初始化的值,就会存在于其对应的执行上下文的变量环境对象当中。当真正执行到读取变量的代码行之时,对于变量a来说,会去变量环境中查找该变量,因为其此时已经初始化,因此能够获取到一个undefined的值;但是对于变量b来说,并没有初始化的操作,也就无法在变量环境对象中找到该变量,因此在执行到读取b的代码行直接抛出了 Cannot access 'b' before initialization 。这也就是所谓的 "暂时性死区"。

对于执行上下文和变量环境对象不了解也没关系,我们先说概念,介绍完毕再到最后统一介绍。

赋值

赋值操作发生在JS引擎的执行过程中,也就是将企图赋予的值真正赋值给变量的步骤。

当执行到声明语句所在代码行的时候,才会进行赋值。其发生在执行阶段。如下述所示:

var a = 10; //10
let b = 10; //10

当赋值发生之后,会将执行上下文中的变量环境对象中相应的值进行更新,之后的代码语句再次读取相应的变量,从该对象中找到的就是最新赋值的值。

经过上述过程,一个变量才完成了一个声明流程。但是对于不同的情况,会有不同的规则。我们可以将它称为"提升规则"。

变量的提升规则

首先明确一点:这里的提升,是指在编译阶段发生的"提升"。

var创建提升;初始化提升
const创建提升
let创建提升
函数声明创建提升;初始化提升;赋值提升

这也就是为什么在函数声明之前可以获取函数体,但是在var声明之前获取到的变量只是一个undefined,其根本原因是因为,函数在编译阶段会额外的进行赋值提升,而var只会初始化提升。

大家可以再思考以下问题:

console.log(a);
var a = 123;
function a() {}

console.log(b);
function b() {}
var b = 123;

a和b的输出分别会是什么?

对于第一个示例,在JS引擎编译解析代码的过程中,首先遇到var,发生创建提升和初始化提升;然后继续解析,遇到函数声明,发现变量a已经创建并初始化,于是只进行复制提升;

对于第二个示例,编译过程中会先遇到函数声明,于是此时变量b创建提升,初始化提升,且赋值提升。当继续解析,遇到var只是,发现变量已经全部提升完毕,则不做任何事情。

因此,最终二者的输出结果是相同的,都是一个函数对象。

经过上述介绍,现在你或许知道如下例题会打印出什么:

console.log(a);
console.log(b);
function a() {}
var b = function () {};

了解了var和函数声明的差异,我们再来看看const和let的区别。

对于let和const来说,在编译阶段都会进行创建提升。也就是会将变量存于内存中,但是由于其没有进行初始化提升,因此不会存在于词法环境对象(与变量环境对象相似,但是这是一个针对于let和const的变量对象)。而当代码执行到相应的声明语句的时候,才会进行初始化。如:

let a 
const b = 1

在初始化之前,二者的行为都暂时保持统一。但是如果声明的时候包含了赋值语句,则二者会存在这样的差别:

let a = 1;
const b = 2;

对于let来说,会在初始化之后,立刻进行赋值操作;但是对于const,其本身不存在赋值这一说,只有初始化操作。也就是说,const b = 2 从本质上并不是先初始化b,然后赋值为2,正确的解释应该是给b初始化为2,并且以后永远都不能进行赋值操作。这也是const的特性:"常量"。

变量环境和词法环境

解释完变量提升,我们再来解释一下在之前的描述过程中出现的变量环境对象、词法环境对象、执行上下文等概念。

首先,什么是执行上下文:在每一个函数调用之时,都会创建一个对应的执行上下文活动对象,该活动对象会在函数执行完毕之后销毁(不考虑闭包的情况下)。而对于全局环境来说,同样的,会存在一个全局执行上下文。

对于执行上下文活动对象,可以参考下图理解:

在执行上下文栈(也就是函数调用栈)中,存在的就是一个个如此的活动对象。

对于var、函数声明来说,其存放的位置为变量环境对象;而对于const和let来说,其存放的位置为词法环境对象。之所以会存在两个不同的环境对象,是因为在支持ES6的新语法的同时,依旧需要保持对旧语法的支持,也就是向下兼容。

结合代码来看:

var a = 123;
function b() {}
let c = 321;
const d = 456;

 如上代码在执行到a声明语句之前的执行上下文中,应该是这样的:

 而当其执行完d的声明语句,则应该是这样的:

 这也就是为什么在声明语句的前面可以访问var和函数声明的变量,但是却无法访问let和const声明的的变量的原因。而这这种现象,也是为什么会在有些地方将const和let解释为不存在变量提升的根本原因。

对于变量提升这一说法,本身就是一个"伪命题",它并没有很清晰的定义与边界。因此,对于const和let是否存在变量提升,无论怎么回答都是有道理的。

如果边界定义为能否在变量的声明语句执行之前使用,也就是变量是否会提升至变量环境对象或词法环境对象(初始化提升),那么const和let的确没有变量提升;

但是如果将变量提升从本质上去理解,他又确实存在提升,因为在JS引擎的编译阶段const和let的变量也会像var和函数声明一样,添加至内存当中(创建提升)。

甚至是在MDN文档当中,对于变量提升这一现象,也是存在着分歧的。从一开始所说的存在变量提升,到现在的解释:

let 允许你声明一个作用域被限制在作用域中的变量、语句或者表达式。与 var 关键字不同的是,var 声明的变量作用域是全局或者整个函数块的。 var 和 let 的另一个重要区别,let 声明的变量不会在作用域中被提升。

因此,除非"变量提升"这一概念被明确的定义,只要你能够自圆其说,怎么解释都可行。比如我就更倾向于将变量提升理解为创建提升。

经过了上述的介绍,你应该对于变量提升有了不一样的理解了吧!

文中内容均带有个人理解,并不保证权威。若有错误,欢迎随时批评指正。

  • 14
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值