精读《深入理解现代JavaScript》


第二章:块级作用域声明: let 和 const

变量声明

对于现在的 JavaScript 来说,声明变量的方式一共有三种:

  • var:变量,会跳出块级作用域
  • let:变量,不会跳出块级作用域
  • const:常量,不会跳出块级作用域

块级作用域

什么是块级作用域?

所谓块级作用域指的是 两个大括号中间的内容,比如 for 循环、if、函数 只要存在 {} 那么都会生成块级作用域。

除了块级作用域之外,varlet、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,改用 letconst:因为无论是 跳出块级作用域也好,还是变量提升也好,在标准图灵完备的编程语言中,都不是一个应该具备的特性。
  • 第二点是 缩小变量的作用域,从而提升可维护性:想要理解这句话,可能需要具备一定的编程经验。如果大家不是很理解的话,那么可以想象一下 一万行代码的文件和一百行代码的文件 哪个更好维护?我们始终需要谨记 代码越少,越容易维护。所以 缩小你的作用域空间,减少逻辑的复杂度。

第三章:函数的新特性

函数作为 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 指向上层作用域中的 this

    const 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 之外,类继承的时候还有另外一个关键字 supersuper 关键字可以用来 处理与父类相关的事情

它的使用场景主要有两个:

作为函数使用

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 个:

  1. 当你需要一个动态的 key 时,可以通过可计算的属性名直接创建该对象
  2. 多使用属性的简写,以此来简化对象构建的过程
  3. Object.assign 方法,这是一个 ES6 新增的方法。可以 将一个对象的可枚举属性复制到另一个对象上 。但是要注意,这是一个浅拷贝的
  4. Symbol 可以构建一个唯一值。 使用 Symbol 作为 key 名,可以避免属性名冲突
  5. 最后是关于实例的原型,之前访问实例的原型时多通过 __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 方法

那么咱们搞明白 生成器与迭代器 的关系之后,下面咱们来看下 生成器 的语法。

想要创建并使用生成器的话,一共分为三步:

  1. 生成器被称为 生成器函数,所以想要构建一个生成器,那么必须要构建一个函数:

    // 通过在 function 后面增加一个 * 来标记当前函数为生成器函数
    function* simple() {
    }
  2. 生成器内部包含 暂停迭代 的功能,这个暂停是配合 next 方法进行使用的

    function* simple() {
      for (let i = 0; i < 3; i++) {
        // 使用 yield 控制暂停迭代
        yield console.log(i);
      }
    }
  3. 调用生成器函数,可以得到一个迭代器对象,通过 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);
image-20230625185609320

旧习换新

就像我们开始所说的一样,在实际企业开发中生成器与自定义迭代器其实使用率并不高,所以这里的旧习换新只有一个:

  • 利用 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。 没有 PromiseES6 之后专门用来处理异步的解决方案。但是要注意:

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 而言,它支持链式调用的方式。只要在 .thenreturn 了内容,那么 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

上面两章的内容相对都比较简单。但是接下来的 MapSet 可能对于很多小伙伴来说是 “盲区”

这一章一共讲到了 4个 新的接口:Map、Set、WeakMap、WeakSet

咱么先来看 MapMap以 键\值对 的形式存储数据的对象,其中键值对可以是任何值。乍一看可能和 普通对象 没有什么区别,但是大家需要注意的是 普通对象的 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);
}

而对于 MapSet 来说,都提供了一个 Weak 版,叫做 WeakMapWeakSet

WeakMapWeakSetMapSet 的区别主要体现在三个地方:

  • 首先第一个地方是 值为弱引用 ,这意味着它并不会影响垃圾回收。
  • 同时因为是弱引用,所以 值不会保存在内存中,这个就比较好理解了。
  • 最后就是 不可迭代

旧习换新

最后就是旧习换新。

  • 首先:如果你要以键值对的形式保存数据,同时 Key 非字符串,那么此时可以考虑使用 Map
  • 其次:Set 对象可以存储唯一值,这在数组去重的场景下会非常有用
  • 最后:如果你希望 key 跟随变量的销毁而被回收时,那么可以使用 WeakMapWeakSet

第十三章:模块

第十三章主要讲解的就是模块化的概念。任何的模块化都会被分成两部分 导入导出

而目前在前端领域,主要的模块化方式有两个 ESMCJS

所以说咱们在这一章中要学习的就是:

  • ESM 的导入、导出
  • CJS 的导入、导出

一个四个环节。

那么首先我们先来看 ESM。所谓 ESM 指的是 ES2015 之后新增的标准模块化方案 ES Module。它相对复杂一点,分为 直接导出按需导出直接导入按需导入 一共四部分。直达链接

除了这标准的四部分之后,有时候在 EMS 中可能还会涉及到 导入同时导出 的场景,如果想要导入并导出,那么可以通过 export {xx} from './xx' 的方式进行。

第二个是 CJSCJS 的逻辑与 ESM 相同,甚至更加简单。只是语法上会稍有不同。CJS 不存在按需的概念,所以只有导入和导出两部分。文档直达链接

除了 CJSEMS 之外,还有一些其他的模块化方案,比如 UMDAMDCMD 等等。这些模块化方案在应用层开发中很少见,所以咱们不做讨论。

旧习换新

最后就是旧习换新环节。

其实对于模块化的问题,在现在的企业开发中已经是非常常见的了。无论是 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 代理了 persongetter 行为和 setter 行为。从而可以监听到 person 的赋值操作和输出操作。

那么下面我们来看 Proxy 配合 Reflect 的场景,这个场景可能稍微有一些复杂,需要大家在搞明白 ProxyReflect 之后再查看:

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 的第三个参数可以控制 fullNamethis 指向,使其指向 receiver,也就是 proxy 实例

旧习换新

如果大家从事业务开发,那么 ProxyReflect 使用的场景应该并不多。而如果有一天,你需要监听某个对象的 getter 或者 setter 动作了,那么可以直接通过 Proxy 来完成。

第十五章:正则表达式

基本上每一本 JavaScript 的书籍都会包含一个 正则表达式 的环节,但是讲道理在日常的开发中 正则表达式 的使用不足以让我们花费大量的时间,同时书中的那些内容也不足以支撑复杂场景的 正则表达式 使用。

在本书的第十五章中,我摘抄出了 3 个新增的正则标记符,为大家提供参考。

第十六章:共享内存

作者在书中说道: 绝大多数的开发者并不需要在线程之间共享内存。 作者说的很对,所以我们这里不会花费篇章来说这个问题。如果你需要,那么可以看下书中讲解的内容。

第十七章:其他特性

第十七章的其他特性,主要呈现了一些单一、不成系统的特性。这些特性虽然大多都比较简单,但是有些在日常开发中还是比较有用的。

我从中抽出来了对我们日常开发有价值的新特性,咱们一起来看一下:

  • BigIntBigInt 主要用来处理大数字问题:

    // 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 方法

    image-20230627142003717

  • 取幂运算符

    // 取幂运算符 **
    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


文章作者: QT-7274
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 QT-7274 !
评论
  目录