IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> JavaScript —— Symbol数据类型的延伸之(const关键字的补充) -> 正文阅读

[JavaScript知识库]JavaScript —— Symbol数据类型的延伸之(const关键字的补充)

Symbol数据类型的延伸之(const关键字的补充)

之前上一篇关于const关键字的延伸,自己回过头看看还是有描述得不够充分的地方,所以今天又翻阅了大量的文献,对之前的文章进行一个补充。
相信我,坚持、耐心看完,希望你会有不一样的收获。

常量,我们在JavaScript当中经常会用到,通常使用const声明一个常量。使用const进行常量的定义一般定义为大写,并且一定要赋予初始值,定义好之后,不能被修改,也不能被重复声明。

所以,我们可以说,const声明的常量也具备有相当的唯一性的,是值得我们拿出来跟Symbol并排讨论的。

一、关于块级作用域的定义

我们知道,使用var关键字声明一个变量,存在着‘‘变量提升’’这种隐晦的问题,该关键字作用于全局作用域,不受块级作用域影响(除非闭包或IIEF)。

那么什么是块级作用域呢?

延伸:块级作用域
为了加强对变量生命周期的控制,ECMAScript 6 引入了块级作用域。而块级作用域存在于:

  • 函数内部
  • 块之中(字符{}之间的区域)

因此,为了更好的理解块级作用域,我们需要详细展开学习一下变量的运行环境,先举一个例子切入:

<script>
function changeColor() {
  // ...
  function swapColor() {
	// ...
  }
  swapColor()
}

changeColor()
</script>

我们结合例子一步步渐进的分析:

  1. 了解变量的运行或执行环境(执行上下文)

JavaScript的运行环境也叫执行上下文,因此在一个JavaScript程序中,势必会出现多个执行上下文,其中主要包括以下三类:

  • 全局环境(全局执行上下文):代码运行起来之后会先进入全局环境;
  • 函数环境(函数执行上下文):当函数被调用时,会进入当前函数中执行代码;
  • eval环境:不建议使用,这里不再赘述。

JavaScript引擎会以栈储存的方式来处理执行上下文,一般是指函数调用栈,块级作用域就存在这里面。我们知道栈底永远都是全局执行上下文,栈顶则永远都是当前正在执行的上下文。即永远遵循后入先出的原则。

关于执行上下文对应的进出栈流程,结合图片看可能会更形象一些:

执行上下文,图片来自互联网

  • 栈流程的第一步是全局上下文入栈;
  • 全局上下文入栈之后,JavaScript执行到changeColor函数调用栈,此函数一旦调用,就会创建自己的执行上下文,我们称之为函数执行上下文,此时changeColor EC入栈;
  • 在新的执行上下文中,开始执行内部可执行代码,直到遇到swapColor调用栈,开始开辟新的函数执行上下文,此时swapColor EC入栈;
  • 等到当前的执行上下文中的可执行代码执行完毕之后,发现不再又其他执行上下文生成的情况下,此上下文会自动从栈中弹出;
  • swapColor EC函数执行上下文弹出后,JavaScript会继续执行 changeColor EC函数执行上下文的可执行代码。执行完毕之后,发现没有其他执行上下文生成,也就自动从栈中弹出;
  • 最后执行栈值剩下全局上下文,若浏览器不关闭,全局上下文会一直存在,直到浏览器窗口关闭,全局上下文才会最终出栈。

姑且认为上面的出入栈流程是宏观的执行上下文,接下来重点学习微观的层面,即执行上下文之中的生命周期。

  1. 执行上下文的生命周期(包含变量的声明周期)

我们通过上面分析的出入栈流程知道,当全局执行上下文或某个执行上下文之中的可执行代码执行到函数(或块级)调用栈时,被调用的这个函数就会开辟一个新的执行上下文,我们称之为函数执行上下文。殊不知这块函数内(或块内)的区域也就是我们耳熟能详的块级作用域

同样的,我们通过图片来了解执行上下文的生命周期
图片来自网络,执行上下文的生命周期

函数执行上下文的生命周期分为三part,分别是:

  • 创建阶段 —— 此阶段执行上下文会分别创建变量对象、确认作用域链、以及确定this指向;
  • 执行阶段 —— 执行代码,这个时候会完成变量赋值、函数引用、以及执行其他可执行代码等工作;
  • 释放阶段 —— 退出执行上下文并销毁。

没想到吧?我们现在知道,在这个函数(或块级)执行上下文里面JavaScript做了很多工作,其中就有变量的生成和变量的赋值,这个过程就是变量的生命周期可以说,在执行上下文的生命周期里面,包含着变量的生命周期!

到这里,其实我们的答案已经出来了。我们老生常谈的变量提升作用域链TDZ、和this指向等头疼的问题产生的源头,其实就包含在函数(块级)执行上下文的生命周期之中!

我们回扣最开始的话:为了加强对变量声明周期的控制,ES6引入了块级作用域。然而所谓的块级作用域,实际上就可以理解为是由函数区域内的执行上下文{}区域内的执行上下文同理)产生的!两者唯一的区别在于,作用域指的是这块执行代码形成的区域;而执行上下文是真实在JavaScript引擎中执行的代码片段。

