# ECMAScript、ES6 简史,let、const、var 区别和应用

TIP

很开心当我们已经把 JavaScript 核心基础到高级进阶部分的内容已全部学完,从本节开始我们将进入到 ES6~最新版本的学习,这也将是我们未来项目开发每天都会用到的。

之前有同学经常问到的一些疑问:

  • 我是一名前端小白,刚接触前端不久,学习了一些 JavaScript 语法,但发现在网上看一些技术文章或一些开源项目的时候,很多语法根本就看不懂。
  • 我是一名求职者,正打算成为一名前端工程师,但面试过程中每次都会被问到一个问题,你对前端基础了解多少呢 ?你对 ES 新特性掌握了多少呢 ?每次遇到这些问题总是有些力不从心。
  • 我是一名初入职场的菜鸟,已经掌握了一些前端开发的技能,正打算在公司大展拳脚,确发现公司里边大牛写的一些语法根本看不懂,甚至都有些怀疑我之前学习的是假的 JavaScript 吗 ?
  • 我是一名技能党,喜欢最求最新的语法,最炫酷的技术,时刻走在技术的最前沿励志做一名技术的弄潮儿,想了解 ES 又出现了哪些新特性以及如何使用这些新特性在我的项目中编写出最炫酷的代码,希望知道 ES 又有哪些新特性呢 ?

其实产生的这些情况的产生都是因为对ES的新特性使用方式不熟悉而产生的一些尴尬情况。

那么到底什么是ESESJS又有什么关系呢 ?

# 一、什么是 ES

我们首先来看 ECMA 是什么

ECMA,读音类似“埃科妈”,是欧洲计算机制造商协会(European Computer Manufacturers Association)的简称,是一家国际性会员制度的信息和电信标准组织。1994 年之后,由于组织的标准牵涉到很多其他国家,为了体现其国际性,更名为 Ecma 国际(Ecma International),因此 Ecma 就不再是首字母缩略字了。

了解了这段历史,为了技术书写的专业性,如果文章中提到 Ecma 的时候,可以写成 Ecma 或者 ecma,不要写成全大写的 ECMA,除非是 ECMAScript 或 ECMA-XXX 这类专有名词。

image-20221004231157607

什么是 Javascript?

1995 年,著名的网景公司(Netscape)的 Brendan Eich 开发了一种脚本语言,最初命名为 Mocha,后来改名为 LiveScript,最后为了蹭当时火热的 Java 热度重命名为了 JavaScript。

什么是 ECMAScript?

了解了 Ecma 国际和 JavaScript,就方便了解 ECMAScript 了,ECMAScript 是一种由 Ecma 国际在标准 ECMA-262 中定义的 脚本语言 规范。这种语言往往被称为 JavaScript 或 JScript ,但实际上 JavaScript 和 JScript 是 ECMA-262 标准的实现和扩展。

一句话总结 ES

ES 是指 ECMAScript,Ecma 是一个专门为技术制定标准的组织。ECMAScript 是由 Ecma 国际通过 ECMA-262 标准化的脚本程序设计语言。

  • ECMAScript 是一种标准或者说叫做一种规范
  • JavaScript 是 ECMAScript 的一种实现,也就是说 JavaScript 是一门遵循了 ECMAScript 语言规范而设计的编程语言

image-20221003161351578

# 1、什么是神秘的 ECMA-262

TIP

Ecma 国际的标准都会以 Ecma-Number 命名,ECMA-262 就是 ECMA 262 号标准,具体就是指 ECMAScript 遵照的标准。1996 年 11 月,网景公司将 JavaScript 提交给 Ecma 国际进行标准化。ECMA-262 的第一个版本于 1997 年 6 月被 Ecma 国际采纳。

Ecma 国际制定了许多标准,而 ECMA-262 只是其中的一个,所有标准列表查看

https://www.ecma-international.org/publications-and-standards/standards/ (opens new window)

截止 2022 年 12 月,最新的 Ecma 标准已经更新到了 ECMA-422

image-20221229160838283

Ecma 标准涉及的类别非常多,官网提供了按照类别和最新修改排序的列表,我们来看看 ECMA-262 (opens new window) 属于哪个类别:

image-20221004232231312

ECMA-262属于“软件工程与接口”类别,该类别截止目前一共有 13 个标准

# 2、语法提案的批准流程

TIP

任何人都可以向标准委员会(又称 TC39 委员会)提案,要求修改语言标准。

一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。

  • Stage 0 - Strawman(展示阶段)
  • Stage 1 - Proposal(征求意见阶段)
  • Stage 2 - Draft(草案阶段)
  • Stage 3 - Candidate(候选人阶段)
  • Stage 4 - Finished(定案阶段)

一个提案只要能进入 Stage 2,就差不多肯定会包括在以后的正式标准里面。ECMAScript 当前的所有提案,可以在 TC39 的官方网站GitHub.com/tc39/ecma262 (opens new window)查看。

# 3、探秘 TC39 神秘组织

TIP

TC39 是 Technical Committee 39 的简称(点击查看,该组织官方介绍 (opens new window)),是制定 ECMAScript 标准的委员会

TC39(Technical Committee 39)是推进 ECMAScript 发展的委员会。其会员都是公司。TC39 定期召开会议,会议由会员公司的代表与特邀专家出席。

由各个主流浏览器厂商的代表构成,主席团三人分别来自 Bloomberg、Igalia 和 Microsoft,下设三个工作组(task group) TC39-TG1(通用语言) (opens new window)TC39-TG2(国际化 API 规范) (opens new window)TC39-TG3(安全) (opens new window)

  • TC39-TG1 工作组主要工作是通用、跨平台、供应商中立的编程语言 ECMAScript® (JavaScriptTM>) 的标准化。这包括语言语法、语义以及支持该语言的库和补充技术。
  • TC39-TG2 工作组 ECMAScript® 国际化 API 标准。支持需要适应不同人类语言和国家/地区使用的语言和文化约定的程序。
  • TC39-TG3 工作组 ECMAScript® (TM) 安全模型对当前和未来不断变化的威胁形势有效。

我们经常会看到类似的新闻:XX 公司成为 Ecma TC39 成员。想要加入 TC39 会议,必须先成为 Ecma 会员, 点击查看,目前已经加入 Ecma 的成员 (ecma-international.org) (opens new window)

image-20221005002910480

