第 83 题:var、let 和 const 区别的实现原理是什么
yygmind opened this issue · 27 comments
先说说这三者的区别吧:
-
var 和 let 用以声明变量,const 用于声明只读的常量;
-
var 声明的变量,不存在块级作用域,在全局范围内都有效,let 和 const 声明的,只在它所在的代码块内有效;
-
let 和 const 不存在像 var 那样的 “变量提升” 现象,所以 var 定义变量可以先使用,后声明,而 let 和 const 只可先声明,后使用;
-
let 声明的变量存在暂时性死区,即只要块级作用域中存在 let,那么它所声明的变量就绑定了这个区域,不再受外部的影响。
-
let 不允许在相同作用域内,重复声明同一个变量;
-
const 在声明时必须初始化赋值,一旦声明,其声明的值就不允许改变,更不允许重复声明;
如 const 声明了一个复合类型的常量,其存储的是一个引用地址,不允许改变的是这个地址,而对象本身是可变的。
然后是原理,原理确实没认真研究过,在网上翻了一番资料,结合自己的理解简单说下(纯属个人愚见,高手轻喷):
变量与内存之间的关系,主要由三个部分组成:
- 变量名
- 内存地址
- 内存空间
JS 引擎在读取变量时,先找到变量绑定的内存地址,然后找到地址所指向的内存空间,最后读取其中的内容。当变量改变时,JS 引擎不会用新值覆盖之前旧值的内存空间(虽然从写代码的角度来看,确实像是被覆盖掉了),而是重新分配一个新的内存空间来存储新值,并将新的内存地址与变量进行绑定,JS 引擎会在合适的时机进行 GC,回收旧的内存空间。
const 定义变量(常量)后,变量名与内存地址之间建立了一种不可变的绑定关系,阻隔变量地址被改变,当 const 定义的变量进行重新赋值时,根据前面的论述,JS 引擎会尝试重新分配新的内存空间,所以会被拒绝,便会抛出异常。
坐等高手答疑。
一、实现原理
(一)、var的实现原理
(二)、let的实现原理
(三)、const的实现原理
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
二、应用场景
(一)var应用场景
(二)let应用场景
for
循环的计数器,就很合适使用let
命令
(三)const应用场景
三、var、let、const的区别
(一)var
var
命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined
。- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
(二)let
- 声明的�全局变量不会挂在顶层对象下面
- 所声明的变量一定要在声明后使用,否则报错,报错
ReferenceError
- 暂时性死区,只要块级作用域内存在
let
命令,它所声明的变量就“绑定”(binding
)这个区域,不再受外部的影响,在代码块内,使用let
命令声明变量之前,该变量都是不可用的。 - 不允许重复声明
(三)const
- 声明的�全局变量不会挂在顶层对象下面
const
声明之后必须马上赋值,否则会报错const
简单类型一旦声明就不能再更改,�复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改。const
一旦声明变量,就必须立即初始化,不能留到以后赋值。const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。
四、参考文章
- 【阮一峰-ECMAScript6入门】(http://es6.ruanyifeng.com/#docs/let)
针对区别做了总结,实现原理属于推测,求真相。
2019.05.29面试题: var、let、const 的区别及实现原理?
变量生命周期:声明(作用域注册一个变量)、初始化(分配内存,初始化为undefined)、赋值
- var:遇到有var的作用域,在任何语句执行前都已经完成了声明和初始化,也就是变量提升而且拿到undefined的原因由来
- function: 声明、初始化、赋值一开始就全部完成,所以函数的变量提升优先级更高
- let:解析器进入一个块级作用域,发现let关键字,变量只是先完成声明,并没有到初始化那一步。此时如果在此作用域提前访问,则报错xx is not defined,这就是暂时性死区的由来。等到解析到有let那一行的时候,才会进入初始化阶段。如果let的那一行是赋值操作,则初始化和赋值同时进行
- const、class都是同let一样的道理
比如解析如下代码步骤:
{
// 没用的第一行
// 没用的第二行
console.log(a) // 如果此时访问a报错 a is not defined
let a = 1
}
步骤:
- 发现作用域有let a,先注册个a,仅仅注册
- 没用的第一行
- 没用的第二行
- a is not defined,暂时性死区的表现
- 假设前面那行不报错,a初始化为undefined
- a赋值为1
对比于var,let、const只是解耦了声明和初始化的过程,var是在任何语句执行前都已经完成了声明和初始化,let、const仅仅是在任何语句执行前只完成了声明
搬运:
变量被重新覆盖之后,之前的内存地址和内存空间,会被垃圾回收机制回收的吧!
var的话会直接在栈内存里预分配内存空间,然后等到实际语句执行的时候,再存储对应的变量,如果传的是引用类型,那么会在堆内存里开辟一个内存空间存储实际内容,栈内存会存储一个指向堆内存的指针
let的话,是不会在栈内存里预分配内存空间,而且在栈内存分配变量时,做一个检查,如果已经有相同变量名存在就会报错
const的话,也不会预分配内存空间,在栈内存分配变量时也会做同样的检查。不过const存储的变量是不可修改的,对于基本类型来说你无法修改定义的值,对于引用类型来说你无法修改栈内存里分配的指针,但是你可以修改指针指向的对象里面的属性
let const 和var三者都会存在变量提升
- let只是创建过程提升,初始化过程并没有提升,所以会产生暂时性死区。
- var的创建和初始化过程都提升了,所以在赋值前访问会得到undefined
- function 的创建、初始化、赋值都被提升了
变量生命周期:声明(作用域注册一个变量)、初始化(分配内存,初始化为undefined)、赋值
- var:遇到有var的作用域,在任何语句执行前都已经完成了声明和初始化,也就是变量提升而且拿到undefined的原因由来
- function: 声明、初始化、赋值一开始就全部完成,所以函数的变量提升优先级更高
- let:解析器进入一个块级作用域,发现let关键字,变量只是先完成声明,并没有到初始化那一步。此时如果在此作用域提前访问,则报错xx is not defined,这就是暂时性死区的由来。等到解析到有let那一行的时候,才会进入初始化阶段。如果let的那一行是赋值操作,则初始化和赋值同时进行
- const、class都是同let一样的道理
比如解析如下代码步骤:
{ //没用的第一行 //没用的第二行 console . log (a) //如果此时访问a报错a is not defined let a = 1 }步骤:
- 发现作用域有let a,先注册个a,仅仅注册
- 没用的第一行
- 没用的第二行
- a is not defined,暂时性死区的表现
- 假设前面那行不报错,a初始化为undefined
- a赋值为1
对比于var,let、const只是解耦了声明和初始化的过程,var是在任何语句执行前都已经完成了声明和初始化,let、const仅仅是在任何语句执行前只完成了声明
咋回事咧?这么明显的错误~
// 暂时性死区了兄弟 Cannot access 'a' before initialization
console.log(a)
let a = 1;
// is not defined 是未定义
console.log(temp); // 此时temp还未定义。
转载:
var和let的区别,面试老生常谈的问题,大多数人回答可能就是作用域和变量提升这两点不同,少有人能够知道内在原理,这样的回答面试官会满意吗?(手动滑稽)
我们就从声明过程,内存分配,和变量提升这三点来看这三者之间的区别。
一.声明过程
var:遇到有var的作用域,在任何语句执行前都已经完成了声明和初始化,也就是变量提升而且拿到undefined的原因由来
function: 声明、初始化、赋值一开始就全部完成,所以函数的变量提升优先级更高
let:解析器进入一个块级作用域,发现let关键字,变量只是先完成声明,并没有到初始化那一步。此时如果在此作用域提前访问,则报错xx is not defined,这就是暂时性死区的由来。等到解析到有let那一行的时候,才会进入初始化阶段。如果let的那一行是赋值操作,则初始化和赋值同时进行
const、class都是同let一样的道理
比如解析如下代码步骤:
{
// 没用的第一行
// 没用的第二行
console.log(a) // 如果此时访问a报错 a is not defined
let a = 1
}
步骤:
发现作用域有let a,先注册个a,仅仅注册
没用的第一行
没用的第二行
a is not defined,暂时性死区的表现
假设前面那行不报错,a初始化为undefined
a赋值为1
对比于var,let、const只是解耦了声明和初始化的过程,var是在任何语句执行前都已经完成了声明和初始化,let、const仅仅是在任何语句执行前只完成了声明。
二.内存分配
var,会直接在栈内存里预分配内存空间,然后等到实际语句执行的时候,再存储对应的变量,如果传的是引用类型,那么会在堆内存里开辟一个内存空间存储实际内容,栈内存会存储一个指向堆内存的指针
let,是不会在栈内存里预分配内存空间,而且在栈内存分配变量时,做一个检查,如果已经有相同变量名存在就会报错
const,也不会预分配内存空间,在栈内存分配变量时也会做同样的检查。不过const存储的变量是不可修改的,对于基本类型来说你无法修改定义的值,对于引用类型来说你无法修改栈内存里分配的指针,但是你可以修改指针指向的对象里面的属性
三.变量提升
let const 和var三者其实会存在变量提升
let只是创建过程提升,初始化过程并没有提升,所以会产生暂时性死区。
var的创建和初始化过程都提升了,所以在赋值前访问会得到undefined
function 的创建、初始化、赋值都被提升了
————————————————
版权声明:本文为CSDN博主「微 光」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/Web_J/article/details/99591116
怎么感觉大家回答的都是表层的东西...栈和堆这些都知道哪个可改变哪个不可改变...问的应该是怎么做到不可改变的吧?
怎么感觉大家回答的都是表层的东西...栈和堆这些都知道哪个可改变哪个不可改变...问的应该是怎么做到不可改变的吧?
其实题目想要考察的就是 javascript 表面语义的东西。
具体不同类型的声明产生的变量在内存中究竟如何存储,那要看 javascript engine 内部是如何实现的。这些实现细节对于上层前端开发者来讲了解即可。如果你真的想要了解究竟是分配在栈上还是堆上,去看 V8,spiderMonkey这些引擎的代码(个人觉得如果不做引擎这块,纯属看个人兴趣去深入了)。这些引擎内部是用 C 或 C++ 这种更加方面操作内存的语言写的。也就知道实现原理是什么了。
怎么感觉大家回答的都是表层的东西...栈和堆这些都知道哪个可改变哪个不可改变...问的应该是怎么做到不可改变的吧?
其实题目想要考察的就是 javascript 表面语义的东西。
具体不同类型的声明产生的变量在内存中究竟如何存储,那要看 javascript engine 内部是如何实现的。这些实现细节对于上层前端开发者来讲了解即可。如果你真的想要了解究竟是分配在栈上还是堆上,去看 V8,spiderMonkey这些引擎的代码(个人觉得如果不做引擎这块,纯属看个人兴趣去深入了)。这些引擎内部是用 C 或 C++ 这种更加方面操作内存的语言写的。也就知道实现原理是什么了。
明白了 十分感谢解惑 因为总觉得这样回答似乎很简单 看来是我想复杂了
怎么感觉大家回答的都是表层的东西...栈和堆这些都知道哪个可改变哪个不可改变...问的应该是怎么做到不可改变的吧?
其实题目想要考察的就是 javascript 表面语义的东西。
具体不同类型的声明产生的变量在内存中究竟如何存储,那要看 javascript engine 内部是如何实现的。这些实现细节对于上层前端开发者来讲了解即可。如果你真的想要了解究竟是分配在栈上还是堆上,去看 V8,spiderMonkey这些引擎的代码(个人觉得如果不做引擎这块,纯属看个人兴趣去深入了)。这些引擎内部是用 C 或 C++ 这种更加方面操作内存的语言写的。也就知道实现原理是什么了。明白了 十分感谢解惑 因为总觉得这样回答似乎很简单 看来是我想复杂了
并且可能非常多前端开发者对于计算基础的知识并不牢靠。比如什么是栈和堆?操作系统的栈和堆概念和编程语言的栈、堆概念一致吗(一般我们了解程序的堆栈概念来自于 C 语言)?和传统的数据结构中的栈结构和二叉堆又有什么关联呢?等等这些概念。
了解了这些概念,是不是有必要去研究下 GC(garbage collection)?
前端可能更加偏向于体验一点。
function foo() {
var a = 1;
let b = 2;
}
我的理解:
代码执行分为两个阶段,create phase 和 execute phase,也就是代码解析和代码执行;
在create phase 阶段,其实var 和 let 定义的变量都已经初始化了:
var定义的变量被赋值为undefined
let定义的变量被赋值为uninitialized
var之所以被变量提升是因为已经赋有效值(undefined),但是引用uninitialized变量编译器会报错。
等到execute phase阶段,a赋值为1,b赋值为2,就没什么可说的了。
另外块级作用域也是因为通过不同方式声明的变量被赋值到不同的EnvironmentRecord,LexicalEnvironment和VariableEnvironment的区别,在一个方法内部,每一个块级代码都会新生成一个LexicalEnvironment,并保留上一个LexicalEnvironment的引用,但是一个方法内只有一个VariableEnvironment。
var 存在变量提升, let const 声明的变量不会
- var 声明变量会挂在window, let const 不会
- let, const 声明形成 作用域
- 同一作用域下 let const 不能声明 同名变量, 而var 可以
- 暂存死区
- const 声明后不得修改
- var会直接在栈里分配一个内存空间,等实际执行到语句时,再保存变量。如果遇到引用类型的变量,会去堆里开辟一个空间来保存对象,然后在栈里存储指向对象的指针。
- let不会直接去栈里分配内存空间,而是做一个预检查,如果有同名变量就会报错。
- const和let一样在分配内容空间之前会做检查。
- 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。
- 通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。
- 在函数的作用域内部,通过 let 声明的变量并没有被存放到词法环境中。
在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出
进入块级作用域不会有编译过程,只不过通过let或者const声明的变量会在进入块级作用域的时被创建,但是在该变量没有赋值之前,引用该变量JavaScript引擎会抛出错误---这就是“暂时性死区”。
const 实现
function _const(key, value) {
const desc = {
value,
writable: false
}
Object.defineProperty(window, key, desc)
}
var let const 都存在变量提升,而且都初始化为undefined,这个可以在浏览器控制台debugger Scope的Local中看到三者都是undefined。
js执行的时候会先编译 对于该作用域下的var变量会初始化为undefined ,var声明的变量存在于变量活动对象中,let const声明的变量也会初始化为undefined,但是是在词法环境中,而js引擎对词法环境中未初始化的let const声明的变量做了访问限制 因此才会存在“暂时性死区”。
这块内容标准中有写https://262.ecma-international.org/6.0/#sec-let-and-const-declarations
13.3.1和13.3.2
function test () {
debugger // 这里在浏览器的scope local中可以查看到三者是undefined
var a = 'a'
let b = 'b'
const c = 'c'
}
test()```
- var声明的变量只会存在于全局/局部作用域,如果声明在代码块(if/else/switch)中,会被提升出去,
比如模块或者一个方法中声明的var a=1,如果声明在模块中,则会默认挂载到window上;如果声明在方法中,则在方法内部任何地方都可以访问;如果声明在代码块里,则会提升到上一级作用域;
- let/const声明的变量,会被保存在局部作用域中
声明在模块中,也不会挂载到window上,而是形成一个单独的作用域;如果声明在一个代码块中,则在代码块以外无法访问,不会被提升到父级作用域(与var是不同的),但是let/const声明的变量存在暂时性死区
var,已废弃
- 函数作用域 & 全局作用域
- 存在声明提升
let/const,更现代
- 块级作用域
- 不能重复声明
- 在声明后才可用,存在死区
var
存在变量提升
可以重复赋值
不存在块级作用域
let
存在 TDZ,不存在变量提升,get 变量前必须声明
不可以重复声明
存在于块级作用域
const
存在 TDZ,不存在变量提升,get 变量前必须声明初始化
不可以重复声明
不可以重复赋值 (但对于引用对象的属性还是可以修改)
存在于块级作用域
var:
- 变量提升
- 重复声明
- 全局作用域绑定(var a = 1; 相当于 window.a = 1;)
let/const:
- 不再拥有 var 的缺陷特性
- 存在 TDZ 临时死区
- 块级作用域绑定
let:
- 变量声明
const:
- 常量声明
- 优先声明最佳实践(无需操作数据的情况下,一般采用 const 声明)