- 在ES5中我们声明变量都是使用的var(variable)关键字,从ES6开始新增了两个关键字可以声明变量:let、const
- let、const在其他编程语言中都是有的,所以也并不是新鲜的关键字
- 但是let、const确确实实给JavaScript带来一些不一样的东西
- 从使用角度来说,只是在原有基础上换一个名字而已,使用的位置和方式是一样的
var name = 'zs'
let name = 'zs'
const name = 'zs'
- let和const的出现,其实是顺延了ES6所想表达的思想,在这里主要表达为消除二义性、提高可读性,以及弥补ES5及之前的缺陷问题
- 从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量
- 而const关键字是
constant单词
的缩写,表示常量、衡量
的意思,意味着保存的数据一旦被赋值,就不能被修改,但是如果赋值的是引用类型
,那么可以通过引用找到对应的对象,修改对象的内容- let、const与var最大的共同区别在于前两者不允许重复声明变量
- 这一点其实很重要,使用var可以重复声明变量其实是一个早期的缺陷,这并不能算作特性
- 每一个变量都有属于自己的作用,我们不会莫名其妙声明一个意义不明的变量出来。而声明变量和修改变量,是两个完全不同的操作,也代表了两种不同的思路走向
- 当进行变量声明(let)时,会确定该变量的作用性质。一旦在不注意的情况下声明相同变量,两个变量可能表达的含义很可能完全不同:
- 例如我声明一个Names变量,第一次声明的同时,我进行赋值
['xiaoyu','coderwhy']
,那Names变量所表达的含义是所有具体名字
,但在后面我又声明了一遍Names,我赋值为2,这次的内容变为了数字,表达一共有几个名字
。同样的变量名导致了不同的含义
- 进行单纯的赋值操作,只能够说明该变量的作用性质已经决定了,我们改变内容并不会影响其使用的本质
- 在开发的时候,未必不会出现这种情况,并且在越大型的合作项目中,越容易出现该问题,因为不清楚该变量名是否已经被同事所使用,会造成不便与排查难度提升,提早的发现有助于排除隐患
- 因此声明和赋值所具备的含义是可以进行拆分使用的,只是大多数情况我们会合并使用
- 但有些情况,也会提前进行声明占位,确定
变量/常量名
的性质,等到后续在进行使用
let name
//赋值
name = 'xiaoyu'
// 重复声明报错
let name
内存地址 如0xb00
将无法改变,这个表达形式为引用无法改变,但const常量并不对堆内容进行封锁,导致位于堆位置的内容是可以改变的const person = {name:'xiaoyu'}
person.name = 'coderwhy'
console.log(person);//{ name: 'coderwhy' }
//其他复杂数据类型例如Set Map 后续会学习到
const set = new Set([1, 2, 3]);
set.add(4); // 可以:添加新元素
set.delete(1); // 可以:删除元素
console.log(set); // Set(3) { 2, 3, 4 }
1.let与const不允许声明变量
2.const只能锁住简单数据类型,而不能锁住复杂数据类型
- 新的声明方式在替代var的时候,同时也改变了其内在声明逻辑,这和语法糖是完全不同的
- var在进行声明的时候,作用域会进行提升到全局,而赋值不会。导致var一旦声明就能够在所有地方进行使用,哪怕在声明之前使用也可以
- 在这种情况下,声明之前进行调用就会产生默认结果undefined,直到运行到赋值的位置,变量值才发生对应的改变
- 这种方式其实是不合理的,因为在变量不存在的情况(未声明),是直接报错
- 报错通常是:ReferenceError: 变量 is not defined,没有引用到对应的变量名而报错
- 而var声明变量之前,变量其实因为作用域提升因素,导致可访问,容易使人误会该变量已经可以使用,但使用的位置如果在赋值之前,就会因为调用JS引擎赋予变量的默认值undefined而报错
- 因此造成错误的原因会不明确,不清晰,从而造成误导、不必要的排查
console.log(person.foo());//undefined.foo() 调用失败 TypeError: Cannot read properties of undefined (reading 'foo')
var person = {
foo(){
console.log('coderwhy');
}
}
console.log(person.foo());//ReferenceError: Cannot access 'person' before initialization
//let与const的效果在这里一样
let person = {
foo(){
console.log('coderwhy');
}
}
图20-1 ECMA262 对 let 和 const 的描述
- 这要如何理解?我们需要抽离一下关键信息:`创建`与`访问` - let/const被划分为两个阶段,从该内容所暴露出来的信息,可以确定`创建阶段`是不变的 - 关键的改变在于`访问阶段`:词法绑定被求值之前(赋值过程之前),无法访问 - 这么看来,在执行上下文的词法环境创建出来的时候,变量事实上已经被创建了,只是这个变量是不能被访问的 - 那么变量已经有了,但是不能被访问,是不是一种作用域的提升呢? - 事实上维基百科并没有对作用域提升有严格的概念解释,那么我们自己从字面量上理解 - 作用域提升:在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升 - 在这里,它虽然被创建出来了,但是不能被访问,我认为不能称之为作用域提升 - 所以我的观点是let、const没有进行作用域提升,但是会在执行上下文创建阶段被创建出来
var声明的变量,具有作用域提升的特性,let与const声明变量没有作用域提升
但这并不意味着let与const声明的变量要在代码执行阶段才会被创建,
事实上这些变量在其词法环境被实例化的时候就已经被创建了,但是是不可以访问它们的,直到词法绑定被求值
var、let、const对比总结
特性/声明类型 |
|
|
|
存储位置 |
变量环境 |
词法环境的块级作用域 |
词法环境的块级作用域 |
作用域 |
函数作用域或全局作用域 |
块级作用域 |
块级作用域 |
提升行为 |
变量提升,初始化为 |
无提升,暂时性死区直到声明处 |
无提升,暂时性死区直到声明并初始化 |
- 当 JavaScript 代码执行时:
- 解析器会先通过词法分析过程识别所有的变量和函数声明
- 对于
var
和函数声明,它们会被添加到变量环境中,并在函数执行之前完成初始化(函数完整地、变量为undefined
)- 对于
let
和const
,它们被添加到词法环境中,对应的作用域是它们所在的块级作用域。直到执行到它们的具体声明语句之前,它们都不可用
- 这种机制确保
let
和const
提供了比var
更严格和更清晰的作用域管理,减少因变量提前使用导致的潜在问题var声明的变量会绑定在windows的全局变量中
- 变量环境 (VE):主要用于存储由
var
声明的变量和函数声明。而let与const则有自己的块级作用域之中
- 词法环境 (LE):用于存储由
let
和const
声明的变量,以及包含块级作用域的其他信息
//块级作用域(ES5中不存在)
{
var foo = 'coderwhy'
}
console.log(foo);//可以访问到
块级作用域
是和函数作用域
具备类似能力的,或者说后者就是前者的一种表达形式1、内层能够链式访问外层
2、外层无法访问内存
{
let foo = 'coderwhy'
}
console.log(foo);//无法访问
{
function foo(){
console.log('zs');
}
}
foo()//浏览器可以访问 node环境不可访问
{
let foo = function(){
console.log('zs');
}
}
foo()//不管是浏览器还是node环境,都无法访问
{}
所结合的结构,比如if判断语句、switch循环语句、for循环语句,其内部都是具备块级作用域效果的if (true) {
let message = "Hello, World!";
console.log(message); // 输出:Hello, World!
}
// console.log(message); // ReferenceError: message is not defined
for (let i = 0; i < 3; i++) {
console.log(i); // 输出:0, 1, 2
}
// console.log(i); // ReferenceError: i is not defined
switch ('coderwhy') {
case 'coderwhy':
let xiaoyu = "Switch Case";
console.log(xiaoyu); // 输出:Switch Case
break;
default:
// 不可访问 'xiaoyu' 变量
break;
}
// console.log(xiaoyu); // ReferenceError: example is not defined
const names = ['coderwhy','xiaoyu','JS高级']
for (let i = 0; i < names.length; i++) {
console.log(names[i]);
}
//产生如下效果
// {
// let i = 0
// console.log(names[i])
// }
// {
// let i = 1
// console.log(names[i])
// }
// {
// let i = 2
// console.log(names[i])
// }
for of
遍历,该遍历的使用目标为可迭代对象
,这一点我们后面还会进行说明for of
是直接遍历集合的元素而不是索引的方法,所以可以不使用let声明一个变量来记录索引,对于不想要改变的数据此时就可以使用const来进行声明,直接访问元素最直观的就是简化迭代const names = ['coderwhy','xiaoyu','JS高级']
//ES6新增遍历数组(对象)
for(const item of names) {
console.log(item);
}
使用let,const声明的变量在{}中产生了块级作用域,外部是无法访问到的
暂时性死区(Temporal Dead Zone,TDZ)是 JavaScript 中与 let
和 const
声明相关的一个行为特征。这个概念主要涉及到变量的生命周期,在变量声明
和初始化
之间存在一个时间段,在这段时间里,变量虽然已经被声明,但还不能被访问或使用
function foo() {
// console.log(this);//指向global
console.log(1111);
console.log(a); //Cannot access 'a' before initialization
//在这段声明代码但没有初始化的阶段我们叫做暂时性死区
let a = 'zs';
console.log(a);
}
foo();
在我们给a赋值为'zs'之前我们是无法访问到a的这段区域我们称之为,暂时性死区