以上企业为普通会员:在大会上有表决权,行使附则和规则中规定的其他专有权

image-20221005003624245

image-20221005003714156

以上企业为准会员:在大会上没有表决权

# 4、如何加入 Ecma 组织成员

TIP

应 Ecma 秘书长的邀请,来自非成员公司的专家可以参加 Ecma 小组,例如熟悉工作方式。要定期参加,组织必须加入 Ecma 作为成员。点击查看官方加入方式 (opens new window)

image-20221005004959828

会员类别 年费(CHF)瑞士法郎 约合人民币(CNY) 会员数量 权限
普通会员 70’000.- 508086.25 9 在大会上有表决权
行使附则和规则中规定的其他专有权
准会员 35’000.- 254043.13 14 在大会上没有表决权
中小企业成员 17’500.- 127021.56 6 /
SPC 成员(小型私营公司或其他合法的营利性组织) 3’500.- 25404.31 10 /
非营利组织(NFP) 0.- 0 42 /

详细规则和细节,查看官方即可 (opens new window)

# 5、ECMAScript 版本

TIP

ECMAScript = 由 ECMA 这个标准化组织制定的一个语言标准

语言标准就是:语法 和 API

  • 语法:如,规定了如何声明变量、如何声明常量、如何声明函数还规定了我们有哪些数据类型(基本数据类型,应用数据类型等 ... 还有其他很多东西)
  • API:如,方法和函数(如数组的方法,对象,全局的 ...)

从 2015 年开始 ECMA 组织决定每一年都会发布一个新的版本,这个新的版本就会包括:ES 新特性,用于语法的升级或弥补之前一些语法的缺陷。

从命名上看,ES6 被命名为 ECMAScript2015,通过命名可以体现出当前这个版本所对应的年份。所以我们经常说的 ES6 和 ES2015 就指的是同一个版本,以后每一年发布的版本我们都会使用年份去命名。

  • 按 ES 的命名方式:ES6 -> ES7 -> ES8 -> ES9 -> ... = 这些都可以统称为 ES6+

  • 按年份的命名方式:ES2015 -> ES2016 -> ES2017 -> ES2018 -> ...

image-20221003182026633

不论使用哪一种命名方式都是可以的,都不重要,只要我们清楚他们对应的方式就 OK 的

  • 比如 6 对应的 2015 ,7 对应的 2016 即可
  • 当我们看到对应的文章或描述的时候能对应上,知道别人在讲什么就好
  • 同时也需要知道 ES6 才是 ES6+的基础,如:ES7、ES8、ES9 本质都是在 ES6 的基础上扩展的语法或 API(升级或弥补之前一些语法的缺陷)

我们学习的重心还是要放在 ES6 上,不要本末倒置就好

每年的ES新版本都会引入很多新特性,如下
版本 发布时间 主要更新内容
ES6(ES2015) 2015 年 改动最多,具有里程碑意义
新增变量 let 和 const,箭头函数
新增数组方法,如:map、filter 等
解构赋值,快速复制数组和对象,模板字符串
模块化,面向对象,Promise 等
ES7(ES2016) 2016 年 数组扩展:Array.prototype.includes() ,幂运算符
ES8(ES2017) 2017 年 异步编程解决方案
新增 async、await
对象扩展:Object.values()Object.entries()
对象属性描述:Object.getOwnPropertyDescriptors()
字符串扩展:String.prototype.padStart()String.prototype.padEnd()

尾逗号 Trailing commas
ES9(ES2018) 2018 年 异步迭代:for await ofSymbol.asyncIterator
正则表达式扩展:dotAll,具名组匹配,后行断言
对象扩展:Rest & Spread
Promise 扩展:Promise.prototype.finally()
字符串扩展:放松模板字符串文字限制
ES10(ES2019) 2019 年 对象扩展:Object.fromEntries()
字符串扩展:String.prototype.trimStart()String.prototype.trimEnd()
数组扩展:Array.prototype.flat()Array.prototype.flatMap()
修订 Function.prototype.toString()
可选的 Catch Binding:省略 catch 绑定的参数和括号
JSON 扩展:JSON superset,JSON.stringify()增强能力
Symbol 扩展:Symbol.prototype.description
ES11(ES2020) 2020 年 全局模式捕获:String.prototype.matchAll()
动态/按需导入:Dynamic import()
新的原始数据类型:BigInt
Promise 扩展:Promise.allSettled()allSettled() vS all()
全局对象:globalThis
可选链:Optional chaining
空值合并运算符:Nullish coalescing Operator
ES12(ES2021) 2021 年 String.prototype.replaceAll:替换字符不用写正则了
Promise.any()
WeakRefs:使用弱引用对象
逻辑运算符和赋值表达式:| | =&&=??=
数字分隔符:在数字之间创建可视化分隔符,通过\_下划线来分割数字
Intl.ListFormat:用来处理和多语言相关的对象格式化操作
Intl.DateTimeFormat API 中的 dateStyle 和 timeStyle 的配置项:用来处理多语言下的时间日期格式化的函数
ES13(ES2022) 2022 年 Top-level Await(顶级 await)
Object.hasOwn()
at()
error.cause
正则表达式匹配索引
类 class:公共实例字段,私有实例字段,私有方法
静态公共字段、静态私有字段、静态私有方法,类静态块

# 6、ES6 之前的历史版本

TIP

之前的版本中有 ES1 ~ ES3ES5 ~ ES6 唯独跳过了 ES4,因为

  • ES4 被废弃了 ,因为 ES4 是一次非常大胆的改革。但是因为太激进了,导致了 ES4 和 ES3 像两门截然不同的语言,跨度太大以至于被废弃了。
  • 它的一些不太激进的部分被吸收进了 ES5
  • 激进一些的被吸收进了 ES6
  • 更激进一些就在后边的版本,接着吸收

ES1 和 ES2 都是比较原始的版本,都还不太成熟,真正成熟的是 ES3,我们现在用的最多其实就是 ES3,可能你以为我们用的 ES5 比较多,但在 ES6 之前你用的最多的还是 ES3 里边的内容

比如:我们现在用的最多的 ES3 中的内容

  • do while
  • switch
  • 正则表达式
  • 等等 ... 一系列我们用得到的语法和 API

