Symbol数据类型的延伸之(const关键字的补充)
之前上一篇关于const 关键字的延伸,自己回过头看看还是有描述得不够充分的地方,所以今天又翻阅了大量的文献,对之前的文章进行一个补充。 相信我,坚持、耐心看完,希望你会有不一样的收获。
常量,我们在JavaScript当中经常会用到,通常使用const 声明一个常量。使用const 进行常量的定义一般定义为大写,并且一定要赋予初始值,定义好之后,不能被修改,也不能被重复声明。
所以,我们可以说,const 声明的常量 也具备有相当的唯一性的,是值得我们拿出来跟Symbol 并排讨论的。
一、关于块级作用域的定义
我们知道,使用var 关键字声明一个变量,存在着‘‘变量提升 ’’这种隐晦的问题,该关键字作用于全局作用域,不受块级作用域影响(除非闭包或IIEF)。
那么什么是块级作用域 呢?
延伸:块级作用域 为了加强对变量生命周期的控制,ECMAScript 6 引入了块级作用域。而块级作用域存在于:
因此,为了更好的理解块级作用域,我们需要详细展开学习一下变量的运行环境 ,先举一个例子切入:
<script>
function changeColor() {
function swapColor() {
}
swapColor()
}
changeColor()
</script>
我们结合例子一步步渐进的分析:
- 了解变量的运行或执行环境(执行上下文)
JavaScript的运行环境也叫执行上下文 ,因此在一个JavaScript程序中,势必会出现多个执行上下文,其中主要包括以下三类:
- 全局环境(全局执行上下文):代码运行起来之后会先进入全局环境;
- 函数环境(函数执行上下文):当函数被调用时,会进入当前函数中执行代码;
eval 环境:不建议使用,这里不再赘述。
JavaScript引擎会以栈储存 的方式来处理执行上下文,一般是指函数调用栈,块级作用域 就存在这里面。我们知道栈底永远都是全局执行上下文,栈顶则永远都是当前正在执行的上下文。即永远遵循后入先出 的原则。
关于执行上下文对应的进出栈流程 ,结合图片看可能会更形象一些:
- 栈流程的第一步是全局上下文入栈;
- 全局上下文入栈之后,JavaScript执行到
changeColor 函数调用栈,此函数一旦调用,就会创建自己的执行上下文,我们称之为函数执行上下文 ,此时changeColor EC 入栈; - 在新的执行上下文中,开始执行内部可执行代码,直到遇到
swapColor 调用栈,开始开辟新的函数执行上下文,此时swapColor EC 入栈; - 等到当前的执行上下文中的可执行代码执行完毕之后,发现不再又其他执行上下文生成的情况下,此上下文会自动从栈中弹出;
swapColor EC 函数执行上下文弹出后,JavaScript会继续执行 changeColor EC 函数执行上下文的可执行代码。执行完毕之后,发现没有其他执行上下文生成,也就自动从栈中弹出;- 最后执行栈值剩下全局上下文,若浏览器不关闭,全局上下文会一直存在,直到浏览器窗口关闭,全局上下文才会最终出栈。
姑且认为上面的出入栈流程是宏观的执行上下文,接下来重点学习微观的层面,即执行上下文之中的生命周期。
- 执行上下文的生命周期(包含变量的声明周期)
我们通过上面分析的出入栈流程知道,当全局执行上下文或某个执行上下文之中的可执行代码 执行到函数(或块级)调用栈时,被调用的这个函数就会开辟一个新的执行上下文,我们称之为函数执行上下文 。殊不知这块函数内(或块内) 的区域也就是我们耳熟能详的块级作用域 !
同样的,我们通过图片来了解执行上下文的生命周期 :
函数执行上下文的生命周期分为三part,分别是:
创建阶段 —— 此阶段执行上下文会分别创建变量对象、确认作用域链、以及确定this 指向;执行阶段 —— 执行代码,这个时候会完成变量赋值、函数引用、以及执行其他可执行代码等工作;释放阶段 —— 退出执行上下文并销毁。
没想到吧?我们现在知道,在这个函数(或块级)执行上下文里面JavaScript做了很多工作,其中就有变量的生成 和变量的赋值 ,这个过程就是变量的生命周期 。可以说,在执行上下文的生命周期里面,包含着变量的生命周期!
到这里,其实我们的答案已经出来了。我们老生常谈的变量提升 、作用域链 、TDZ 、和this指向 等头疼的问题产生的源头,其实就包含在函数(块级)执行上下文的生命周期之中!
我们回扣最开始的话:为了加强对变量声明周期的控制,ES6 引入了块级作用域。然而所谓的块级作用域 ,实际上就可以理解为是由函数区域内的执行上下文 ({} 区域内的执行上下文同理)产生的!两者唯一的区别在于,作用域指的是这块执行代码形成的区域;而执行上下文是真实在JavaScript引擎中执行的代码片段。
二、临时性死区的补充
上一篇文章对于TDZ 的解释,其实是个人的一部分浅显的理解,望大家见谅。但想要充分认识TDZ 的性质,还得要继续剖析我们上面分析到的:变量的生命周期 ~
- 创建变量对象(有自己的生命周期)—— 对标声明和初始化阶段
JavaScript声明的所有变量都保存在变量对象之中,变量对象的创建依次经历以下步骤:
- 首先获得函数的参数变量及其值(在JavaScript里面函数是
一等公民 ,任何执行上下文都会优先收集函数内的参数及其对应的值,即arguments ); - 依次获取当前上下文中所有的函数声明,在变量对象中会以函数名建立一个属性,属性值指向该函数所在的
堆内存地址 ; - 依次获取当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性(开辟栈内存地址)。如果是
var 关键字声明,则属性值会初始化为undefined 。
- 变量的数据存储(一张图足以说明一切)—— 对标赋值阶段
变量对象创建完成后,接下来就是对数据的存储,基础数据类型 往往会保存在栈内存 之中(特殊情况除外),而引用数据类型 的值则是保存在堆内存 之中。
我们知道,在JavaScript里面,是不允许我们直接访问堆内存 空间中的数据的。所以当我们在操作对象时实际上实在操作对象的引用 ,而不是实际的对象。
因此,引用数据类型都是按引用访问的,这里的引用可以理解为保存在栈内存 中的一个地址,该地址与堆内存 中的对象相关联。
- 变量的存储空间(有自己的生命周期)
变量内存空间的使用过程,也有自己的执行流程 :
- 分配内存阶段(变量的定义)
- 使用分配到的内存(可执行的变量语句)
- 不需要时释放内存 (
垃圾回收 )
举个例子就明白了:
var a = 20;
console.log(a + 1);
a = null;
经过层层嵌套循环渐进的剖析挖掘,我们发现变量内存空间的生命周期也包含着垃圾回收机制 。
- 变量的垃圾回收
垃圾回收机制的原理就是:找出那些不再继续使用的变量 ,垃圾收集器会按照固定的时间间隔,周期性的释放 其占用的内存。
最常用的垃圾收集方式是标记清除算法 。主要依靠 “引用” 的概念,当一块内存空间中的数据能够被访问时,垃圾回收器就会认为 “该数据能够被获得”,不能够被获得的数据,就会被打上标记,并回收内存空间,这种方式叫做:标记 --- 清除算法 。
搞清楚变量的生命周期之后,现在我们可以充分的分析临时性死区的原因了(dog狗头)。
根据上面分析的变量的生命周期 的本质,我们大概可以把它囊括成四个阶段:
- 声明阶段(创建变量对象、依次获取上下文中的变量声明(声明语句)、函数声明直接开辟内存并执行初始化);
- 初始化阶段(开辟栈或堆内存,
var 和let 被初始化为undefined ,const 并未执行初始化); - 赋值阶段(将变量值赋值到对应的栈或堆地址)
- 释放阶段(垃圾回收)
接着我们从造成TDZ 的源头入手,分别为var 、let 和const 声明变量的JavaScript语句执行一次变量生命周期:
var 声明变量的生命周期
由于var 将声明阶段和初始化阶段耦合 到了一起,导致执行上下文一开始就将变量初始化成了undefined ,因此造成了变量提升 ,故而该变量不存在有TDZ 。 引入一个例子作为参考:
function fn() {
console.log(a);
var a = 1;
}
console.log(a)
{
var a = 1;
}
let 声明变量的生命周期
与var 相反,这种关键字声明的变量是遵循变量的生命周期的。声明阶段和初始化阶段解耦 ,所以该变量只有在声明阶段 被提升至作用域顶部收集到变量对象 之中,而初始化阶段 并没有被提升。因此在声明阶段和初始化阶段之间就产生了TDZ ,俗称临时性死区 。此阶段不可访问该变量,否则就会抛出异常。
而初始化阶段的变量则被初始化为undefined 。
最重要的是受块级作用域 的影响,let 只在代码块内声明变量,从而避免全局污染。
引入一个例子作为参考:
console.log(a);
let a = 1;
{
let a = 1;
}
console.log(a);
let a;
let a = 1;
let a;
console.log(a);
const 声明常量的生命周期
这种关键字声明的变量跟let 一样,也是遵循变量的生命周期的,都是存在声明提升 。唯一的不同是,const 是用来声明常量 的,通常只声明一个只读引用的数据类型,且它的初始化阶段和赋值阶段要同时一起完成,否则就会报错。
引入例子参考举证:
console.log(a);
const a = 1;
{
const a = 1;
}
console.log(a);
const a = 1;
a = 2;
const a;
使用const 声明变量时,实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。 但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。 所以一般声明复合类型的变量一般都用const 来声明,声明基本数据类型的变量一般都用let 声明。
不管是var、let、还是const声明的变量,在变量使用完毕后,最终都会随着执行上下文的出栈、浏览器的关闭、或者手动释放内存的环节进行垃圾回收。由于JavaScript中自动垃圾回收机制的存在,使我们往往在开发时忽略了内存使用的问题,但这个是所有变量都要经历的过程。
三、for循环的本质
我们来看一个例子:
var funcs = [];
for (const i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0]();
按照我们之前的理解,for 循环的本质是,在每一个循环的过程中开辟一个隐藏的执行上下文 ,形成对应每一个循环的块级作用域 。循环执行完成之后,该执行上下文也就被释放。按道理说这里不应该报错呀?各个作用域互不相干?
其实本质上不是这样子的,for循环的底层原理对var、let和const有着不同的处理方式。
首先我们要搞清楚,for 循环由四个部分组成:
- 声明变量
i ; - 判断条件;
- 执行可执行语句;
- 自增变量
i 。
通过我们文中之前分析的截图,这里需要强调分析for 循环的本质:
for () —— 遇见for 循环了,创建for 执行上下文入栈,形成块级作用域;const i = 0; —— 每个迭代循环开始,都会收集for 循环的参数变量及其值,创建变量对象(声明并赋值变量);i < 10; —— 判断条件,如果为true 则执行块内的可执行代码;{} —— 创建{} 的执行上下文入栈,经历执行上下文生命周期,创建新的变量对象,收集{} 的函数以及变量,经历变量的生命周期,等可执行代码执行完,释放当前的块级执行上下文;i++ —— 执行自增,销毁当前for 执行上下文,继续下一个循环;- 创建新的
for 执行上下文入栈,形成块级作用域;
debugger
const i = 1; —— 每个迭代循环开始,收集for 循环的参数变量及其值,创建变量对象(声明阶段和初始化阶段耦合,声明变量i ,并把上一个i 做为它的初始化值,然后我们再进行赋值阶段的时候,就会报错了。如果使用let 就不会有这种问题);
分析下来原理就是,我们知道,每种声明变量的关键字都有它自己独特的生命周期。但for循环里面,变量i的生命周期不会遵循关键字形成的生命周期,永远都是,声明、初始化、赋值三个阶段。并且在初始化阶段,i会把上一个迭代循环中的i作为初始值。但由于const 有只读引用的属性,所以新的const 常量无法被重新赋值。
值得一提的是,在for in 循环中,使用const 也没问题,因为它的每次迭代循环所新声明的i ,是完全遵照关键字声明的生命周期的。
四、总结
每次写完文章之后,我都会从头到尾的看好几遍,逻辑是否通顺?论点是否可信?论据是否充足?所以看完之后,也会总结文中的知识点纳入到自己的知识网之中:
JavaScript 运行环境也叫执行上下文,JavaScript引擎 通过栈储存的方式处理执行上下文;- 全局执行上下文永远在最底端,最顶端永远都是当前正在执行的执行上下文,遵循后入先出原则;
- 执行上下文有生命周期,分为创建、执行和释放;
块级作用域 就是当前执行上下文所形成的代码区域;- 执行上下文的生命周期包含:变量的生命周期、确定作用域链、确定
this 指向等等; - 变量的生命周期分为:创建变量对象及其生命周期、变量储存到内存的原则、变量的使用及其使用时内存的生命周期、变量的释放(标记清除法,垃圾回收机制);
- 关键字不同,声明的变量其生命周期也不同。基本分为:声明阶段、初始化阶段、赋值阶段、应用阶段和销毁阶段;
var 将声明和初始化耦合,存在变量提升,不存在TDZ ;let 没有耦合声明和初始化,存在声明提升,不存在初始化提升,存在TDZ ;const 没有耦合声明和初始化,存在声明提升,不存在初始化提升,存在TDZ ,但初始化和赋值必须同时执行;for 非常坑,里面的i 不遵循关键字自己的生命周期,只遵循基本的变量生命周期,并且每个初始化阶段都会赋值前一个迭代循环中的i 做为初始值;for in 没有那么多花里胡哨,在每个初始化阶段也不会赋值前一个迭代循环中的i做为初始值;
通篇写完,不知道是否感觉到自己的废话多了,这点需要大家为我明鉴,希望我有不对的或者多余的地方,大家能够为我指出。非常感谢阅读到这里的你,愿你的未来一篇光明。
- 参考文章:
- https://juejin.cn/post/6844903608316592141 —— 冴羽大神的掘金
- https://zhuanlan.zhihu.com/p/158817336 —— 知乎社区
|