二、临时性死区的补充

上一篇文章对于TDZ的解释,其实是个人的一部分浅显的理解,望大家见谅。但想要充分认识TDZ的性质,还得要继续剖析我们上面分析到的:变量的生命周期~

  1. 创建变量对象(有自己的生命周期)—— 对标声明和初始化阶段

JavaScript声明的所有变量都保存在变量对象之中,变量对象的创建依次经历以下步骤:

  • 首先获得函数的参数变量及其值(在JavaScript里面函数是一等公民,任何执行上下文都会优先收集函数内的参数及其对应的值,即arguments);
  • 依次获取当前上下文中所有的函数声明,在变量对象中会以函数名建立一个属性,属性值指向该函数所在的堆内存地址
  • 依次获取当前上下文中的变量声明,每找到一个变量声明,就在变量对象中以变量名建立一个属性(开辟栈内存地址)。如果是var关键字声明,则属性值会初始化为undefined
  1. 变量的数据存储(一张图足以说明一切)—— 对标赋值阶段

图片来自网络,变量的数据存储
变量对象创建完成后,接下来就是对数据的存储,基础数据类型往往会保存在栈内存之中(特殊情况除外),而引用数据类型的值则是保存在堆内存之中。

我们知道,在JavaScript里面,是不允许我们直接访问堆内存空间中的数据的。所以当我们在操作对象时实际上实在操作对象的引用,而不是实际的对象。

因此,引用数据类型都是按引用访问的,这里的引用可以理解为保存在栈内存中的一个地址,该地址与堆内存中的对象相关联。

  1. 变量的存储空间(有自己的生命周期)

变量内存空间的使用过程,也有自己的执行流程

  • 分配内存阶段(变量的定义)
  • 使用分配到的内存(可执行的变量语句)
  • 不需要时释放内存 (垃圾回收

举个例子就明白了:

var a = 20; // 分配内存阶段(变量的定义)
console.log(a + 1); // 使用内存阶段(可执行的变量语句)
a = null; // 释放内存(垃圾回收)

经过层层嵌套循环渐进的剖析挖掘,我们发现变量内存空间的生命周期也包含着垃圾回收机制

  1. 变量的垃圾回收

垃圾回收机制的原理就是:找出那些不再继续使用的变量,垃圾收集器会按照固定的时间间隔,周期性的释放其占用的内存。

最常用的垃圾收集方式是标记清除算法。主要依靠 “引用” 的概念,当一块内存空间中的数据能够被访问时,垃圾回收器就会认为 “该数据能够被获得”,不能够被获得的数据,就会被打上标记,并回收内存空间,这种方式叫做:标记 --- 清除算法

搞清楚变量的生命周期之后,现在我们可以充分的分析临时性死区的原因了(dog狗头)。

根据上面分析的变量的生命周期的本质,我们大概可以把它囊括成四个阶段:

  • 声明阶段(创建变量对象、依次获取上下文中的变量声明(声明语句)、函数声明直接开辟内存并执行初始化);
  • 初始化阶段(开辟栈或堆内存,varlet被初始化为undefinedconst并未执行初始化);
  • 赋值阶段(将变量值赋值到对应的栈或堆地址)
  • 释放阶段(垃圾回收)

接着我们从造成TDZ的源头入手,分别为varletconst声明变量的JavaScript语句执行一次变量生命周期:

  1. var声明变量的生命周期

由于var将声明阶段和初始化阶段耦合到了一起,导致执行上下文一开始就将变量初始化成了undefined,因此造成了变量提升,故而该变量不存在有TDZ
引入一个例子作为参考:

// 第一个特性 ---- 变量提升
function fn() {
  // 不存在 TDZ
  console.log(a); // undefined
  var a = 1;
}

// 第二个特性 ---- 不受块级作用域限制
console.log(a) // undefined
{
  var a = 1;
}
  1. let声明变量的生命周期

var相反,这种关键字声明的变量是遵循变量的生命周期的。声明阶段和初始化阶段解耦,所以该变量只有在声明阶段被提升至作用域顶部收集到变量对象之中,而初始化阶段并没有被提升。因此在声明阶段和初始化阶段之间就产生了TDZ,俗称临时性死区。此阶段不可访问该变量,否则就会抛出异常。

而初始化阶段的变量则被初始化为undefined

最重要的是受块级作用域的影响,let只在代码块内声明变量,从而避免全局污染。

引入一个例子作为参考:

// 第一个特性 ---- 存在 TDZ
console.log(a); // Uncaught ReferenceError: a is not defined
let a = 1;

// 第二个特性 ---- 块级作用域
{
  let a = 1;
}
console.log(a); // Uncaught ReferenceError: a is not defined

// 第三个特性 ---- 不可重复声明
let a;
let a = 1; // Uncaught SyntaxError: Identifier 'a' has already been declared

// 第四个特性 ---- 声明提升之后执行初始化
let a;
console.log(a); // undefined
  1. const声明常量的生命周期

这种关键字声明的变量跟let一样,也是遵循变量的生命周期的,都是存在声明提升。唯一的不同是,const是用来声明常量的,通常只声明一个只读引用的数据类型,且它的初始化阶段和赋值阶段要同时一起完成,否则就会报错。

引入例子参考举证:

// 第一个特性 ---- 存在 TDZ
console.log(a); // Uncaught ReferenceError: a is not defined
const a = 1;

// 第二个特性 ---- 块级作用域
{
  const a = 1;
}
console.log(a); // Uncaught ReferenceError: a is not defined

// 第三个特性 ---- 不可修改
const a = 1;
a = 2; // Uncaught TypeError: Assignment to constant variable.

// 第四个特性 ---- 必须声明的同时赋值
const a; // Uncaught SyntaxError: Missing initializer in const declaration

使用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](); // Uncaught TypeError: Assignment to constant variable.