而我们感觉用的比较多的 ES5 如下方法反而用的不多,可能都没有用到过,当然这个跟我们关系不大,因为这是跟兼容性有关的,因为之前的 ES3 兼容性是非常好的,因此用的更多。

  • forEach
  • map
  • filter
  • Object.create
  • Object.defineProperty
  • 等 ...
版本 发布时间 主要内容
ES1 1997 年 制定了语言的基本语法
ES2 1998 年 较小的改动,只改变编辑方式
ES3 1999 年 引入正则表达式、异常处理try/catch、格式化输出等,IE 开始支持
ES4 2007 年 过于激进,未发布
ES5 2009 年 引入严格模式、JSON,扩展对象、数组、原型、字符串、日期方法等

如今,已经进入了 ES6 了 ...

ECMA-262(ECMAScript)历史版本查看网址:https://www.ecma-international.org/publications-and-standards/standards/ecma-262/ (opens new window)

# 7、ES、ES6 与 JavaScript 的关系

TIP

  • JavaScript(浏览器端)= ECMAScript(语法+API)+ DOM(文档对象模型) + BOM(浏览器对象模型)

  • ES 等同于 ECMAScript ,是语言的标准,6 是版本号,即 ES6 = ECMAScript 这门语言的第 6 代标准

# 8、ES6 的兼容性

TIP

  • 主流浏览器的最新版几乎全部支持 ES6
  • IE 老版本等不支持的浏览器,可以用 Babel 转码 (opens new window)
  • 因此,放心大胆的使用 ES6 即可

兼容性检测查询地址:http://kangax.github.io/compat-table/es6/ (opens new window)

image-20221005160502430

# 9、ES6 环境搭建

TIP

目前各大浏览器基本上都支持 ES6 的新特性,其中 Chrome 和 Firefox 浏览器对 ES6 新特性最友好,IE7~11 不支持 ES6

支持 ES6 的浏览器,相应的开始时间及版本

浏览器 版本 日期
Chrome 58 2017 年 4 月
Firefox 54 2017 年 6 月
Edge 14 2016 年 8 月
Safari 10 2016 年 9 月
Opera 55 2017 年 8 月

如果浏览器不支持 ES 新的语法时,怎么办

从 ES6、ES7、ES8、ES9、ES10、ES11、ES12、ES13 ... 到未来更多新的语法

ES 每一年都会不断的更新,我们的目的是希望这些语法都能被浏览器所识别,但问题是这些新的语法并不能被所有的浏览器非常好的识别。

因为,我们的浏览器也是用代码写的,ES 这些新的语法之所以能被浏览器识别是因为浏览器的代码能够识别 ES 新的方法、函数、API 等。但并不是每一个浏览器厂商都会随着 ES 的更新而同步升级的。

  • 其中做的最好的是 Google 浏览器,因此强烈建议大家在进行前端开发的时候,首选 Google 浏览器,并进行调试。同时 Google 浏览器的调试功能也非常强大,我们可以很方便的定位问题或分析网页的性能等问题。
  • 当 ES 新的语法不能被浏览器识别时,我们就会配置相应的工具来将 ES 新的语法转换成浏览器能够识别的代码。
  • 我们都知道 ES5 是可以很好的被浏览器识别的,我们通过 Babel (opens new window) 将 ES6 及最新版本的语法转换成 ES5 就能够被浏览器识别了。
  • 一般 Babel 都会配合 Webpack 来一起使用。因此,我们先专注学完 ES6+相关语法后,再来学习 Webpack 和 Babel

image-20221005184219400

此次课程学习我们先使用 Google 浏览器来运行和学习 ES6+ 最新的语法,学完后再配置 Webpack 和 Babel,目前先了解整个过程。

# 10、为什么要学习 ES6~ES13 呢

TIP

有些同学会认为我们现在前端开发不都在使用框架吗 ?那 ES 还重要吗 ?

是的没错,目前项目基本都是基于框架来开发的。很多同学会误以为学会了框架就等于掌握了前端

image-20221003161434647

但是,不管你使用前端的主流框架 Vue、React、微信小程序生态、node.js 开发服务端也好,其实都像是一个盖房子的过程。

也就是说我们可以采用 Vue 的方式去盖房子,也可以采用 React 的方式去盖房子,但不管采用哪种方式去盖房子都少不了盖房子的一个基本要素,也就是砖头。而 ES 语法就像是一个个的砖头一样,不管我们选择什么样的框架开发什么样的需求都要使用 ES 这个砖头来完成。

所以说,只要是你开发的是前端项目或者说是 Node.js 的项目 ES 一定是你逃不掉的一项必备技能。

image-20221003162940227

注:

其实市面上大部分的资料要么总结的不够全面,要么只是语法如何使用,但在实际项目中我们遇到实际问题的时候却总是想不起来去用我们学过的这些新特性,其实总有一种纸上得来终觉浅的感觉。

本质上是,我们没有真的理解如下内容

  • ES 新特性优势
  • 每一个新特性的应用场景

这样就会导致,我们在项目中虽然好像学过很多语法,但遇到实际情况的时候,我们根本就想不到这样的语法可以解决实际场景的问题,而本次的学习就是为了弥补这种遗憾的

# 二、let 和 const

TIP

ES6(ES2015)新增加了两个重要的 JavaScript 关键字: letconst

# 1、什么是 let 和 const ?

TIP

let 和 const 是用来声明变量或声明常量的,在 ES6 之前我们声明变量都是使用 var,在 ES6 中

  • let 替代 var,声明变量
  • const 声明常量,constant 的缩写

# 2、let 和 const 的用法

TIP

let 和 const 的用法与 var 一样

var username = "清心老师";
let age = 18;
const sex = "female";
console.log(username, age, sex); // 清心老师 18 female

# 3、什么是变量,什么是常量 ?

TIP

  • var、let 声明的就是变量,变量一旦初始化之后,还可以重新赋值
  • const 声明的就是常量,常量一旦初始化,就不能重新赋值了,否则就会报错
var username = "清心老师";
let age = 18;
const sex = "female";
console.log(username, age, sex); // 清心老师 18 female

// 什么是变量,什么是常量
username = "arry老师";
age = 20;
console.log(username, age); // arry老师 20 ,我们可以看到变量是可以重新赋值的

sex = "male"; // 控制台报错 Uncaught TypeError: Assignment to constant variable. 错误意思:给常量赋值了

# 4、为什么需要 const ?

思考:

  • 我们为什么需要常量,难道不够用吗 ?
  • 如果够用的话谁还用 const 呢 ?
