第二章:块级作用域声明: let 和 const
变量声明
对于现在的 JavaScript
来说,声明变量的方式一共有三种:
var
:变量,会跳出块级作用域let
:变量,不会跳出块级作用域const
:常量,不会跳出块级作用域
块级作用域
什么是块级作用域?
所谓块级作用域指的是 两个大括号中间的内容,比如
for 循环、if、函数
只要存在{}
那么都会生成块级作用域。
除了块级作用域之外,var
和 let、const
在特性上也有一些区别。这个区别主要体现在两个方面 变量提升、暂时性死区(TDZ
)
变量提升与暂时性死区
咱们先来看变量提升。对于 var
声明的变量而言,会存在变量提升的概念,也就是可以 先使用、后定义,咱么来看这段代码:
console.log(msg) // undefined (并不会报错)
var msg = 'hello world'
在这段代码中,msg
变量先被使用,后声明。虽然打印了 undefined
,但是它并不会报错。
原因是因为,以上代码会被编译为以下形式:
var msg
console.log(msg) // undefined
msg = 'hello world'
即 msg 变量的定义会被提升到最前面。而这种形式就叫做 变量提升。
但是如果我们使用 let 或 const
来代替 var
的话,因为 let、const
不具备变量提升,所以就会抛出对应的错误:
console.log(msg2); // Uncaught ReferenceError: Cannot access 'msg2' before initialization
let msg2 = 'hello word'
而这样的错误,就被叫做 暂时性死区( temporal dead zone,简称TDZ )。
旧习换新
这里的旧习换新主要包含两点:
- 第一点是 不要使用
var
,改用let
和const
:因为无论是 跳出块级作用域也好,还是变量提升也好,在标准图灵完备的编程语言中,都不是一个应该具备的特性。 - 第二点是 缩小变量的作用域,从而提升可维护性:想要理解这句话,可能需要具备一定的编程经验。如果大家不是很理解的话,那么可以想象一下 一万行代码的文件和一百行代码的文件 哪个更好维护?我们始终需要谨记 代码越少,越容易维护。所以 缩小你的作用域空间,减少逻辑的复杂度。
第三章:函数的新特性
函数作为 JavaScript 世界
的 一等公民,是我们在实际项目开发中,无时无刻不在使用的东西。
在这一章中,咱们主要从 参数、this 指向、构造函数 这三个方面来去说明函数的新特性。
参数
函数的参数分为两种 形参、实参。所谓形参指的是 定义函数时指定的形式参数。所谓实参指的是 调用函数时,传递的实际参数。
而在定义形参时,我们可以通过 赋值符 = 的形式,为形参指定 默认值。这表示 如果没有传递对应的实参,则该形参默认为该值
function fn(name = '张三') {
console.log(name); // 张三
}
fn()
这个默认值可以为 任意的单一表达式,比如我们可以指定一个 立即执行的箭头函数,那么此时默认值会为该函数的值:
// 箭头函数:() => '李四'
// (() => '李四')() 表示立即执行的箭头函数
function fn(name = (() => '李四')()) {
console.log(name);
}
fn()
在 JavaScript
中,函数的实参和形参并不要求是一一对应的。也就是说 实参的数量可以超过形参的数量。那么在这种情况下,如果我们想要获取到 多余的实参,一共有两种方式:
-
第一种是传统的
arguments
,但是它并不是通用的,在箭头函数中无法使用function fn(name) { console.log(arguments); // ['张三', 30, '男', callee: ƒ, Symbol(Symbol.iterator): ƒ] } const fn = (name) => { console.log(arguments); // Uncaught ReferenceError: arguments is not defined } fn('张三', 30, '男')
-
第二种是
ES6 之后
新增的 “rest” 参数,它是通用的,表示 接收所有的剩余参数function fn(name, ...args) { console.log(args); // [30, '男'] } const fn = (name, ...args) => { console.log(args); // [30, '男'] } fn('张三', 30, '男')
this 指向
针对 this
指向,指的是 普通函数和箭头函数 下的 this
指向问题。
这应该是一个 面试 时的高频问点。当大家遇到这样的问题时,大多数时候只需要从三个方面进行回答即可:
-
首先第一个方面是 普通函数的 this 指向:针对于普通函数而言,
this
指向调用方function fn() { console.log(this); // window } fn() // window.fn()
-
然后是箭头函数:针对于箭头函数而言,不会修改
this
指向,即 this 指向上层作用域中的 thisconst person = { name: '张三', fn() { console.log(this); // person const subFn = () => { console.log(this); // 指向上层作用域(fn)中的 this } subFn() } } person.fn()
-
最后是
call、apply、bind
这三个API
:它们都可以在 普通函数 中修改this
指向,this
指向它们的第一个参数const person = { name: '张三' } const fn = () => { console.log(this); // window 箭头函数永远不会修改 this 指向 } function fn2() { console.log(this); // person } fn.apply(person) fn2.call(person) fn.bind(person)()
构造函数
构造函数通常指 首字母大写的普通函数 。也就是说:箭头函数永远不可以作为构造函数使用
function Person(name) {
// this 指向实例对象
this.name = name
}
const p = new Person('张三')
console.log(p); // Person {name: '张三'}
const Person2 = (name) => {
// this 指向 window
this.name = name
}
const p2 = new Person2('张三')
console.log(p2); // Uncaught TypeError: Person2 is not a constructor
旧习换新
这一章的旧习换新主要包含四点内容,也是对本章重点内容的总结:
- 首选是关于箭头函数和普通函数的使用场景: 不想修改 this 指向时,使用箭头函数。需要改变 this 的指向时,使用普通函数
- 其次是关于参数默认值:使用参数默认值,而不要使用代码为参数赋初始值
- 第三是关于剩余参数: 使用 rest 参数替代 arguments 关键字 来获取剩余参数
第四章:类
针对于第四章而言,从名字到内容都非常的纯粹,一个字 类。
那么对于这一章的内容,让我们从一个问题开始:我们常说 JavaScript 实际上没有类,只是用原型来模拟了类?是这样的吗?
答案是 当然不是。其实从 ES2015
之后,ECMAScript
标准为 JavaScript
提供了 类 的概念。它并不是原型的模拟,只是 可以用原型来模拟类而已。
那么下面咱们就来看看 ES2015
之后的 类语法
类语法
类语法分为 创建 和 使用 两部分。
咱们先来看类的创建:
const fnName = 'fn' + Math.floor(Math.random() * 1000)
class Color {
// 一个《构造函数》
constructor(r = 0, g = 0, b = 0) {
// 三个《数据属性》
this.r = r
this.g = g
this.b = b
}
// 一个《访问器属性》
get rgb() {
// 可通过 实例.rgb 访问
return `rgb(${this.r}, ${this.g}, ${this.b})`
}
set rgb(val) {
// 为 r、g、b 赋值
// 可通过 实例.rgb = xx 访问
}
// 一个《原型方法》
toString() {
return `重写的原型方法:${this.rgb}`
}
// 一个静态方法
static fromCss(r, g, b) {
// 利用 new this 可以直接得到 Color 实例
return new this(r, g, b)
}
// 动态方法名
[fnName]() {
return `动态方法名为:${fnName}`
}
}
在这段代码中,我们通过类语法 class
创建了一个类 Color
,这里大家注意,根据规范 类名首字母应该大写。这里的代码我已经写好了注释,大家可以在这里暂停来查看下对应的代码内容。
而如果想要使用类的话,那么必须要通过 new
关键字来进行使用:
const c = new Color(30, 144, 255)
console.log(c[fnName]()); // 动态方法名为:fn275
console.log(c.toString()); // 重写的原型方法:rgb(30, 144, 255)
console.log(c.rgb); // rgb(30, 144, 255)
console.log(Color.fromCss(255, 255, 255)); // Color {r: 255, g: 255, b: 255}
类继承
继承在编程语言中是一个非常常见的概念,在 ES6
之前想要完成继承,那么多数情况下需要使用 原型继承 的方式。而原型继承有很多种,比如:组合式继承、原型式继承、寄生式继承、寄生式组合继承 … 很多种方式。
但是在实际开发中,如果我们直接使用类语法的话,那么想要实现继承就非常容易了。只需要使用到一个关键字 extends
class SubColor extends Color {}
super 关键字
而除了 extends
之外,类继承的时候还有另外一个关键字 super
。 super
关键字可以用来 处理与父类相关的事情。
它的使用场景主要有两个:
作为函数使用
super
关键字可以直接作为函数进行使用。比如:在构造函数中使用时,super
可以直接调用父类的构造函数,在通常情况下 这是一个必须的操作
class SubColor extends Color {
constructor(r = 0, g = 0, b = 0, a = 1) {
// 触发父类的构造函数
super(r, g, b)
this.a = a
}
}
作为属性查询使用
super
关键字可以用来 访问一个对象字面或类的 [[Prototype]] 的方法和属性。比如:我们可以在静态方法中利用 super
访问父类的静态方法
class SubColor extends Color {
static fromCss(r, g, b, a = 1) {
// 通过 super 调用父类的静态方法
const result = super.fromCss(r, g, b)
// code....
return new this(r, g, b, a)
}
}
// 子类重写的 formCss。result = {color: red} + {fontSize: 20px}
console.log(Color.fromCss(255, 255, 255));
console.log(SubColor.fromCss(255, 255, 255, 1)); // SubColor {r: 255, g: 255, b: 255, a: 1}
new.target
对于类而言,最后一个需要大家关注的概念就是 new.target
。
new.target
属性允许你 检测函数或构造方法是否是通过 new 运算符被调用的,并且可以返回一个指向构造方法或函数的引用。
我们可以利用它来判断 当前触发构造函数时是通过哪个类来触发的 ,这在 多层继承判断来源时会非常有用
class Color {
constructor() {
console.log(`new.target.name: ${new.target.name}`);
}
}
class SubColor extends Color {
constructor() {
super()
}
}
new Color() // new.target: 指向 Color
new SubColor() // new.target: 指向 SubColor
旧习换新
这里的旧习换新环节,就比较简单了,只有一点: 实际开发中,通过 class
来完成类的构建和继承。
第五章:对象的新特性
接下来我们来看对象在 ES6
之后的新特性。
对象在我们日常开发中使用的场景是非常多的,所以这一章中的很多新特性大家或多或少的应该都有一些了解。我挑选了几个日常开发中最常用的语法,来给大家进行下分享。
首先是 可计算的属性名。
有些时候,我们可能希望 对象的 key
是一个不确定的唯一值。 比如:世界上每一个人都是唯一的,所以 person
对象应该具备一个唯一的 “特性”
,那么我们就可以通过这种方式来进行表示
const key = Symbol('key')
const person = {
name: '张三',
// 可计算的属性名
[key]: key
}
console.log(person); // {name: '张三', Symbol(key): Symbol(key)}
在这段代码中,我们利用 Symbol
构建了一个 key
,然后利用 [key]
作为 person
的唯一 key 名
。
同时,为了方便对象字面量的编写,ES6
之后提供了 属性简写 的语法: 当 key 和 value 拥有同样的变量名时,那么可以进行简写
const name = '张三'
const person = {
// name: name
name
}
属性简写 是我们在日常开发中非常常用的一种方式。
除了属性简写之后,还有另外一个新特性也是我们在日常开发中非常常见的,那就是 展开运算符。
展开运算符以 ...
的形式进行表示,可以用在对象的展开和合并的多个场景中:
const names = ['张三', '李四', '王五']
// 展开
console.log(...names); // 张三 李四 王五
// 合并
console.log(['赵六', ...names]); //['赵六', '张三', '李四', '王五']
旧习换新
最后是对象的旧习换新环节,这个环节的内容比较多,主要有 5 个:
- 当你需要一个动态的
key
时,可以通过可计算的属性名直接创建该对象 - 多使用属性的简写,以此来简化对象构建的过程
- 是
Object.assign
方法,这是一个ES6
新增的方法。可以 将一个对象的可枚举属性复制到另一个对象上 。但是要注意,这是一个浅拷贝的 Symbol
可以构建一个唯一值。 使用Symbol
作为key
名,可以避免属性名冲突- 最后是关于实例的原型,之前访问实例的原型时多通过
__proto__
访问。现在可以通过Object.setPrototypeOf、Object.getPrototypeOf
来直接访问原型
第六章:可迭代对象、迭代器、生成器
从这一章的名字就可以看出来,这一章中主要讲了三个东西 可迭代对象、迭代器、生成器
。本章的内容在我们日常的业务项目开发中其实用的不是特别多,并且很多时候有更习惯的替代方案。但是在面试中,确有可能经常被问到,所以不妨一听。
可迭代对象、迭代器
首先咱们先来看可迭代对象、迭代器。想要了解这两个东西,咱们需要先搞清楚他们的概念:
迭代器:所谓迭代器指的是 一个具有 next 方法的对象。也就是说,从 理论上,只要一个对象具备
next
方法,那么它就是迭代器。这里大家注意:迭代器可以应用在数组中,却 不可以 应用在普通对象中可迭代对象:而可迭代对象指的是 可以通过标准方法获取迭代器,以遍历其内容的对象。
所以说 可迭代对象、迭代器 通常是配合来进行说明的。
而对于迭代器而言,分为 隐式迭代器 和 显示迭代器 两种。
咱们先来看隐式迭代器 for of
,它拥有 隐式 的 next
方法:
const names = ['张三', '李四', '王五']
for (const iterator of names) {
console.log(iterator); // 张三、李四、王五
}
const person = {
name: '张三',
age: 30
}
// Uncaught TypeError: person is not iterable
// 普通对象默认不可迭代
for (const iterator of person) {
console.log(iterator);
}
而显示迭代器被叫做 Symbol.iterator
,每个数组都包含一个 Symbol.iterator
的属性,可以利用该属性获取显示迭代器,它拥有 显示 的 next
方法:
const names = ['张三', '李四', '王五']
const it = names[Symbol.iterator]()
console.log(it); // Array Iterator {}
console.log(it.next()); // {value: '张三', done: false}
console.log(it.next()); // {value: '李四', done: false}
console.log(it.next()); // {value: '王五', done: false}
console.log(it.next()); // {value: undefined, done: true}
// const person = {
// name: '张三',
// age: 30
// }
// // Uncaught TypeError: person is not iterable
// // 普通对象默认不可迭代
// for (const iterator of person) {
// console.log(iterator);
// }
而针对于对象而言,咱们也说过 普通对象默认不可迭代。不可迭代的原因其实是因为 缺少 Symbol.iterator 属性。所以如果我们希望让普通对象可迭代的话,那么可以通过以下两步来完成:
- 为对象添加
Symbol.iterator
属性,返回iterator
迭代器对象 iterator
迭代器对象中包含next
方法
因为在实际开发中使用场景不多,所以其中具体的代码咱们就不在这里说了。
生成器
虽然迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数,同时它可以自动维护自己的状态。所以我们可以 利用生成器得到一个迭代器对象(Generator
符合 可迭代协议 和 迭代器协议),同时该迭代器对象同样会拥有 next
方法。
那么咱们搞明白 生成器与迭代器 的关系之后,下面咱们来看下 生成器 的语法。
想要创建并使用生成器的话,一共分为三步:
-
生成器被称为 生成器函数,所以想要构建一个生成器,那么必须要构建一个函数:
// 通过在 function 后面增加一个 * 来标记当前函数为生成器函数 function* simple() { }
-
生成器内部包含 暂停迭代 的功能,这个暂停是配合
next
方法进行使用的function* simple() { for (let i = 0; i < 3; i++) { // 使用 yield 控制暂停迭代 yield console.log(i); } }
-
调用生成器函数,可以得到一个迭代器对象,通过
next
方法控制 迭代过程const s = simple() s.next() // 0 s.next() // 1 s.next() // 2
同时对于生成器而言,它还可以 传递参数(消费值),咱们来看这个例子:
function* add() {
console.log('开始');
// yield 后面的内容被叫做 value,并且 yield 包含返回值
const value1 = yield "请输入第一次的值"
console.log(`第一次的值为:${value1}`);
const value2 = yield "请输入第二次的值"
console.log(`第二次的值为:${value2}`);
return value1 + value2
}
let result
const gen = add()
// 开始
result = gen.next()
console.log(result);
// 第一次输入值
result = gen.next(35)
console.log(result);
// 第二次输入值
result = gen.next(7)
console.log(result);
旧习换新
就像我们开始所说的一样,在实际企业开发中生成器与自定义迭代器其实使用率并不高,所以这里的旧习换新只有一个:
- 利用 DOM 的可迭代特性,通过 for…of… 进行循环。比如在通过
querySelectAll
获取DOM
的伪数组之后,伪数组虽然没有办法直接forEach
,但是可以利用隐式迭代器for of
完成循环操作
第七章:解构
关于对象相关的最后一部分就是 解构 了。解构应该是日常开发中非常常用的语法。其目的是为了 从数据结构中快速提取对应的内容
解构分为两部分:对象解构 和 数组解构。
对象解构
当对对象进行解构时,需要 配合 大括号 完成,大括号中放入需要提取的字段名,同时该字段名会作为新的变量名被创建
const person = {
name: '张三',
age: 30
}
// 以大括号的形式来结构对象
// 以 key 的形式,获取指定属性
const { name } = person // const name = person.name
console.log(name); // 张三
而当 对象呈嵌套形式时,同样可以利用 嵌套解构 的形式进行结构:
const person = {
name: '曹操',
age: 58,
children: [
{
name: '曹丕',
age: 35
},
{
name: '曹植',
age: 28
}
]
}
// { children } 表示获取 person 的 children
// { children: [caoPi] } 表示从 children 中获取第一个元素,命名为 caoPi
const { children: [caoPi] } = person
console.log(caoPi); // {name: '曹丕', age: 35}
数组解构
而对于数组的解构,需要 **配合 中括号 完成,**中括号中 按照下标的顺序,依次写入新的变量名
const arr = ['a', 'b', 'c']
// 以中括号的形式来结构对象
// 中括号定义变量名
const [v1, v2, v3] = arr // const v1 = arr[0], v2 = arr[1], v3 = arr[2]
console.log(v1, v2, v3); // 'a', 'b', 'c'
同时,对于数组解构而言,也可以通过 “rest” 语法
直接获取 剩余元素,剩余元素会被赋值给新的数组
const arr = ['a', 'b', 'c']
// 以中括号的形式来结构对象
// 中括号定义变量名
// “rest” 语法... 表示剩余所有组成新数组
const [v1, ...v2] = arr // const v1 = arr[0], v2 = [arr[1], arr[2]]
console.log(v1, v2); // 'a', ['b', 'c']
旧习换新
解构是日常开发中常用的语法,但是本身比较简单。只要大家在以后的开发中多使用解构语法,就会发现它本质上是一个非常简单的东西。
第八章:Promise
接下来咱们来看第八章、第九章关于异步处理的部分。
说道异步,肯定有很多小伙伴直接想到的就是 Promise
。 没有 Promise
是 ES6
之后专门用来处理异步的解决方案。但是要注意:
Promise
本身并不执行任何操作,它只是一种观察异步操作结果的方案。
在 Promise
内部对整个异步的操作分为 三种状态,对应 三种结果。而语法分为 定义 和 使用 两部分。其中三种状态发生在定义阶段,三种结果发生在使用阶段。
function reload(b) {
// 创建 promise 实例
return new Promise((resolve, reject) => {
console.log('代码进入 pending 状态');
setTimeout(() => {
if (b) {
resolve('代码进入 已成功 状态')
} else {
reject('代码进入 已拒绝 状态')
}
}, 500);
})
}
const p1 = reload(true)
p1.then((data) => {
console.log(data); // 代码进入 已成功 状态
}).finally(() => {
console.log('p1 已敲定');
})
const p2 = reload(false)
p2.then((data) => {
console.log(data); // 代码进入 已拒绝 状态
}).finally(() => {
console.log('p2已敲定');
})
同时,对于 Promise
而言,它支持链式调用的方式。只要在 .then
中 return
了内容,那么 return
的内容就会被封装为 Promise.resolve
,从而可以继续 .then
:
const p1 = reload(true)
p1.then((data) => {
console.log(data); // 代码进入 已成功 状态
return '进入第二次 Promise'
}).then(data => {
console.log(data);
return '进入第三次 Promise'
}).then(data => {
console.log(data);
console.log('三次结束');
})
旧习换新
对于 Promise
来说,现在在开发中的使用已经非常普遍了。所以当大家以后遇到异步问题时,应该首先考虑 Promise
第九章:异步函数、迭代器、生成器
Promise
可以帮助我们处理异步操作,但是从上面的代码我们可以看出,在 Promise
的 使用 阶段,代码的复杂度其实并不低。
所以在 ES7
之后,TC39
推出了 异步函数 的概念,以解决 Promise
使用的复杂度问题。
想要定义异步函数,那么需要通过 async
关键字来进行定义,在异步函数中,可以通过 await
关键字来 让异步操作,变为同步的写法
function reload1() {
// 创建 promise 实例
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('reload1 进入 已成功 状态')
}, 500);
})
}
function reload2() {
// 创建 promise 实例
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('reload2 进入 已成功 状态')
}, 500);
})
}
function reload3() {
// 创建 promise 实例
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('reload3 进入 已成功 状态')
}, 500);
})
}
// async 和 await 必须同时出现
// async 标记指定方法为《异步方法》,该方法会返回一个 Promise
// await 只能在《异步方法》中使用
async function start() {
// 该操作同样为异步操作,只不过拥有了同步的写法
const result1 = await reload1()
console.log(result1);
const result2 = await reload2()
console.log(result2);
const result3 = await reload3()
console.log(result3);
return 'start 返回值'
}
const p = start()
console.log(p); // promise
在使用 async/await
时,默认情况下返回值是 .then
的内容,如果想要捕获异常,那么需要通过 try...catch...
来完成。
同时对于异步函数来说,也可以配合生成器进行使用,得到 异步生成器函数。这种异步生成器,在需要 手动控制多个异步请求的顺序以及结果的时候,会有些用处,如果大家没有这方面的需求,只需要有个印象就可以了。
// async 函数必然返回 promise
async function* fetchInSeries([...urls]) {
for (const url of urls) {
const res = await fetch(url)
// g.next().value 得到 res.json()
yield res.json()
}
}
async function getData() {
const g = fetchInSeries(['1.json', '2.json', '3.json'])
/*
let result = await g.next()
while (!result.done) {
console.log(result.value);
result = await g.next()
}
*/
let result
while (!(result = await g.next()).done) {
console.log(result.value);
}
}
getData()
在以上代码的 getData()
方法中,我们通过 fetchInSeries
方法得到了一个迭代器 g
,然后利用 while
循环的方式进行了迭代。
而整个迭代的过程可以进一步的简写,利用 for await of
语法,会更加轻松
async function getData() {
// for await of:利用迭代器的特性配合 await 解析生成器
for await (const value of fetchInSeries(['1.json', '2.json', '3.json'])) {
console.log(value);
}
}
旧习换新
使用 async、await
配合 promise
处理异步请求,是在当前阶段开发中非常常见的一种场景。当大家遇到异步请求时,那么首先应该想到的就是 Promise + async\await
。
第十章:模板字面量、标签函数和新的字符串特性
从第十章开始,到第十四章为止,这五个章节主要是对传统技术的升级。
以第十章为例,传统技术下,想要拼接字符串与变量,那么一般需要通过 + 运算符 进行隐式转换。而 ES6
之后,提供了 模板字符串 的概念:
const msg = 'world'
console.log(`hello ${msg}`); // hello world
处理之外,还提供了一些新的字符串方法。
以及通过 for of
隐式迭代器对字符串进行迭代操作。
旧习换新
这一章的内容中,对我们日常开发和面试有用的内容其实比较少,里面还包含一些对我们没有什么用处的东西,比如 Unicode 的改进
,这些我就略过了,如果大家对 Unicode
感兴趣的话,可以去看一下书中的内容。
第十一章:新数组特性、类型化数组
对于第十一章来说,和第十章其实非常的类似。第十一章主要讲解的是 Array 数组的改进
。内容主要就是两部分。
第一部分是对于 Array
数组的新增方法。
第二部分是 类型化数组。
对于很多小伙伴来说,类型化数组的概念大家可能是第一次听到。但是大家应该知道 JavaScript 中传统的 “数组” 并不是真正的数组,而是一个对象。 所以在 ES6
之后,TC39
提出了一个新的概念就是 类型化数组,不同的类型化数组可以存储不同的值,但是在日常的使用中,我们可能很少会主动使用这个东西。
旧习换新
数组的新增方法,在日常开发中是非常有用的。所以以上我们所列举出来的方法大家一定去 MDN
上看一下,至少做到有个印象。
第十二章:Map 和 Set
上面两章的内容相对都比较简单。但是接下来的 Map
和 Set
可能对于很多小伙伴来说是 “盲区”。
这一章一共讲到了 4个
新的接口:Map、Set、WeakMap、WeakSet
。
咱么先来看 Map
。Map
是 以 键\值对 的形式存储数据的对象,其中键值对可以是任何值。乍一看可能和 普通对象 没有什么区别,但是大家需要注意的是 普通对象的 key 只能是 字符串。 而 Map 对象的 key 可以是任意值。
也就是说虽然存储方式相同,但是可存储的内容确实大大不同的。
除此之外,在 API
上,Map
和 {}
也有较大的区别。我们可以来看下关于 Map
对象的基本语法:
// 创建
const m = new Map()
// 增
m.set('name', '张三')
m.set('age', '30')
// 改
m.set('name', '李四')
// 查
m.get('name')
// 删
m.delete('name')
// 获取长度
console.log(m.size);
// 迭代
for (const [key, value] of m) {
console.log(key, value);
}
而对于 Set
而言,它与数组有些类似,但是不同的地方在于 Set
存储唯一值,也就是元素不可以重复。
同样它的语法和传统的 Array
也有较大的区别:
// 创建
const s = new Set()
// 增
s.add('张三').add('李四')
// 是否包含
s.has('张三')
// 删
s.delete('张三')
// 清空
s.clear()
// 获取长度
console.log(s.size);
// 迭代
for (const value of s) {
console.log(value);
}
而对于 Map
和 Set
来说,都提供了一个 Weak
版,叫做 WeakMap
和 WeakSet
。
WeakMap
和 WeakSet
与 Map
和 Set
的区别主要体现在三个地方:
- 首先第一个地方是 值为弱引用 ,这意味着它并不会影响垃圾回收。
- 同时因为是弱引用,所以 值不会保存在内存中,这个就比较好理解了。
- 最后就是 不可迭代。
旧习换新
最后就是旧习换新。
- 首先:如果你要以键值对的形式保存数据,同时
Key
非字符串,那么此时可以考虑使用Map
- 其次:
Set
对象可以存储唯一值,这在数组去重的场景下会非常有用 - 最后:如果你希望
key
跟随变量的销毁而被回收时,那么可以使用WeakMap
或WeakSet
第十三章:模块
第十三章主要讲解的就是模块化的概念。任何的模块化都会被分成两部分 导入 和 导出。
而目前在前端领域,主要的模块化方式有两个 ESM
和 CJS
。
所以说咱们在这一章中要学习的就是:
ESM
的导入、导出CJS
的导入、导出
一个四个环节。
那么首先我们先来看 ESM
。所谓 ESM
指的是 ES2015 之后新增的标准模块化方案 ES Module
。它相对复杂一点,分为 直接导出、按需导出、直接导入、按需导入 一共四部分。直达链接。
除了这标准的四部分之后,有时候在 EMS
中可能还会涉及到 导入同时导出 的场景,如果想要导入并导出,那么可以通过 export {xx} from './xx'
的方式进行。
第二个是 CJS
。CJS
的逻辑与 ESM
相同,甚至更加简单。只是语法上会稍有不同。CJS
不存在按需的概念,所以只有导入和导出两部分。文档直达链接
除了 CJS
和 EMS
之外,还有一些其他的模块化方案,比如 UMD
,AMD
、CMD
等等。这些模块化方案在应用层开发中很少见,所以咱们不做讨论。
旧习换新
最后就是旧习换新环节。
其实对于模块化的问题,在现在的企业开发中已经是非常常见的了。无论是 EMS
还是 CJS
都已经得到了广泛的应用。所以这一章的内容,对于大多数小伙伴来说,应该都没有太大的难度才对。
第十四章:反射和代理
看完模块化之后,下面咱们来看下反射和代理的概念。其中反射代表的是 Reflect
,代理代表的是 Proxy
。我第一次接触它们两个是在 Vue 3
中接触到的。
这两个 API
多数情况下应该是配合使用的,任意一个单独拿出来,在复杂场景中都意义不大。
所以说我们在介绍这两个 API
的时候会先介绍它们两个的作用,然后再把它们两个合起来去说。
那么咱们先来看 反射对象(Reflect)
。它 提供了《对象基本操作的各种方法,比如:获取和设置属性值、获取和设置对象的原型、从对象中删除属性…》 等等…
const person = {
name: '张三'
}
console.log(person.name) // 张三
console.log(Reflect.get(person, 'name')) // 张三
在这段代码中,我们可以利用 Reflect.get(person, 'name')
方法,获取到 person
对象下 name
属性的值。
但是肯定有很多小伙伴看到这就说了 这有啥用啊? 我们完全可以通过 person.name
来获取啊。
所以 Reflect
的真正有意义的使用,就需要配合 Proxy
进行了。
const person = {
name: '张三',
age: 30
}
const p = new Proxy(person, {
/**
* 代理 person 的 setter 行为
* @param {*} target person 被代理对象
* @param {*} key 修改时的 key
* @param {*} value 修改的 value
* @param {*} receiver proxy 实例 p,被代理对象
*/
set(target, key, value, receiver) {
console.log(target, key, value, receiver)
// 修改被代理对象
target[key] = value
// 标记修改成功
return true
},
/**
* 代理 person 的 getter 行为
* @param {*} target person 被代理对象
* @param {*} key 修改时的 key
* @param {*} receiver proxy 实例 p,被代理对象
* @returns
*/
get(target, key, receiver) {
return target[key]
}
})
p.name = '李四' // 触发 set。注意:只有修改 proxy 实例才会触发 set
console.log(p.name) // 触发 get。注意:只有通过 proxy 实例才会触发 get
在这段代码中,我们通过 Proxy
代理了 person
的 getter
行为和 setter
行为。从而可以监听到 person
的赋值操作和输出操作。
那么下面我们来看 Proxy
配合 Reflect
的场景,这个场景可能稍微有一些复杂,需要大家在搞明白 Proxy
和 Reflect
之后再查看:
const person = {
lastName: '张',
firstName: '三',
// 通过 get 标识符标记,可以让方法的调用像属性的调用一样
get fullName() {
return this.lastName + this.firstName
}
}
const proxy = new Proxy(person, {
get(target, key, receiver) {
console.log('触发了 getter')
// getter 行为本应触发三次,但是只触发了一次。这是因为 fullName 中的 this 指向了 target(person),而不是 proxy 实例
// return target[key]
// 正常触发三次
return Reflect.get(target, key, receiver)
}
})
console.log(proxy.fullName)
在上面的这段代码中,我们为 person
提供了一个 get fullName()
的属性方法,当 fullName
被触发时,它应该存在 三次 getter
行为,即: fullName、lastName、firstName。
但是如果我们在 get
监听中,使用 target[key]
的方式,那么这三次的 getter
行为只会被监听一次。原因是因为 fullName
中的 this
指向 person
,而不会指向 proxy 实例
。
所以我们需要通过 Reflect.get
来完成这个行为。 Reflect.get
的第三个参数可以控制 fullName
的 this
指向,使其指向 receiver
,也就是 proxy 实例
。
旧习换新
如果大家从事业务开发,那么 Proxy
和 Reflect
使用的场景应该并不多。而如果有一天,你需要监听某个对象的 getter
或者 setter
动作了,那么可以直接通过 Proxy
来完成。
第十五章:正则表达式
基本上每一本 JavaScript
的书籍都会包含一个 正则表达式 的环节,但是讲道理在日常的开发中 正则表达式 的使用不足以让我们花费大量的时间,同时书中的那些内容也不足以支撑复杂场景的 正则表达式 使用。
在本书的第十五章中,我摘抄出了 3 个新增的正则标记符,为大家提供参考。
第十六章:共享内存
作者在书中说道: 绝大多数的开发者并不需要在线程之间共享内存。 作者说的很对,所以我们这里不会花费篇章来说这个问题。如果你需要,那么可以看下书中讲解的内容。
第十七章:其他特性
第十七章的其他特性,主要呈现了一些单一、不成系统的特性。这些特性虽然大多都比较简单,但是有些在日常开发中还是比较有用的。
我从中抽出来了对我们日常开发有价值的新特性,咱们一起来看一下:
-
BigInt:
BigInt
主要用来处理大数字问题:// BigInt:处理大数字(大于 2^53 - 1 的整数)问题 const bigNum = 900719925474099596 console.log(bigNum); // 9867273627366328000 数值错误 // const bigIntObj = BigInt(900719925474099596n) const bigIntObj = 900719925474099596n // 加上 n 表示 bigint console.log(bigIntObj.toString()); // 900719925474099596
-
新的整数字面量语法(二进制、八进制):主要针对二进制、八进制
// 二进制 0b 开头 console.log(0b1000); // 8 // 八进制 0o 开头 console.log(0o17); // 15
-
省略 catch 绑定的异常
// 省略 catch 绑定的异常 try { JSON.parse('abc') } catch { console.log('不需要为 catch(err) {...}'); }
-
新的 Math 方法
-
取幂运算符
// 取幂运算符 ** console.log(2 ** 3); // 2 * 2 * 2 = 8
-
空值合并
// 空值合并 ?? const num = 0 // 0 是一个正常的数值,但是在 JavaScript 中 0 参与逻辑运算会被当做 “假” // 利用逻辑或的逻辑中断特性 console.log(num || 300); // 300 // 但是在程序中,有些时候 0 会被作为一个有意义的值,所以上面案例,我们去期望得到的是 0 // 利用空值合并:?? 前面的值只有为 null 或者 undefined 才会被认为 “假” console.log(num ?? 300); // 0
-
可选链
// 可选链 xx?.xx const person = { name: '张三' } // console.log(person.child.name); // child 为 undefined,所以报错 Cannot read properties of undefined (reading 'name') // 利用可选链,如果 ? 前面为 null 或者 undefined ,则逻辑短路 console.log(person.child?.name); // undefined
第十八章:即将推出的类特性
第十九章:展望未来
最后的十八和十九章,对我们现在而言意义就不大了。里面很多的特性现在并不支持,在实际开发中使用的场景也非常有限。
如果你对这些东西比较感兴趣的话,那么可以看一下。否则意义不大。
视频出处:【一小时读完《深入理解现代 JavaScript》,彻底掌握 ES6 之后 JavaScript 新特性!】 https://www.bilibili.com/video/BV1qD4y1G7YK/?share_source=copy_web&vd_source=a9f0fd4630ebe41da19ca2c83eb295e6
作者:LGD_Sunday