按照我们之前的理解,for循环的本质是,在每一个循环的过程中开辟一个隐藏的执行上下文,形成对应每一个循环的块级作用域。循环执行完成之后,该执行上下文也就被释放。按道理说这里不应该报错呀?各个作用域互不相干?

其实本质上不是这样子的,for循环的底层原理对var、let和const有着不同的处理方式。

首先我们要搞清楚,for循环由四个部分组成:

  • 声明变量i
  • 判断条件;
  • 执行可执行语句;
  • 自增变量i

文中截图,变量的声明周期
文中截图,变量的声明周期囊括
通过我们文中之前分析的截图,这里需要强调分析for循环的本质:

  1. for () —— 遇见for循环了,创建for执行上下文入栈,形成块级作用域;
  2. const i = 0; —— 每个迭代循环开始,都会收集for循环的参数变量及其值,创建变量对象(声明并赋值变量);
  3. i < 10; —— 判断条件,如果为true则执行块内的可执行代码;
  4. {} —— 创建{}的执行上下文入栈,经历执行上下文生命周期,创建新的变量对象,收集{}的函数以及变量,经历变量的生命周期,等可执行代码执行完,释放当前的块级执行上下文;
  5. i++ —— 执行自增,销毁当前for执行上下文,继续下一个循环;
  6. 创建新的for执行上下文入栈,形成块级作用域;
debugger
  1. const i = 1; —— 每个迭代循环开始,收集for循环的参数变量及其值,创建变量对象(声明阶段和初始化阶段耦合,声明变量i,并把上一个i做为它的初始化值,然后我们再进行赋值阶段的时候,就会报错了。如果使用let就不会有这种问题);

分析下来原理就是,我们知道,每种声明变量的关键字都有它自己独特的生命周期。但for循环里面,变量i的生命周期不会遵循关键字形成的生命周期,永远都是,声明、初始化、赋值三个阶段。并且在初始化阶段,i会把上一个迭代循环中的i作为初始值。但由于const有只读引用的属性,所以新的const常量无法被重新赋值。

值得一提的是,在for in循环中,使用const也没问题,因为它的每次迭代循环所新声明的i,是完全遵照关键字声明的生命周期的。

四、总结

每次写完文章之后,我都会从头到尾的看好几遍,逻辑是否通顺?论点是否可信?论据是否充足?所以看完之后,也会总结文中的知识点纳入到自己的知识网之中:

  1. JavaScript运行环境也叫执行上下文,JavaScript引擎通过栈储存的方式处理执行上下文;
  2. 全局执行上下文永远在最底端,最顶端永远都是当前正在执行的执行上下文,遵循后入先出原则;
  3. 执行上下文有生命周期,分为创建、执行和释放;
  4. 块级作用域就是当前执行上下文所形成的代码区域;
  5. 执行上下文的生命周期包含:变量的生命周期、确定作用域链、确定this指向等等;
  6. 变量的生命周期分为:创建变量对象及其生命周期、变量储存到内存的原则、变量的使用及其使用时内存的生命周期、变量的释放(标记清除法,垃圾回收机制);
  7. 关键字不同,声明的变量其生命周期也不同。基本分为:声明阶段、初始化阶段、赋值阶段、应用阶段和销毁阶段;
  8. var将声明和初始化耦合,存在变量提升,不存在TDZ
  9. let没有耦合声明和初始化,存在声明提升,不存在初始化提升,存在TDZ
  10. const没有耦合声明和初始化,存在声明提升,不存在初始化提升,存在TDZ,但初始化和赋值必须同时执行;
  11. for非常坑,里面的i不遵循关键字自己的生命周期,只遵循基本的变量生命周期,并且每个初始化阶段都会赋值前一个迭代循环中的i做为初始值;
  12. for in 没有那么多花里胡哨,在每个初始化阶段也不会赋值前一个迭代循环中的i做为初始值;

通篇写完,不知道是否感觉到自己的废话多了,这点需要大家为我明鉴,希望我有不对的或者多余的地方,大家能够为我指出。非常感谢阅读到这里的你,愿你的未来一篇光明。

  • 参考文章:
  • https://juejin.cn/post/6844903608316592141 —— 冴羽大神的掘金
  • https://zhuanlan.zhihu.com/p/158817336 —— 知乎社区
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-09 18:14:27  更:2022-04-09 18:16:30 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 2:30:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码