// 假如只有let的情况,我们期望声明的 sex 值一旦声明后就是不变的。
// 当然性别一般情况下就是不变的,也符合常识
let sex = "male";
// ... 经历很多程序之后,如果不小心修改了 sex 的值,浏览器也不会报错
sex = "semale";
console.log(sex); // semale

// 我们可以看到 sex 的值被修改了,其实它按我们的期望来讲就是隐形一个错误
// 也就是说这样的错误很可能发生,并没有任何提示,但它确实是一个错误,会造成我们的程序出现问题,类似这样的问题在过去么有什么很好的办法解决,只能通过开发者自己小心来定义

但,const 的出现就不会有以上的问题了

// 我们现在使用 const 将 sex 声明为常量
const sex = "male";
// ... 如果不小心完成了以下赋值操作,就报错了
sex = "semale"; // Uncaught TypeError: Assignment to constant variable.
console.log(sex);

注:

运行以上代码就直接报错了,使用 const 就直接从语法层面杜绝了类似错误的发生,我们在实际开发中也不需要为这些问题而小心翼翼了,这就是我们为什么需要 const 的原因。

const 的设计初衷

const 就是为了那些一旦初始化就不希望重新赋值的情况而设计的

# 5、const 的注意事项

  • 使用 const 声明常量,一旦声明,就必须立即初始化,不能留到以后赋值
// 以下错误演示
const sex; // Uncaught SyntaxError: Missing initializer in const declaration 未捕获语法错误:const声明中缺少初始值设定项
sex = 'male';

// 正确的做法是:声明+初始化应该一气呵成
const sex = 'male';
  • const 声明的常量,允许在不重新赋值的情况下修改它的值

情况一:const 声明的基本数据类型

// 基本数据类型
const sex = "male";
sex = "female"; // Uncaught TypeError: Assignment to constant variable.

// 我们看到报错了,也就是说:对于基本数据类型来说,我们是没有办法在不重新赋值的情况下修改它的值
// 因此,对于const声明的常量是基本数据类型来说就没有办法修改它的值

情况二:const 声明的引用数据类型

// 引用数据类型
const person = { username: "清心" };
// 对person重新赋值,通过前边的学习,我们知道对于const声明的常量来说是不被允许的
// person = {}; // 报错了 Uncaught TypeError: Assignment to constant variable.

// 但,引用数据类型不一定要通过重新赋值的方式来修改值
// 可以直接找到对应的属性,对它完成修改
person.username = "arry";
console.log(person); // 正确输出修改后的对象 {username: 'arry'}

总结

const 声明的常量,其实在某些情况下是可以修改它的值得,但一定要保证不重新赋值 ,这一点主要针对引用数据类型

这就是说:const 声明常量为引用类型,不可以重新赋值,但可以修改里面的值。

# 6、什么时候用 const ,什么时候用 let

TIP

什么时候用 const 声明常量,什么时候用 let 声明变量,这个在实际开发中经常会困扰大家。之前我们只有一个 var 没得选,好处就是用就完了。

那么,现在有得选了,到底使用 const 好呢 ?还是用 let 好呢 ?

  • 对于一些比较简单情况我们一眼能够看出进来的,就直接使用就好,没必要考虑什么 。如下下代码, 就直接用 let 就好
for (let i = 0; i < 5; i++) {}
  • 对于一些在刚开始写代码的时候,我们也不清楚到底是用 let 还是 const
// 比如,我们定义一个 username 来保存用户名
// 但我们在写代码的时候,还不清楚后边会不会希望修改这个用户,我们这时的需求还没有确定,到底用 let 还是 const 好呢 ? 就很难抉择
username = "arry";

实际开发中的经验总结

  • 当你不知道用什么的时候,我们可以先用 const 来声明,即使不修改也不会报错,如果后边发生了修改。这时,也不用担心 !因为程序会报错,再修改成 let 也是来得及的。
  • 只要错误不被淹没都可以随时修改。这样的好处就是,即使发生了错误也不会漏掉。
  • 如果一开始就用 let ,不小心值就发生了改变,但这并不是我们希望发生的。
  • 因此,推荐大家在实际开发中不知如何抉择时,可以使用这个的方式。

以后再也不必纠结到底使用哪个了 !这也是开发中的一些经验总结

# 三、let、const 和 var 的区别

TIP

let、const 和 var 的区别总共有 5 点:

  • 不允许重复声明
  • 不存在变量提升
  • 暂时性死区
  • window 对象的属性和方法(全局作用域中)
  • 块级作用域

其中 块级作用域是 let、const 和 var 之间最重要的一个区别了。这也是我们面试真题中高频面试题了,能否真正给出有竞争力的回答,就看我们是否有真正的理解到位了。

# 1、不允许重复声明

TIP

  • 重复声明:是指在同—作用域下已经存在的变量或常量,又声明了—遍
  • 同一作用域下,var 允许重复声明,let、const 不允许
// 如:使用var重复声明变量
var i = 1;
// ... 在写了很多行代码之后,突然忘记了之前有声明过a变量,又声明了一次
var i = 2;
console.log(i); // 2 ,这里最可气的是 控制台居然没有报错,还给我们修改了值

// 如:使用 let 或 const 重复声明变量
let n = 1;
// ...
let n = 2; // Uncaught SyntaxError: Identifier 'n' has already been declared 已声明标识符 "n"
console.log(n);

// 使用 const 声明与 let 类似

另一种重复声明的场景也会报错

// 声明一个函数(以函数参数的形式声明的变量)
function foo(i) {
  let i = 2; // Uncaught SyntaxError: Identifier 'i' has already been declared 已声明标识符 "i"
}
foo();

注:

以上 let 或 const 重复声明变量时,直接报错,会明确的告诉我们该变量已被声明了,不能再重复声明一遍。这样,就从语法层面直接杜绝了错误的发生。

我们可以看到,使用 let 和 const 声明都是类似的。因此在学习后边的区别学习中,就不单独把 const 拿出来讲了,let 已经可以代表了。

也就是说 let 和 const 的表现是一致的。

# 2、不存在变量提升

TIP

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

  • var 会提升变量的声明到当前作用域的顶部
// 不声明变量,直接使用
console.log(a); // Uncaught ReferenceError: a is not defined

先输出,后声明,分析变量提升的过程

