【JS】变量提升的本质

article/2025/9/16 19:01:50


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

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 声明的变量不会在作用域中被提升。

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

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

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


http://chatgpt.dhexx.cn/article/oTcahjDx.shtml

相关文章

js作用域和变量提升

作用域 作用域的分类 全局作用域 在一个js脚本中,最外层的作用域就是全局作用域,在此范围内声明的任何变量都会是全局变量,可以在程序的任意位置访问 局部作用域 函数作用域 var locales { school: function() { // 这里的school…

js之变量提升

首先 javascript 是一种弱类型、动态的、解释型的脚本语言。 弱类型:类型检查不严格,偏向于容忍隐式类型转换。 强类型:类型检查严格,偏向于不容忍隐式类型转换。 动态类型:运行的时候执行类型检查。 静态类型&…

JS中的变量提升总结

1.JS代码执行顺序 我们直觉上会认为JS的代码在执行时是由上到下一行一行执行的,但实际并不完全正确,下面的例子会证明: a haha var a console.log(a)上面的代码会输出什么呢? 如果按照我们认为的由上到下一行一行执行&#xf…

Flink Table 和 DataStream 转换

文章目录 Flink Table 和 DataStream 转换1. 表(Table) 转换为 流(DataStream)1.1 处理(仅插入)流1.1.1 fromDataStream()方法:1.1.1.1 fromDataStream(DataStream var1)1.1.1.2 fromDataStream(DataStream var1, Expression... var2)1.1.1.3…

数据流—DataStreamAPI

Hello Flink 1:构建一个典型的Flink流式应用需要一下几步: 1:设置执行环境。 2:从数据源中读取一条或多条流 3:通过一系列流式转换来实现应用逻辑。 4:选择性的将结果输出到一个或多个数据汇(用…

【Flink】DataStream API使用之转换算子(Transformation)

转换算子(Transformation) 数据源读入数据之后,就是各种转换算子的操作,将一个或者多个DataSream转换为新的DataSteam,并且Flink可以针对一条流进行转换处理,也可以进行分流或者河流等多流转换操作&#xf…

Flink-DataStream执行环境和数据读取

​编辑执行环境 创建执行环境 执行模式 触发程序执行 源算子(Source) 读取有界数据流 读取无界数据 读取自定义数据源(源算子) DataStream是一个 Flink 程序,其实就是对 DataStream 的各种转换。具体来说&#xff0c…

Flink数据流类型之间的转换(WindowedStream、DataStream、KeyedStream、AllWindowStream之间的转换)

Flink提供了一些流API,其中包括WindowedStream、DataStream、KeyedStream和AllWindowStream。 🍊WindowedStream是一种特殊的流,其中数据已按时间或数据元素的键进行分组,并且每个分组的数据都在窗口中按时间划分。这意味着&…

DataStream API

目录 原算子 准备工作,环境搭建 读取数据 从文件中读取数据 从集合中读取数据 从元素中读取数据 从source文件中读取数据 从kafka中读取数据 自定义source类型输出 转换算子 map转换 Filter转换 FlatMap转换 原算子 准备工作,环境搭建 为…

Flink学习——DataStream API

一个flink程序,其实就是对DataStream的各种转换。具体可以分成以下几个部分: 获取执行环境(Execution Environment)读取数据源(Source)定义基于数据的转换操作(Transformations)定义…

大数据开发-Flink-数据流DataStream和DataSet

文章目录 一、DataStream的三种流处理Api1.1 DataSource1.2 Transformation1.3 Sink 二、DataSet的常用Api2.1 DataSource2.2 Transformation2.3 Sink Flink主要用来处理数据流,所以从抽象上来看就是对数据流的处理,正如前面大数据开发-Flink-体系结构 &…

Flink DataStream API 介绍

Flink DataStream API 介绍 StreamExecutionEnvironment #mermaid-svg-JKeWa22W2vWA4zBS {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-JKeWa22W2vWA4zBS .error-icon{fill:#552222;}#mermaid-svg-JKeWa22W2vWA4z…

DataStream API介绍与使用(一)

详细API参考官网 DataStream编程模型 在Flink整个系统架构中,对流计算的支持是其最重要的功能之一,Flink基于Google提出的DataFlow模型,实现了支持原生数据流处理的计算引擎。Flink中定义了DataStream API让用户灵活且高效地编写Flink流式应…

DataStream API(一)

Flink 有非常灵活的分层 API 设计,其中的核心层就是 DataStream/DataSet API。由于新版 本已经实现了流批一体, DataSet API 将被弃用,官方推荐统一使用 DataStream API 处理流数 据和批数据。由于内容较多,我们将会用几章的篇幅来…

DataStream(二)

目录 5.3.2 聚合算子(Aggregation) 5.3.3 用户自定义函数(UDF) 3. 扁平映射(flatMap) flatMap 操作又称为扁平映射,主要是将数据流中的整体(一般是集合类型)拆分成一个 …

Flink DataStream API

Flink DataStream API 编程指南 概览前言什么是DataStreamFlink程序剖析程序样例 Data SourcesDataStream Transformations算子数据流转换算子物理分区算子链和资源组 Data Sinks迭代执行参数 概览 前言 Flink中的DataStream程序是常规程序,可对数据流进行转换&am…

DataStream API(三)

目录 5.3.4 物理分区(Physical Partitioning) 5.4 输出算子(Sink) 5.4.1 连接到外部系统 5.4.2 输出到文件 5.4.3 输出到 Kafka 5.4.4 输出到 MySQL(JDBC) 5.4.5 自定义 Sink 输出 5.5 本章总结 5.3.…

流式数据采集和计算(十):Flink的DataStream学习笔记

Flink的DataStream学习笔记.. 1 Flink 基础.. 3 Flink特性.. 3 Flink和Spark对比.. 3 设计思路.. 3 状态管理.. 3 Flink 初探.. 4 设计架构.. 4 Flink on yarn. 5 流程分析.. 6 DataStream. 7 API程序结构.. 7 DataSource 8 Transformation. 9 Sink. 13 Time 14…

DataStream API(基础篇) 完整使用 (第五章)

DataStream API基础篇 一、执行环境(Execution Environment)1、创建执行环境1. getExecutionEnvironment2. createLocalEnvironment3. createRemoteEnvironment 二、执行模式(Execution Mode)1. BATCH模式的配置方法(1)通过命令行…

DataStream API 四 之 Flink DataStream编程

DataStream API 四 之 Flink DataStream编程 1.分布式流处理基本模型2.流应用开发步骤3.数据类型4. Connector5. Execution environment6. 参数传递7.配置并⾏度8.Watermark9.Checkpoint10.State11. Data Source11.111.2 自定义Source 12.Transformations13.Window13.1窗⼝处理…