console.log(a); // undefined
var a = 1;
// 没有报错,输出了 undefined ,这就涉及到了变量提升

// 以上代码通过变量提升后,实际的相当于如下步骤
var a;
console.log(a); // undefined
a = 1;
console.log(a); // 1

// 其实变量提升带给我们更多的是困惑,因为它会和我们的想法和逻辑是不相符的,这也是我们学习JS需要记住的点

通过 浏览器 REPL 执行环境进行调试可以看到变量提升的过程

var-variable-promotion

  • let、const 不存在变量提升
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization 初始化之前无法访问 “a”
let a = 1;

// 之所以报错,是因为 let 或 const 不存在变量提升

总结

let 和 const 之所以不存在变量提升,还是为了让我们养成良好的编程习惯。

对于所有的变量或常量,我们一定要做到 先声明,后使用

# 3、暂时性死区

TIP

  • 只要作用域内存在 let、const ,它们所声明的变量或常量就自动 “绑定” 这个区域,不再受到外部作用域的影响
  • let、const 存在暂时性死区,var 不存在
let a = 1;
function foo() {
  console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization 初始化之前无法访问“a”
  let a = 2;
}
foo();

// 以上代码存在全局变量 a ,但函数作用域内 let 又声明了一个局部变量 a ,导致后者绑定这个函数作用域
// 所以在 let 声明变量前,输出 a 会报错

ES6 明确规定

如果区块中存在 let 和 const 命令,则这个区块对这些命令声明的变量从一开始就形成封闭作用域。只要在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上称为 “暂时性死区” (temporal dead zone ,简称 TDZ)

if (true) {
  // TDZ开始
  tmp = "abc"; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

// 上面代码中,在let命令声明变量tmp之前,都属于变量tmp的 “死区”

有些“死区”比较隐蔽,不太容易发现

function bar(x = y, y = 2) {
  return [x, y];
}

bar(); // 报错
bar(2, 3); // 不报错

// 上面代码中,调用bar函数之所以报错,是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。
// 如果y的默认值是x,就不会报错,因为此时x已经声明了。

function bar(x = 2, y = x) {
  return [x, y];
}
bar(); // [2, 2]

另外,下面的代码也会报错,与var的行为不同。

// 不报错
var x = x;

// 报错
let x = x;
// ReferenceError: x is not defined

// 上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。
// 上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。

注:

ES6 规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。

总之,let、const 存在暂时性死区,暂时性死区的本质是:在当前作用域,所要使用的变量已经存在,不会再访问该作用域以外的同名变量,并且只有在声明变量之后,才可以获取和使用该变量,否则就会报错。

同样,只要养成良好的编程习惯,对于所有的变量或常量,做到先声明,后使用就没有问题。

# 4、window 对象的属性和方法

TIP

全局作用域中,var 声明的变量,通过 function 声明的函数,会自动变成window 对象的属性或方法

// 全局作用域中,var声明的变量,通过function声明的函数,会自动变成window对象的属性或方法
var age = 20;
function add() {}
console.log(window.age);
console.log(window.add === add);

var-window

通过调试,可看到使用 var,function 声明,会自动变成 window 对象的属性或方法

全局作用域中,let、const 声明的变量 或 function 声明的函数,不会自动变成window 对象的属性或方法

// 全局作用域中,let、const 声明的变量 或 function声明的函数,不会自动变成window对象的属性或方法
let age = 20;
const add = function () {};
console.log(window.age); // undefined
console.log(window.add === add); // false

let-const-window

通过调试,可看到使用 let,const 声明,不会自动变成 window 对象的属性或方法

# 5、块级作用域

TIP

let、const 和 var 最重要的区别即:是否拥有块级作用域。

在深入了解它们的区别前,我们需要了解一下,在 JavaScript 中有哪些作用域:

  • 全局作用域
  • 函数作用域/局部作用域
  • 块级作用域(ES6 新增)

上面是 JavaScript 中的三种作用域,那什么是作用域呢 ?

首先要明白的是:几乎所有的编程语言都存在在变量中储值的能力,存储完就需要使用这些值。所以,作用域就是一套规则,按照这套规则可以方便地去存储和访问变量。

在 ES5 中的作用域有全局作用域和函数作用域,而块级作用域是 ES6 的概念。

# 5.1、全局作用域

TIP

全局作用域顾名思义,就是在任何地方都能访问到它,在浏览器中能通过 window 对象拿到的变量就是全局作用域下声明的变量。

var username = "icoding";
console.log(window.username); // icoding

// 使用 var 定义的变量,可以在 window 对象上拿到此变量
// 这里的 name 就是全局作用域下的变量

# 5.2、函数作用域

TIP

  • 函数作用域,也称为局部作用域,所有写在函数内部的代码,就是在函数作用域中
  • 声明在函数作用域中的变量为局部变量,从外层是无法直接访问函数内部的变量
function foo() {
  var username = "icoding";
}
console.log(username); // Uncaught ReferenceError: username is not defined

在函数内部定义的 username 变量,在函数外部是访问不了的。要想在函数外部访问函数内部的变量可以通过 return 的方式返回出来。

function foo(value) {
  var username = " arry";
  return value + username;
}
console.log(foo("hello")); // hello arry

借助 return 执行函数 foo 可以取到函数内部的变量 username 的值进行使用。

# 5.3、块级作用域(ES6 新增)

TIP

块级作用域是 ES6 的概念,它的产生是要有一定的条件的,在花括号{}中,使用 letconst 声明的变量,才会产生块级作用域。

这里需要注意的是

  • 块级作用域的产生是 letconst 带来的,而不是花括号,花括号的作用是限制 letconst 的作用域范围。
  • 当不在大括号中声明时, letconst 的作用域范围是全局,但是不在 window 对象身上
let age = 18;
console.log(window.age); // undefined

上面的代码可以看到,使用 let 方式声明的变量在 window 下是取不到的。

// var声明的变量,不会产生块级作用域
var age = 18;
{
  var age = 20;
  console.log(age); // 20
}
console.log(age); // 20

在使用 var 声明的情况下,可以看出,外层的 age 会被 {} 中的 age 覆盖,所以没有块级作用域的概念,下面看下使用 let 方式声明:

let age = 18;
{
  console.log(age); // Uncaught ReferenceError: Cannot access 'age' before initialization
  let age = 20;
  console.log(age); // 20
}
console.log(age); // 18

这里可以看出 {} 内外是互不干涉和影响的,如果在声明 age 的前面进行打印的话,还会报错,这个时候,age 处于暂存死区,是不能被使用的,下面我们会具体说明。

在低版本浏览器中不支持 ES6 语法,通常需要把 ES6 语法转换成 ES5,使用 babel 把上面的代码转换后得到如下结果:

var age = 18;
{
  console.log(_age); // undefined
  var _age = 20;
  console.log(_age); // 20
}
console.log(age); // 18

从上面的代码中可以看出,虽然在 ES6 语法使用的是相同的变量名字,但是底层 JS 进行编译时会认为他们是不同的变量。也就是说即使花括号中声明的变量和外面的变量是相同的名字,但是在编译过程它们是没有关系的。

ES6 允许块级作用域的任意嵌套

{
  {
    {
      {
        {
          let username = "icoding";
        }
        console.log(username); // Uncaught ReferenceError: username is not defined
      }
    }
  }
}

上面代码使用了一个五层的块级作用域,每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量。

内层作用域可以定义外层作用域的同名变量。

{
  {
    {
      {
        let username = "icoding";
        {
          let username = "icoding";
        }
      }
    }
  }
}

块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。以前我们想要将某个全局变量变成私有的,我们会用 IIFE 来实现,现在有了块级作用域,我们只需要用块级作用域来解决就好。

// IIFE 写法
(function () {
  var age = ...;
  ...
}());

// 块级作用域写法
{
  let age = ...;
  ...
}

# 5.4、为什么需要块级作用域 ?

TIP

我们通过之前的学习知道,ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。所以在 ES6 中新增了块级作用域。

  • 第一种不合理场景: 内层变量可能会覆盖外层变量
var atmp = 2;

function foo() {
  console.log(atmp);
  if (false) {
    var atmp = "hello world";
  }
}

foo(); // undefined

// 上面代码的原意是,if代码块的外部使用外层的atmp变量,内部使用内层的atmp变量。
// 但是,函数foo执行后,输出结果为undefined,原因在于变量提升,导致内层的atmp变量覆盖了外层的atmp变量。

var-has-no-block-level-scope1

let 和 const 有块级作用域,就可以必免这种问题产生

let atmp = 2;
function foo() {
  console.log(atmp);
  if (false) {
    let atmp = "hello world";
  }
}
foo(); // 2
  • 第二种不合理场景: 用来计数的循环变量泄露为全局变量
for (var i = 0; i < 2; i++) {
  console.log("循环内:" + i);
}
console.log("循环外:" + i); // 2

// 上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

let 和 const 有块级作用域,就可以必免这种问题产生

for (let i = 0; i < 2; i++) {
  console.log("循环内:" + i);
}
console.log("循环外:" + i); // Uncaught ReferenceError: i is not defined

// 之所以会报错,是因为使用 let 或 const 声明的变量是有块级作用域的
// let声明的 i 和 for(){} 共同构成了块级作用域,因此在块级作用域内定义的变量 i 只能在for的{}内可访问
// 执行完for循环后,该作用域就销毁了,我们在全局作用域中就找不到i,就报错了

# 5.6、深入理解块级作用域

TIP

很多人对于for(let i=0; i<5; i++){ } 这里不理解,不理解为什么外面就访问不到 i 了。我们说这是 es6 的语法规定的,let 可以形成块级作用域。

那如果没有 es6,那我们要实现相同的功能,用 es5 如何模拟呢 ?

我们来看下 babel 是如何将这段代码转换成 es5 版本的。点击,查看 babel 官网 (opens new window)

// es6版本
for (let i = 0; i < 5; i++) {
  console.log(i);
}
console.log(i);

// babel转换成对应的es5版本
("use strict");

for (var _i = 0; _i < 5; _i++) {
  console.log(_i);
}
console.log(i);

很多人对于for(let i=0; i<5; i++){ }每次迭代都会创建一个新的块级作用域不太理解,这里我们将 es6 的语法代码用 babel 转换成 es5 的语法来看下

// es6版本 for 循环一共创建了5个块级作用域
for (let i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}
console.log(i);

// babel转换成对应的es5版本
("use strict");

var _loop = function _loop(i) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
};
for (var _i = 0; _i < 5; _i++) {
  _loop(_i);
}
console.log(i);

# 5.7、作用域链的复习

TIP

关于变量的查找会涉及到作用域链,我们再次来复习一下

function foo() {
  // 函数作用域
  for (let i = 0; i < 2; i++) {
    console.log(i); // 0 1
    // 块级作用域
  }
  console.log(i); // Uncaught ReferenceError: i is not defined
}

// 全局作用域

foo();
console.log(i); // Uncaught ReferenceError: i is not defined

画图分析以上代码是如何执行的以及它的作用域链

image-20221007231003236

代码解读

  • 首先最外层会有一个全局作用域
  • 当函数 foo() 被调用时,会形成一个函数作用域,注:只有当函数被调用时,才会形成函数作用域,函数调用结束函数作用域就销毁
  • 继续执行 for 循环,for(){} 和 let 共同构成了一个块级作用域
  • 这时,我们就有了一个嵌套的三层的作用域,最内层是块级作用域,外层是函数作用域,最外层是全局作用域
  • 当在 for 循环中打印输出 i ,首先会在当前的块级作用域中去查找是否存在 i 如果找到了就输出,如果查找一个不存在的 i 那就往上一层作用域中查找或向外层作用域中查找(即 foo 函数构成的函数作用域中查找),如果还没有,就继续往外层查找(到全局作用域中查找 i)如果还是找不到就报错了,这时就终止了
  • 这样的过程就构成了一个链条的形式,由内层 -> 到外层 -> 一直到最外层,这些作用域的节点和节点之间就构成了一个链条,这就是变量或常量的查找的一个链条。
  • 首先在当前作用域中找,找到了就不会再找了,就跟外层作用域没关系了,如果找不到就会往外找,一直找到最外层,找到全局作用域中才截止。
  • 因此,for 循环中找到了块级作用域中 i 就输出 0 1 就不会往外层查找了
  • 在函数作用域中打印输出 i 依次往外层查找,发现没有找到 i 就报错了,这时程序就终止了
  • 如果,前边都没有报错,当 foo() 函数执行完毕后,最后在全局作用域中打印输出 i 同样也会报错
  • 要注意:变量的查找只能有内往外,不能由外往内查找

总结

作用域链的流程:内层(块级)作用域 -> 外层(函数)作用域 -> ... -> 全局作用域

我们是以这样的顺序去查找变量或常量的,当然也不会一直查下去,一但找到就终止了

# 5.8、ES6 中有哪些块级作用域

TIP

大部分具有花括号{}的结构,都可以构成块级作用域

// 只要一个花括号 {} 就可以构成块级作用域
{
  let age = 20;
  console.log(age); // 20
}
console.log(age); // Uncaught ReferenceError: age is not defined

报错原因:{} 花括号就是一个块级作用域, 它执行完毕之后就会被销毁

TIP

  • 还具有{}块级作用域的结构,如:{}for(){}while(){}do{}while()if(){}switch(){}
  • 其中 function(){} 也有{} 但属于函数作用域,不属于块级作用域
  • 另外,还有对象也有 {} 如:const person = {} 注:对象是不构成任何作用域的,我们知道 JavaScript 的作用域就 3 个,块级作用域、函数作用域、全局作用域

注:以上这些结构只有和 let、const 配合使用才会有块级作用域,var 是没有块级作用域的。

总结:

块级作用域是 ES6 中新增的一个作用域,指在花括号{}里面使用 let 或 const 关键字声明变量或常量,就会形成一个块级作用域。

但有两个需要特殊记忆,函数和对象的花括号{}不属于块级作用域。

# 四、let、const 在实际开发中的应用

应用实现需求:

有 3 个按钮,点击 0 号按钮打印索引值为 0,点击 1 号按钮打印索引值为 1,点击 2 号按钮打印索引值为 2

# 1、实现方式一:在 ES6 之前使用 var 如何实现

<head>
  <style>
    body {
      padding: 100px 0 0 200px;
    }
    .btn {
      width: 50px;
      height: 50px;
      font-size: 30px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <button class="btn">0</button>
  <button class="btn">1</button>
  <button class="btn">2</button>

  <script>
    // 1、在ES6之前,使用var实现
    var btns = document.querySelectorAll(".btn");
    for (var i = 0; i < btns.length; i++) {
      btns[i].addEventListener(
        "click",
        function () {
          console.log(i);
        },
        false
      );
    }
  </script>
</body>

image-20221008144915035

运行以上程序,分别点击 0,1,2 三个按钮都会输出 3 ,而不是按我们想象的 0,1,2 来输出,我们通过画图来分析作用域来看下为什么 ?

image-20221008143415045

# 2、在 ES6 之前,我们该如何解决这个问题 ?

<head>
  <style>
    body {
      padding: 100px 0 0 200px;
    }
    .btn {
      width: 50px;
      height: 50px;
      font-size: 30px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <button class="btn">0</button>
  <button class="btn">1</button>
  <button class="btn">2</button>

  <script>
    // 2、在ES6之前,使用闭包来解决此类问题
    var btns = document.querySelectorAll(".btn");
    for (var i = 0; i < btns.length; i++) {
      // 在for循环中,不直接给每个按钮绑定事件处理函数,先声明一个立即执行的匿名函数
      (function (index) {
        btns[index].addEventListener(
          "click",
          function () {
            console.log(index);
          },
          false
        );
      })(i);
    }
  </script>
</body>

image-20221008184951273

运行以上代码,分别点击 0,1,2 三个按钮会正确输出 0,1,2

我们通过画图分析的方式来解释一下

  • 首先我们可以看到声明的立即执行函数会在 for 循环中循环执行 3 次
  • for 循环中的 var 声明的变量 i 是全局变量,当循环 3 次后 i 在全局作用域中 依然还是 i = 3
  • 当 for 循环 3 次,其中的立即执行的匿名函数(function(index){ ... })(i); 就调用了 3 遍,调用函数就会创建函数作用域,这样就会创建 3 个函数作用域。并且每一个函数作用域中就会有一个 index
  • 当点击 0,1,2 三个按钮时,就会调用事件处理函数对应的内部 function(){console.log(index);} 函数,这时就会形成自己的函数作用域
  • 开始打印输出 console.log(index); 这是就会一层层的往外找,找到立即执行匿名函数作用域中的index 找到了,就打印输出 0,1,2 ,这时候就不会再到外层全局作用域中去找 i = 3

这时候,你会发现这里的实现方式和第一种方式都是通过作用域链的查找机制来查找变量,但唯一的区别在于,这里通过 IIFE 立即执行函数形成了闭包,将全局变量转换为了局部变量来实现。

# 3、实现方式二:使用 ES6 中的 let、const 完成此需求

<head>
  <style>
    body {
      padding: 100px 0 0 200px;
    }
    .btn {
      width: 50px;
      height: 50px;
      font-size: 30px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <button class="btn">0</button>
  <button class="btn">1</button>
  <button class="btn">2</button>

  <script>
    // 3、使用ES6中的 let、const 完成此需求,本质和第一种方式代码一样,唯一不同的是将var改成let或const了
    // 主要就是用到了 let 或 const 能形成块级作用域的特性
    const btns = document.querySelectorAll(".btn");
    for (let i = 0; i < btns.length; i++) {
      btns[i].addEventListener(
        "click",
        function () {
          console.log(i);
        },
        false
      );
    }
  </script>
</body>

代码解读

同样,通过画图分析来解读程序的执行过程。直接使用 ES6 中的 let 或 const 就不用再使用闭包来实现了。

  • 首先,for 循环 3 次,在这个过程中作用域是什么情况呢
  • 最外层是有一个全局作用域的,可以看到 let 和 for 循环形成了块级作用域 和 之前的 var 不同,这时 i 就是块级作用域中的一个变量(是局部的),在全局作用域中就没有 i
  • for 循环,循环了 3 次,就创建了 3 个块级作用域,每循环一次就创建一个块级作用域,用 3 个来表示
  • i = 0 创建第一个块级作用域,i = 1 创建第二个块级作用域,i = 2 创建第三个块级作用域
  • 当点击 0,1,2 三个按钮时,就会调用事件处理函数对应的内部 function(){console.log(i);} 函数,这时就会形成自己的函数作用域
  • 开始打印输出 console.log(index); 这是就会一层层的往外找,在当前函数作用域中查找,没有 i 继续往外找,找到块级作用域中的局部变量 i ,因此就正确输出了 0,1,2

image-20221008222618556

只要我们分析清楚作用域之间的嵌套关系,再加上我们对作用域链查找机制的理解,最后沿着链条往外找就 OK 了

# 五、总结

TIP

总结本章重难点知识,理清思路,把握重难点。并能轻松回答以下问题,说明自己就真正的掌握了。

用于故而知新,快速复习。

# 1、ES 标准相关,ES6 简介及相关历史

TIP

这个部分作为了解和开阔视野,深入了解我们日常开发中遵循的标准都是怎么来的。

# 2、let 和 const 是什么 ?

TIP

  • let 声明变量的一个关键字
  • const 声明常量的一个关键字
    • const 声明的是常量
    • const 的设计初衷就是:为了那些一旦初始化就不希望重新赋值的情况而设计的
    • const 声明后必须立即初始化,常量的声明和初始化是一气呵成的
    • const 声明的常量可以修改,但不能重新赋值。比如:引用数据类型就可以做到,基本数据类型无法做到
  • 尽量使用 let 去代替 var 来声明变量。

# 3、let、const 与 var 的区别 ?

TIP

  • 重复声明:let、const 不允许重复声明,var 允许重复声明
  • 变量提升:let、const 不允许变量提升,var 允许变量提升
  • 暂时性死区:let、const 拥有暂时性死区,var 没有
  • window 对象的属性和方法(全局作用域中):
    • var 声明的变量 或 function 声明的函数,会自动变成 window 对象的属性或方法。
    • let、const 声明的变量 或 function 声明的函数,不会自动变成 window 对象的属性或方法
  • 块级作用域:let、const 可以形成块级作用域,var 没有块级作用域。(这点最重要)

学会分析程序中有哪些作用域,作用域之间的关系是怎样的,我们查找变量和常量的时候,作用域、作用域链的机制什么时候起作用,理解了这些才算真正理解了块级作用域

  • 有哪些块级作用域:{}for(){}while(){}do{}while()if(){}switch(){}

image-20221007231003236

自己能画出作用域嵌套的关系图,作用域链的方向

# 六、测试题

TIP

自我测试:在不看答案的前提下,看看自己是否真正掌握了本节所学内容。

# 1、关于以下描述正确的选项是 ?

  • A、let 是替代 var 用来声明变量的关键字
  • B、const 是替代 var 用来声明常量的关键字
  • C、变量声明之后,可以重新赋值
  • D、常量声明之后,不可以被重新赋值
自己先分析,再点击查看正确答案

正确答案:A C D

答案解析:本题主要是考查常量和变量的概念。ES6 中新增的 const 关键字是用来声明常量,并不是用来替代 var 的,B 选项描述是错的。

# 2、以下代码中,可以正常输出结果的是 ?

A、

const age;
age = 18;
console.log(age);

B、

const age = 18;
age = 20;
console.log(age);

C、

const obj = {
  age: 18,
};
obj.age = 20;
console.log(obj);

D、

const obj = {
  age: 18,
};
obj = {};
conosle.log(obj);
自己先分析,再点击查看正确答案

正确答案:C

答案解析:本题主要考查 const 的使用方式

  • const 声明常量初始化时必须赋值,否则会报错,A 选项错误。
  • const 声明常量初始化后,不可以再重新赋值,B、D 选项错误。
  • const 声明常量为引用数据类型,不可以重新赋值,但可以修改里面的值。C 选项正确。

# 3、以下代码运行的结果是 ?

let i = 2;
{
  console.log(i);
  let i = 3;
}
  • A、2
  • B、3
  • C、undefined
  • D、报错
自己先分析,再点击查看正确答案

正确答案:D

答案解析:本题主要考查暂时性死区

let 声明的变量存在暂时性死区,暂时性死区的本质就是:在当前作用域,所要使用的变量已经存在,不会再访问该作用域以外的同名变量,并且只有在声明变量之后,才可以获取和使用该变量,否则就会报错。

本题中:在块级作用域中使用 let 声明变量 i ,形成了暂时性死区,导致无法访问全局作用域中的同名变量 i ,且输出语句是在声明变量的前面,会出现报错。

# 4、以下代码中,在 ES6 中属于块级作用域的是 ?

A、

{
  let i = 1;
}

B、

let foo = function () {};

C、

const obj = {
    age = 18
}

D、

for (let i = 0; i < 5; i++) {
  console.log(i);
}
自己先分析,再点击查看正确答案

正确答案:A D

答案解析:本题考查 es6 块级作用域的基本概念

块级作用域是 ES6 中新增的一个作用域,指在大括号{}里面使用 let 或 const 关键字声明变量或常量,就会形成一个块级作用域。但有两个需要特殊记忆,函数和对象的大括号{}不属于块级作用域。

B 选项中,是表达式声明了一个函数,C 选项中是声明了一个对象,都不属于块级作用域。

# 5、以下代码中,访问常量 m 时存在的作用域链,描述正确的是(单选) ?

选择一项

const m = 88;
function func() {
  for (let i = 0; i < 5; i++) {
    if (i == 3) {
      let b = 6;
      bar(m);
    }
  }
}
function bar(tmp) {
  console.log(tmp);
}
func();
  • A、函数作用域 -> 块级作用域 –> 函数作用域 –> 全局作用域
  • B、块级作用域 –> 函数作用域 –> 全局作用域
  • C、函数作用域 -> 块级作用域 –> 块级作用域 –> 函数作用域 –> 全局作用域
  • D、块级作用域 –> 块级作用域 –> 函数作用域 –> 全局作用域
自己先分析,再点击查看正确答案

正确答案:D

答案解析:本题主要考查作用域和作用域链的知识

  • 作用域就是代码的执行环境,如:全局执行环境就是全局作用域
  • 访问一个变量/常量的值,若在当前作用域中没有查到,就会向上级作用域中查找,一直查找到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

在本题中,当执行到 bar(m)时,会先对 m 右查询,找到 m 对应的值。在找 m 值时,先到 if 块级作用域中找,然后再到 for 块级作用域中查找,最后在 func 函数作用域中找,都没找到,最后找到全局作用域中的 m

在这个过程中形成的作用域链是: if 块级作用域 –> for 块级作用域 –> func 函数作用域 –> window 全局作用域

上次更新时间: 6/8/2023, 9:23:17 PM

大厂最新技术学习分享群

大厂最新技术学习分享群

微信扫一扫进群,获取资料

X