阅读《深入理解ES6》书籍,笔记整理(下)

3,476 阅读36分钟

由于全部笔记有接近4W的字数,因此分开为上、下两部分,第二部分内容计划于明后两天更新。
如果你觉得写的不错请给一个star,如果你想阅读上、下两部分全部的笔记,请点击阅读全文

阅读《深入理解ES6》书籍,笔记整理(上)
阅读《深入理解ES6》书籍,笔记整理(下)

JavaScript中的类

ES5中的近类结构

ES5及早期版本中没有类的概念,最相近的思路创建一个自定义类型:首先创建一个构造函数,然后定义另一个方法并赋值给构造函数的原型,例如:

function Person (name) {
  this.name = name
}
Person.prototype.sayName = function () {
  console.log(this.name)
}
let person = new Person('AAA')
person.sayName()                      // AAA
console.log(person instanceof Person) // true
console.log(person instanceof Object) // true

通过以上一个在ES5中近似类的结构的特性,许多JavaScript类库都基于这个模式进行开发,而且ES6中的类也借鉴了类似的方法。

类的声明

要声明一个类,需要使用class关键来声明,注意:类声明仅仅只是对已有自定义类型声明的语法糖而已。

class Person {
  // 相当于Person构造函数
  constructor (name) {
    this.name = name
  }
  // 相当于Person.prototype.sayName
  sayName () {
    console.log(this.name)
  }
}
const person = new Person('AAA')
person.sayName()                      // AAA
console.log(person instanceof Person) // true
console.log(person instanceof Object) // true

代码分析:

  • constructor():我们可以看到constructor()方法相当于我们上面写到的Person构造函数,在constructor()方法中我们定义了一个name的自有属性。所谓自有属性,就是类实例的属性,其不会出现在原型上,且只能在类的构造函数或方法中被创建。
  • sayName()sayName()方法就相当于我们上面写到的Person.prototype.sayName。有一个特别需要注意的地方就是:与函数有所不同,类属性不可被赋予新值,例如:Person.prototype就是这样一个只读的类属性。

类和自定义类型的差异:

  • 函数声明可以被提升,而类声明与let声明类似,不能被提升;真正执行声明语句之前,它们一直存在暂时性死区。
  • 类声明中的所有代码将自动运行在严格模式下,而且无法强行让代码脱离严格模式执行。
  • 在自定义方法中,需要通过Object.defineProperty()方法手动指定某个方法不可枚举;而在类中,所有方法都是不可枚举的。
  • 每一个类都有一个名叫[[Construct]]的内部方法,通过关键字new调用那些不含[[Construct]]的方法会导致程序抛出错误。
  • 使用除关键字new以外的方式调用类的构造函数会导致程序抛出错误。
  • 在类中修改类名会导致程序报错。

在了解了类和自定义类型的差异以后,我们可以使用除了类之外的语法来编写等价的代码:

// ES5等价类
let Person = (function() {
  'use strict'
  const Person = function(name) {
    if (typeof new.target === 'undefined') {
      throw new Error('必须通过关键字new调用此构造函数')
    }
    this.name = name
  }
  Object.defineProperty(Person.prototype, 'sayName', {
    value: function () {
      if (typeof new.target !== 'undefined') {
        throw new Error('不可通过关键字new来调用此方法')
      }
      console.log(this.name)
    },
    enumerable: false,
    writable: false,
    configurable: true
  })
  return Person
}())

const person = new Person('AAA')
person.sayName()                      // AAA
console.log(person instanceof Person) // true

类的表达式

类和函数都有两种存在形式:声明形式和表达式形式

// 类的表达式形式
let Person = class {
  constructor (name) {
    this.name
  }
  sayName () {
    console.log(this.name)
  }
}

从以上代码可以看出:类声明和类表达式的功能极为相似,只是编写的方式略有差异,二者均不会像函数声明和函数表达式一样被提升。
在我们最上面,我们的类声明是一个匿名的类表达式,其实类和函数一样,都可以定义为命名表达式:

let PersonClass = class Person{
  constructor (name) {
    this.name
  }
  sayName () {
    console.log(this.name)
  }
}
const person = new PersonClass('AAA')
person.sayName()                // AAA
console.log(typeof PersonClass) // function
console.log(typeof Person)      // undefined

类和单例

类表达式还有一种用法:通过立即调用类构造函数可以创建单例,用new调用类表达式,紧接着通过一对小括号调用这个表达式:

let person = new class {
  constructor (name) {
    this.name = name
  }
  sayName () {
    console.log(this.name)
  }
}('AAA')
person.sayName() // AAA

一等公民的类

一等公民是指一个可以传入函数,也可以从函数中返回,并且可以赋值给变量的值。

function createObject (classDef) {
  return new classDef()
}
const obj = createObject (class {
  sayHi () {
    console.log('Hello!')
  }
})
obj.sayHi() // Hello!

访问器属性

除了可以在构造函数中创建自己的属性,还可以在类的原型上直接定义访问器属性。

class Person {
  constructor (message) {
    this.animal.message = message
  }
  get message () {
    return this.animal.message
  }
  set message (message) {
    this.animal.message = message
  }
}
const desc = Object.getOwnPropertyDescriptor(Person.prototype, 'message')
console.log('get' in desc)  // true
console.log('set' in desc)  // true

为了更好的理解类的访问器属性,我们使用ES5代码来改写有关访问器部分的代码:

// 省略其它部分
Object.defineProperty(Person.prototype, 'message', {
  enumerable: false,
  configurable: true,
  get: function () {
    return this.animal.message
  },
  set: function (val) {
    this.animal.message = val
  }
})

我们经过对比可以发现,比起ES5等价代码而言,使用ES6类的语法要简洁得多。

可计算成员名称

类和对象字面量还有很多相似之处,类方法和访问器属性也支持使用可计算名称。

const methodName= 'sayName'
const propertyName = 'newName'
class Person {
  constructor (name) {
    this.name = name
  }
  [methodName] () {
    console.log(this.name)
  }
  get [propertyName] () {
    return this.name
  }
  set [propertyName] (val) {
    this.name = val
  }
}
let person = new Person('AAA')
person.sayName()            // AAA
person.name = 'BBB'
console.log(person.newName) // BBB

生成器方法

在类中,同样可以像对象字面量一样,在方法名前面加一个星号(*)的方式来定义生成器。

class MyClass {
  * createIterator () {
    yield 1
    yield 2
    yield 3
  }
}
let instance = new MyClass()
let it = instance.createIterator()
console.log(it.next().value)  // 1
console.log(it.next().value)  // 2
console.log(it.next().value)  // 3
console.log(it.next().value)  // undefined

尽管生成器方法很有用,但如果类仅仅只是用来表示值的集合,那么为它定义一个默认的迭代器会更加有用。

class Collection {
  constructor () {
    this.items = [1, 2, 3]
  }
  *[Symbol.iterator]() {
    yield *this.items.values()
  }
}
const collection = new Collection()
for (let value of collection) {
  console.log(value)
  // 1
  // 2
  // 3
}

静态成员

ES5及其早期版本中,直接将方法添加到构造函数中来模拟静态成员是一种常见的模式:

function PersonType (name) {
  this.name = name
}
// 静态方法
PersonType.create = function (name) {
  return new PersonType(name)
}
// 实例方法
PersonType.prototype.sayName = function () {
  console.log(this.name)
}
const person = PersonType.create('AAA')
person.sayName() // AAA

ES6中,类语法简化了创建静态成员的过程,在方法或者访问器属性名前面使用正式的静态注释static即可。
注意:静态成员只能在类中访问,不能在实例中访问

class Person {
  constructor (name) {
    this.name = name
  }
  sayName () {
    console.log(this.name)
  }
  static create (name) {
    return new Person(name)
  }
}
const person = Person.create('AAA')
person.sayName() // AAA

继承与派生类

ES6之前,实现继承与自定义类型是一个不小的工作,严格意义上的继承需要多个步骤实现。

function Rectangle (width, height) {
  this.width = width
  this.height = height
}
Rectangle.prototype.getArea = function () {
  return this.width * this.height
}
function Square(length) {
  Rectangle.call(this, length, length)
}
Square.prototype = Object.create(Rectangle.prototype, {
  constructor: {
    value: Square,
    enumerable: true,
    configurable: true,
    writabel: true
  }
})
const square = new Square(3)
console.log(square.getArea())             // 9
console.log(square instanceof Square)     // true
console.log(Square instanceof Rectangle)  // false

代码分析:为了使用ES6之前的语法实现继承,我们必须用一个创建自Rectangle.prototype的新对象来重写Square.prototype并调用Rectangle.call()方法。在ES6中由于类的出现我们可以轻松的实现继承,需要使用我们熟悉的关键词extends来指定类继承的函数。原型会自动调用,通过调用super()方法即可访问基类的构造函数,因此我们使用ES6类的语法来重写以上示例:

class Rectangle {
  constructor (width, height) {
    this.width = width
    this.height = height
  }
  getArea () {
    return this.width * this.height
  }
}
class Square extends Rectangle {
  constructor (length) {
    // 等价于 Rectangle.call(this, length, length)
    super(length, length)
  }
}
const square = new Square(3)
console.log(square.getArea())             // 9
console.log(square instanceof Square)     // true
console.log(Square instanceof Rectangle)  // false

注意:继承自其它类的类被称作派生类,如果在派生类中指定了构造函数则必须要调用super(),否则会抛出错误。如果不选择使用构造函数,则当创建新的实例时会自动调用super()并传入所有参数,如下:

// 省略其它代码
class Square extends Rectangle {
  // 没有构造函数
}
// 等价于
class Square extends Rectangle {
  constructor (...args) {
    super(...args)
  }
}

类方法遮蔽

注意:派生类中的方法总是会覆盖基类中的同名方法。

class Rectangle {
  constructor (width, height) {
    this.width = width
    this.height = height
  }
  getArea () {
    return this.width * this.height
  }
}
class Square extends Rectangle {
  constructor (length) {
    super(length, length)
    this.length = length
  }
  getArea () {
    return this.length * this.length
  }
}
const square = new Square(3)
console.log(square.getArea()) // 9

代码分析:由于Square类已经定义了getArea()方法,便不能在Square的实例中调用Rectangle.prototype.getArea()方法。如果我们想调用基类中的同名方法,可以使用super.getArea()

class Rectangle {
  constructor (width, height) {
    this.width = width
    this.height = height
  }
  getArea () {
    return this.width * this.height
  }
}
class Square extends Rectangle {
  constructor (length) {
    super(length, length)
    this.length = length
  }
  getArea () {
    return super.getArea()
  }
}
const square = new Square(3)
console.log(square.getArea()) // 9

静态成员继承

如果基类中有静态成员,那么这些静态成员在派生类中也可以使用。

class Rectangle {
  constructor (width, height) {
    this.width = width
    this.height = height
  }
  getArea () {
    return this.width * this.height
  }
  static create (width, length) {
    return new Rectangle(width, length)
  }
}
class Square extends Rectangle {
  constructor (length) {
    super(length, length)
  }
}
const square1 = new Square(3)
const square2 = Square.create(4, 4)
console.log(square1.getArea())             // 9
console.log(square2.getArea())             // 16
console.log(square1 instanceof Square)     // true
console.log(square2 instanceof Rectangle)  // true,因为square2是Rectangle的实例,而不是Square的实例

派生自表达式的类

ES6最强大的一面或许是表达式导出类的功能了,只要表达式可以被解析成为一个函数并且具有[[Construct]]属性和原型,那么就可以用extends进行派生。

function Rectangle (width, height) {
  this.width = width
  this.height = height
}
Rectangle.prototype.getArea = function () {
  return this.width * this.height
}
class Square extends Rectangle {
  constructor (length) {
    super(length, length)
  }
}
var square = new Square(3)
console.log(square.getArea())             // 9
console.log(square instanceof Rectangle)  // true

代码分析:Rectangle是一个典型的ES5风格的构造函数,Square是一个类,由于Rectangle具有[[Constructor]]属性和原型,因此Square类可以直接继承它。

extends动态继承

extends强大的功能使得类可以继承自任意类型的表达式,从而创造更多的可能性,例如动态确定类的继承目标。

function Rectangle (width, height) {
  this.width = width
  this.height = height
}
Rectangle.prototype.getArea = function () {
  return this.width * this.height
}
function getBaseClass () {
  return Rectangle
}
class Square extends getBaseClass() {
  constructor (length) {
    super(length, length)
  }
}
var square = new Square(3)
console.log(square.getArea())             // 9
console.log(square instanceof Rectangle)  // true

我们已经可以从上面的例子中看到,可以用过一个函数调用的形式,动态的返回需要继承的类,那么扩展开来,我们可以创建不同的继承mixin方法:

const NormalizeMixin = {
  normalize () {
    return JSON.stringify(this)
  }
}
const AreaMixin = {
  getArea () {
    return this.width * this.height
  }
}
function mixin(...mixins) {
  const base = function () {}
  Object.assign(base.prototype, ...mixins)
  return base
}
class Square extends mixin(AreaMixin, NormalizeMixin) {
  constructor (length) {
    super()
    this.width = length
    this.height = length
  }
}
const square = new Square(3)
console.log(square.getArea())     // 9
console.log(square.normalize())   // {width:3, height: 3}

代码分析:与getBaseClass()方法直接返回单一对象不同的是,我们定义了一个mixin()方法,作用是把多个对象的属性合并在一起并返回,然后使用extends来继承这个对象,从而达到继承NormalizeMixin对象的normalize()方法和AreaMixin对象的getArea()方法。

内建对象的继承

ES5及其早期版本中,如果我们想要通过继承的方式来创建属于我们自己的特殊数组几乎是不可能的,例如:

// 内建数组的行为
const colors = []
colors[0] = 'red'
console.log(colors.length)  // 1
colors.length = 0
console.log(colors[0])      // undefined
// 尝试ES5语法继承数组
function MyArray () {
  Array.apply(this, arguments)
}
MyArray.prototype = Object.create(Array.prototype, {
  constructor: {
    value: MyArray,
    enumerable: true,
    writable: true,
    configurable: true
  }
})
const colors1 = new MyArray()
colors1[0] = 'red'
console.log(colors1.length)  // 0
colors1.length = 0
console.log(colors1[0])      // 'red'

代码分析:我们可以看到我们自己的特殊数组的两条打印结果都不符合我们的预期,这是因为通过传统的JavaScript继承形式实现的数组继承没有从Array.apply()或原型赋值中继承相关的功能。

因为ES6引入了类的语法,因此使用ES6类的语法我们可以轻松的实现自己的特殊数组:

class MyArray extends Array {}
const colors = new MyArray()
colors['0'] = 'red'
console.log(colors.length)  // 1
colors.length = 0
console.log(colors[0])      // undefined

Symbol.species属性

内建对象继承的一个实用之处是:原本在内建对象中返回的实例自身的方法将自动返回派生类的实例。例如:如果我们有一个继承自Array的派生类MyArray,那么像slice()这样的方法也会返回一个MyArray的实例。

class MyArray extends Array {}
const items1 = new MyArray(1, 2, 3, 4)
const items2 = items1.slice(1, 3)
console.log(items1 instanceof MyArray) // true
console.log(items2 instanceof MyArray) // true

Symbol.species属性是诸多内部Symbol中的一个,它被用于定义返回函数的静态访问器属性。被返回的函数是一个构造函数,每当要在实例的方法中创建类的实例时必须使用这个构造函数,以下内建类型都已定义了Symbol.species属性:

  • Array
  • ArrayBuffer
  • Map
  • Promise
  • RegExp
  • Set
  • Typed arrays

构造函数中的new.target

我们在之前曾经了解过new.target及其值会根据函数被调用的方式而改变的原理,在类的构造函数中也可以通过new.target来确定类是如何被调用的,一般而言new.target等于类的构造函数。

class Rectangle {
  constructor (width, height) {
    this.width = width
    this.height = height
    console.log(new.target === Rectangle)
  }
}
const rect = new Rectangle(3, 4)  // 输出true 

然而当类被继承的时候,new.target是等于派生类的:

class Rectangle {
  constructor (width, height) {
    this.width = width
    this.height = height
    console.log(new.target === Rectangle)
    console.log(new.target === Square)
  }
}
class Square extends Rectangle {
  constructor (length) {
    super(length, length)
  }
}
const square = new Square(3)
// 输出false
// 输出true

根据new.target的特性,我们可以定义一种抽象基类:即不能被直接实例化,必须通过继承的方式来使用。

class Shape {
  constructor () {
    if (new.target === Shape) {
      throw new Error('不能被直接实例化')
    }
  }
}
class Rectangle extends Shape {
  constructor (width, height) {
    super()
    this.width = width
    this.height = height
  }
}
const rect = new Rectangle(3, 4)
console.log(rect instanceof Shape) // true

改进的数组功能

此章节关于定型数组的部分暂未整理。

创建数组

背景

ES6之前,创建数组只有两种形式,一种是使用Array构造函数,另外一种是使用数组字面量。如果我们想将一个类数组对象(具有数值型索引和length属性的对象)转换为数组,可选的方法十分有限,经常需要编写额外的代码。在此背景下,ES6新增了Array.ofArray.from这两个方法。

Array.of

ES6之前,使用Array构造函数创建数组有许多怪异的地方容易让人感到迷惑,例如:

let items = new Array(2)
console.log(items.length) // 2
console.log(items[0])     // undefined
console.log(items[1])     // undefined

items = new Array('2')
console.log(items.length) // 1
console.log(items[0])     // '2'

items = new Array(1, 2)
console.log(items.length) // 2
console.log(items[0])     // 1
console.log(items[1])     // 2

items = new Array(3, '2')
console.log(items.length) // 2
console.log(items[0])     // 3
console.log(items[1])     // '2'

迷惑行为:

  • 如果给Array构造函数传入一个数值型的值,那么数组的length属性会被设置为该值。
  • 如果传入一个非数值类型的值,那么这个值会成为目标数据的唯一项。
  • 如果传入多个值,此时无论这些值是不是数值类型,都会变成数组的元素。

为了解决以上的问题,ES6引入了Array.of()方法来解决这个问题。

Array.of()总会创建一个包含所有参数的数组,无论有多少个参数,无论参数是什么类型。

let items = Array.of(1, 2)
console.log(items.length) // 2
console.log(items[0])     // 1
console.log(items[1])     // 2

items = Array.of(2)
console.log(items.length) // 1
console.log(items[0])     // 2

items = Array.of('2')
console.log(items.length) // 1
console.log(items[0])     // '2'

Array.from

JavaScript不支持直接将非数组对象转换为真实的数组,arguments就是一种类数组对象,在ES5中将类数组对象转换为数组的代码可以这样下:

function makeArray(arrayLike) {
  let result = []
  for (let i = 0; i < arrayLike.length; i++) {
    result.push(arrayLike[i])
  }
  return result
}
function doSomething () {
  let args = makeArray(arguments)
  console.log(args)
}
doSomething(1, 2, 3, 4) // 输出[1, 2, 3, 4]

以上方法是使用for循环的方式来创建一个新数组,然后遍历arguments参数并将它们一个一个的push到数组中,最终返回。除了以上代码,我们还可以使用另外一种方式来达到相同的目的:

function makeArray (arrayLike) {
  return Array.prototype.slice.call(arrayLike)
}
function doSomething () {
  let args = makeArray(arguments)
  console.log(args)
}
doSomething(1, 2, 3, 4) // 输出[1, 2, 3, 4]

尽管我们提供了ES5两种不同的方案来将类数组转换为数组,但ES6还是给我们提供了一种语义清晰、语法简洁的新方法Array.from()

`Array.from()`方法接受可迭代对象或者类数组对象作为第一个参数。

function doSomething () {
  let args = Array.from(arguments)
  console.log(args)
}
doSomething(1, 2, 3, 4) // 输出[1, 2, 3, 4]

Array.from映射转换

可以提供一个映射函数作为Array.from()方法的第二个参数,这个函数用来将类数组对象的每一个值转换成其他形式,最后将这些结果按顺序存储在结果数组相应的索引中。

function translate() {
  return Array.from(arguments, (value) => value + 1)
}
let numbers = translate(1, 2, 3)
console.log(numbers) // [2, 3, 4]

正如我们上面看到的那样,我们使用一个(value) => value + 1的映射函数,分别为我们的参数+1,最终结果然后[2, 3, 4]。另外一种情况是,如果我们的映射函数处理的是对象的话,可以给Array.from()方法的第三个参数传递一个对象,来处理映射函数中相关this指向问题。

let helper = {
  diff: 1,
  add (value) {
    return value + this.diff
  }
}
function translate () {
  return Array.from(arguments, helper.add, helper)
}
let numbers = translate(1, 2, 3)
console.log(numbers) // [2, 3, 4]

Array.from转换可迭代对象

Array.from()可以将所有含有Symbol.iterator属性的对象转换为数组。

let iteratorObj = {
  * [Symbol.iterator]() {
    yield 1
    yield 2
    yield 3
  }
}
let numbers = Array.from(iteratorObj)
console.log(numbers) // [1, 2, 3]

注意:如果一个对象即是类数组对象又是可迭代对象,那么Array.from会优先根据迭代器来决定转换哪个值。

ES6数组新增方法

ES6为数组新增了几个方法:

  • find()findIndex()方法可以帮助我们在数组中查找任意值。
  • fill()方法可以用指定的值填充数组。
  • copyWithin()方法可以帮助我们在数组中复制元素,它和fill()方法是有许多相似之处的。

find()方法和findIndex()方法

find()和findIndex()都接受两个参数:一个是回调函数,另一个是可选参数,用于指定回调函数中的this值。

函数介绍:find()findIndex()方法都是根据传入的回调函数来查找,区别是find()方法返回查找到的值,findIndex()方法返回查找到的索引,而一旦查找到,即回调函数返回true,那么find()findIndex()方法会立即停止搜索剩余的部分。

let numbers = [25, 30, 35, 40, 45]
console.log(numbers.find(item => item >= 35))       // 35
console.log(numbers.findIndex(item => item === 35)) // 2

fill()方法

find()方法可以用指定的值填充一个至多个数组元素,当传入一个值时,fill()方法会用这个值重写数组中的所有值。

let numbers = [1, 2, 3, 4]
numbers.fill(1)
console.log(numbers.toString()) // [1, 1, 1, 1]

如果只想改变数组中的某一部分值,可以传入开始索引(第二个参数)和不包含结束索引(第三个参数)这两个可选参数,像下面这样:

let numbers = [1, 2, 3, 4]
numbers.fill(1, 2)
console.log(numbers)  // [1, 2, 1, 1]
numbers.fill(0, 1, 3)
console.log(numbers)  // [1, 0, 0, 1]

copyWithin()方法

copyWithin()方法需要传入两个参数:一个是方法开始填充值的索引位置,另一个是开始复制值的索引位置。

let numbers = [1, 2, 3, 4]
numbers.copyWithin(2, 0)
console.log(numbers.toString()) // 1, 2, 1, 2

代码分析:根据copyWithin()方法的特性,numbers.copyWithin(2, 0)可以解读为:使用索引0-1处对应的值,在索引2-3除开始复制粘贴值,默认情况下,如果不提供copyWithin()的第三个参数,则默认一直复制到数组的末尾,34的值会被重写,即结果为[1, 2, 1, 2]

let numbers = [1, 2, 3, 4]
numbers.copyWithin(2, 0, 1)
console.log(numbers.toString()) // 1, 2, 1, 4

代码分析:根据copyWithin()方法的特性,我们传递了第三个参数,结束复制的位置为1,即数组中只有3的值被替换为了1,其它值不变,即结果为:[1, 2, 1, 4]

Promise和异步编程

异步编程的背景知识

JavaScript引擎是基于单线程事件循环的概念创建的,同一时间只允许一个代码块在执行,所以需要跟踪即将运行的代码。那些代码被放在一个叫做任务队列中,每当一段代码准备执行时,都会被添加到任务队列中。每当JavaScript引擎中的一段代码结束执行,事件循环会执行队列中的下一个任务,它是JavaScript引擎中的一段程序,负责监控代码执行并管理任务队列。

事件模型

当用户点击按钮或者按下键盘上的按键时会触发类似onClick这样的事件,它会向任务队列添加一个新任务来响应用户的操作,这是JavaScript中最基础的异步编程模式,直到事件触发时才执行事件处理程序,且执行上下文与定义时的相同。

let button = document.getElemenetById('myBtn')
button.onClick = function () {
  console.log('click!')
}

事件模型适用于处理简单的交互,然而将多个独立的异步调用连接在一起会使程序更加复杂,因为我们必须跟踪每个事件的事件目标。

回调模式

Node.js通过普及回调函数来改进异步编程模型,回调函数与事件模型类似,异步代码都会在未来的某个时间点执行,二者的区别是回调模式中被调用的函数是作为参数传入的,如下:

readFile('example.pdf', function(err, contents) {
  if (err) {
    throw err
  }
  console.log(contents)
})

我们可以发现回调模式比事件模型更灵活,因此通过回调模式链接多个调用更容易:

readFile('example.pdf', function(err, contents) {
  if (err) {
    throw err
  }
  writeFile('example.pdf', function(err, contents) {
    if (err) {
      throw err
    }
    console.log('file was written!')
  })
})

我们可以发现,通过回调嵌套的形式,可以帮助我们解决许多问题,然而随着模块越来越复杂,回调模式需要嵌套的函数也越来越多,就形成了回调地狱,如下:

method1(function(err, result) {
  if (err) {
    throw err
  }
  method2(function(err, result) {
    if (err) {
      throw err
    }
    method3(function(err, result) {
      if (err) {
        throw err
      }
      method4(function(err, result) {
        if (err) {
          throw err
        }
        method5(result)
      })
    })
  })
})

Promise基础

Promise相当于异步操作结果的占位符,它不会去订阅一个事件,也不会传递一个回调函数给目标函数,而是让函数返回一个Promise

Promise的生命周期

每个Promise都会经历一个短暂的生命周期: 先是处于pending进行中的状态,此时操作尚未完成,所以它也是未处理状态的,一旦操作执行结束,Promise则变为已处理。操作结束后,Promise可能会进入到以下两个状态中的其中一个:

  • Fulfilled:异步操作成功完成。
  • Rejected:由于程序错误或者一些其他原因,异步操作未能成功完成。

根据以上介绍的状态,Promise的内部属性[[PromiseState]]被用来表示这三种状态:pendingfulfilledrejected。这个属性不会暴露在Promise对象上,所以不能通过编码的方式检测Promise的状态。

Promise.then()方法

我们已经知道,Promise会在操作完成之后进入FulfilledRejected其中一个,而Promise提供了Promise.then()方法。它有两个参数,第一个是Promise的状态变为fulfilled时要调用的函数,第二个是当Promise状态变为rejected时调用的函数,其中这两个参数都是可选的。

如果一个对象实现了上述`.then()`方法,那么这个对象我们称之为`thenable`对象。

let Promise = readFile('example.pdf')
// 同时提供执行完成和执行被拒的回调
Promise.then(function(content) {
  console.log('complete')
}, function(err) {
  console.log(err.message)
})
// 仅提供完成的回调
Promise.then(function(content) {
  console.log('complete')
})
// 仅提供被拒的回调
Promise.then(null, function(err) {
  console.log(err.message)
})

Promise.catch()方法

Promise还有一个catch()方法,相当于只给其传入拒绝处理程序的then()方法,所以和以上最后一个例子等价的catch()代码如下:

promise.catch(function(err) {
  console.log(err.message)
})
// 等价于
Promise.then(null, function(err) {
  console.log(err.message)
})

then()方法和catch()方法一起使用才能更好的处理异步操作结果。这套体系能够清楚的指明操作结果是成功还是失败,比事件和回调函数更好用。如果使用事件,在遇到错误时不会主动触发;如果使用回调函数,则必须要记得每次都检查错误参数。如果不给Promise添加拒绝处理程序,那所有失败就自动被忽略。

创建未完成的Promise

Promise构造函数可以创建新的Promise,构造函数只接受一个参数:包含初始化Promise代码的执行器函数。执行器函数接受两个参数,分别是resolve函数和reject函数。执行器成功完成时调用resolve函数,失败时则调用reject函数。

let fs = require('fs')
function readFile(filename) {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, function (err, contents) {
      if (err) {
        reject(err)
        return
      }
      resolve(contents)
    })
  })
}
let promise = readFile('example.pdf')
promise.then((contents) => {
  console.log(contents)
}, (err) => {
  console.log(err.message)
})

创建已处理的Promise

Promise.resolve()方法只接受一个参数并返回一个完成态的Promise,该方法永远不会存在拒绝状态,因而该Promise的拒绝处理程序永远不会被调用。

let promise = Promise.resolve(123)
promise.then(res => {
  console.log(res) // 123
})

可以使用Promise.reject()方法来创建已拒绝Promise,它与Promise.resolve()方法很像,唯一的区别是创建出来的是拒绝态的Promise

let promise = Promise.reject(123)
promise.catch((err) => {
  console.log(err) // 123
})

非Promise的Thenable对象

Promise.resolve()方法和Promise.reject()方法都可以接受非Promisethenable对象作为参数。如果传入一个非Promisethenable对象,则这些方法会创建一个新的Promise,并在then()函数中被调用。
拥有then()方法并且接受resolvereject这两个参数的普通对象就是非PromiseThenable对象。

let thenable = {
  then (resolve, reject) {
    resolve(123)
  }
}
let promise1 = Promise.resolve(thenable)
promise1.then((res) => {
  console.log(res) // 123
})

执行器错误

如果执行器内部抛出一个错误,则Promise的拒绝处理程序就会被调用。

let promise = new Promise((resolve, reject) => {
  throw new Error('promise err')
})
promise.catch((err) => {
  console.log(err.message) // promise err
})

代码分析:在上面这段代码中,执行器故意抛出了一个错误,每个执行器中都隐含一个try-catch块,所以错误会被捕获并传入拒绝处理程序,以上代码等价于:

let promise = new Promise((resolve, reject) => {
  try {
    throw new Error('promise err')
  } catch (ex) {
    reject(ex)
  }
})
promise.catch((err) => {
  console.log(err.message) // promise err
})

串联Promise

每当我们调用then()或者catch()方法时实际上创建并返回了另一个Promise,只有当第一个Promise完成或被拒绝后,第二个才会被解决。这给了我们可以将Promise串联起来实现更复杂的异步特性的方法。

let p1 = new Promise((resolve, reject) => {
  resolve(123)
})
p1.then(res => {
  console.log(res)      // 123
}).then(res => {
  console.log('finish') // finish
})

如果我们将以上例子拆解开来,那么会是如下的情况:

let p1 = new Promise((resolve, reject) => {
  resolve(123)
})
let p2 = p1.then(res => {
  console.log(res)      // 123
})
p2.then(res => {
  console.log('finish') // finish
})

串联Promise中捕获错误

我们已经知道,一个Promise的完成处理程序或者拒绝处理程序都有可能发生错误,而在Promise链中是可以捕获这些错误的:

let p1 = new Promise((resolve, reject) => {
  resolve(123)
})
p1.then(res => {
  throw new Error('error')
}).catch(error => {
  console.log(error.message)  // error
})

不仅可以捕获到then()方法中的错误,还可以捕获到catch()方法中的错误:

let p1 = new Promise((resolve, reject) => {
  resolve(123)
})

p1.then(res => {
  throw new Error('error then')
}).catch(error => {
  console.log(error.message)  // error then
  throw new Error('error catch')
}).catch(error => {
  console.log(error.message)  // error catch
})

Promise链返回值

Promise链的一个重要特性就是可以给下游的Promise传递值。

let p1 = new Promise((resolve, reject) => {
  resolve(1)
})
p1.then(res => {
  console.log(res)  // 1
  return res + 1
}).then(res => {
  console.log(res)  // 2
  return res + 2
}).then(res => {
  console.log(res)  // 4
})

在Promise链中返回Promise

我们在上面的例子中已经知道了,可以给下游的Promise传递值,但如果我们return的是另外一个Promise对象又该如何去走呢?实际上,这取决于这个Promise是完成还是拒绝,完成则会调用then(),拒绝则会调用catch()

let p1 = new Promise((resolve, reject) => {
  resolve(1)
})
let p2 = new Promise((resolve, reject) => {
  resolve(2)
})
let p3 = new Promise((resolve, reject) => {
  reject(new Error('error p3'))
})
p1.then(res => {
  console.log(res)            // 1
  return p2
}).then(res => {
  // p2完成,会调用then()
  console.log(res)            // 2
})

p1.then(res => {
  console.log(res)            // 1
  return p3
}).catch((error) => {
  // p3拒绝,会调用catch()
  console.log(error.message)  // error p3
})

响应对个Promise

Promise.all()方法

特点:Promise.all()方法只接受一个参数并返回一个Promise,且这个参数必须为一个或者多个Promise的可迭代对象(例如数组),只有当这个参数中的所有Promise对象全部被解决后才返回这个Promise。另外一个地方值得注意的是:Promise返回值,是按照参数数组中的Promise顺序存储的,所以可以根据Promise所在参数中的位置的索引去最终结果的Promise数组中进行访问。

let p1 = new Promise((resolve, reject) => {
  resolve(1)
})
let p2 = new Promise((resolve, reject) => {
  resolve(2)
})
let p3 = new Promise((resolve, reject) => {
  resolve(3)
})
let pAll = Promise.all([p1, p2, p3])
pAll.then(res => {
  console.log(res[0]) // 1:对应p1的结果
  console.log(res[1]) // 2:对应p2的结果
  console.log(res[2]) // 3:对应p3的结果
})

Promise.race()方法

特点:Promise.race()方法和Promise.all()方法对于参数是一致的,但是在行为和结果上有一点差别:Promise.race()方法接受参数数组,只要数组中的任意一个Promise被完成,那么Promise.race()方法就返回,所以Promise.race()方法的结果只有一个,也就是最先被解决的Promise的结果。

let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 100)
})
let p2 = new Promise((resolve, reject) => {
  resolve(2)
})
let p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(3)
  }, 100)
})
let pRace = Promise.race([p1, p2, p3])
pRace.then(res => {
  console.log(res) // 2 对应p2的结果
})

自Promise继承

Promise与其他内建类型一样,也是可以当做基类派生其他类的。

class MyPromise extends Promise {
  // 派生Promise,并添加success方法和failure方法
  success(resolve, reject) {
    return this.then(resolve, reject)
  }
  failure(reject) {
    return this.catch(reject)
  }
}
let p1 = new MyPromise((resolve, reject) => {
  resolve(1)
})
let p2 = new MyPromise((resolve, reject) => {
  reject(new Error('mypromise error'))
})
p1.success(res => {
  console.log(res)            // 1
})
p2.failure(error => {
  console.log(error.message)  // mypromise error
})

代理(Proxy)和反射(Reflect)API

数组问题

ES6出现之前,我们不能通过自己定义的对象模仿JavaScript数组对象的行为方式:当给数组的特定元素赋值时,会影响到数组的length属性,也可以通过length属性修改数组元素。

let colors = ['red', 'blue', 'green']
colors[3] = 'black'
console.log(colors.length) // 4
colors.length = 2
console.log(colors.length) // 2
console.log(colors)        // ['red', 'blue']

代理和反射

代理:代理可以拦截JavaScript引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数。
反射:反射APIReflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的Reflect方法。

代理陷阱 覆写特性 默认特性
get 读取一个属性值 Reflect.get
set 写入一个属性 Reflect.set
has in操作符 Reflect.has
apply 调用一个函数 Reflect.apply()
deleteProperty delete操作符 Reflect.deleteProperty()
construct 用new调用一个函数 Reflect.construct()
getPrototypeOf Object.getPrototypeOf() Reflect.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf() Reflect.setPrototypeOf()
isExtensible Object.isExtensible() Reflect.isExtensible()
preventExtensions Object.preventExtensions() Reflect.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor() Reflect.getOwnPropertyDescriptor()
defineProperty Object.defineProperty() Reflect.defineProperty()
ownKeys Object.keys()、Object.getOwnPropertyNames()和Object.getOwnPropertySymbols() Reflect.ownKeys()

创建一个简单的代理

Proxy构造函数创建代理需要传入两个参数:目标target和处理程序handler

处理程序handler是定义了一个或者多个陷阱的对象,在代理中,除了专门为操作定义的陷阱外,其余操作均使用默认特性,即意味着:不使用任何陷阱的处理程序等价于简单的转发代理。

let target = {}
let proxy = new Proxy(target, {})
proxy.name = 'AAA'
console.log(proxy.name)   // AAA
console.log(target.name)  // AAA
target.name = 'BBB'
console.log(proxy.name)   // BBB
console.log(target.name)  // BBB

使用set陷阱

set陷阱接受4个参数:

  • trapTarget:用于接受属性(代理的目标)的对象。
  • key:要写入的属性键(字符串或者Symbol类型)。
  • value:被写入属性的值。
  • receiver:操作发生的对象。

特点:Reflect.set()set陷阱对应的反射方法和默认特性,它和set代理陷阱一样也接受相同的四个参数,以方便在陷阱中使用。如果属性已设置陷阱应该返回true,否则返回false

案例:如果我们想创建一个属性值是数字的对象,对象中每新增一个属性都要加以验证,如果不是数字必须抛出错误。

let target = {
  name: 'target'
}
let proxy = new Proxy(target, {
  // 已有属性不检测
  set (trapTarget, key, value, receiver) {
    if (!trapTarget.hasOwnProperty(key)) {
      if (isNaN(value)) {
        throw new TypeError('属性值必须为数字')
      }
    }
    return Reflect.set(trapTarget, key, value, receiver)
  }
})
proxy.count = 1
console.log(proxy.count)  // 1
console.log(target.count) // 1
proxy.name = 'AAA'
console.log(proxy.name)   // AAA
console.log(target.name)  // AAA
proxy.anotherName = 'BBB' // 属性值非数字,抛出错误

使用get陷阱

get陷阱接受三个参数:

  • trapTarget:被读取属性的源对象(代理的目标)。
  • key:要读取的属性键(字符串或者Symbol)。
  • receiver:操作发生的对象。

JavaScript有一个我们很常见的特性,当我们试图访问某个对象不存在的属性的时候,不会报错而是返回undefined。如果这不是你想要的结果,那么可以通过get陷阱来验证对象结构。

let proxy = new Proxy({}, {
  get (trapTarget, key, receiver) {
    if (!(key in trapTarget)) {
      throw new Error(`属性${key}不存在`)
    }
    return Reflect.get(trapTarget, key, receiver)
  }
})
proxy.name = 'proxy'
console.log(proxy.name)  // proxy
console.log(proxy.nme)   // 属性值不存在,抛出错误

使用has陷阱

has陷阱接受两个参数:

  • trapTarget:读取属性的对象(代理的目标)
  • key:要检查的属性键(字符串或者Symbol)

in操作符特点:in操作符可以用来检测对象中是否含有某个属性,如果自有属性或原型属性匹配这个名称或者Symbol就返回true,否则返回false

let target = {
  value: 123
}
console.log('value' in target)    // 自有属性返回true
console.log('toString' in target) // 原型属性,继承自Object,也返回true

以上展示了in操作符的特性,可以使用has陷阱来改变这一特性:

let target = {
  value: 123,
  name: 'AAA'
}
let proxy = new Proxy(target, {
  has (trapTarget, key) {
    // 屏蔽value属性
    if (key === 'value') {
      return false
    } else {
      return Reflect.has(trapTarget, key)
    }
  }
})
console.log('value' in proxy)     // false
console.log('name' in proxy)      // true
console.log('toString' in proxy)  // true

使用deleteProperty陷阱

deleteProperty陷阱接受两个参数:

  • trapTarget:要删除属性的对象(代理的目标)。
  • key:要删除的属性键(字符串或者Symbol)。

我们都知道,delete操作符可以删除对象中的某个属性,删除成功则返回true,删除失败则返回false。如果有一个对象属性是不可以被删除的,我们可以通过deleteProperty陷阱方法来处理:

let target = {
  name: 'AAA',
  value: 123
}
let proxy = new Proxy(target, {
  deleteProperty(trapTarget, key) {
    if (key === 'value') {
      return false
    } else {
      return Reflect.deleteProperty(trapTarget, key)
    }
  }
})
console.log('value' in proxy)   // true
let result1 = delete proxy.value
console.log(result1)            // false
console.log('value' in proxy)   // true
let result2 = delete proxy.name
console.log(result2)            // true
console.log('name' in proxy)    // false

使用原型代理陷阱

setPrototypeOf陷阱接受两个参数:

  • trapTarget:接受原型设置的对象(代理的目标)。
  • proto:作为原型使用的对象。 getPrototypeOf陷阱接受一个参数:
  • trapTarget:接受获取原型的对象(代理的目标)。

我们在之前已经了解过,ES6新增了Object.setPrototypeOf()方法,它是ES5Object.getPrototypeOf()方法的补充。当我们想要在一个对象被设置原型或者读取原型的时候做一点什么,可以使用setPrototypeOf()陷阱和getPrototypeOf()陷阱。

let target = {}
let proxy = new Proxy(target, {
  getPrototypeOf(trapTarget) {
    // 必须返回对象或者null
    return null
  },
  setPrototypeOf(trapTarget, proto) {
    // 只要返回的不是false的值,就代表设置原型成功。
    return false
  }
})
let targetProto = Object.getPrototypeOf(target)
let proxyProto = Object.getPrototypeOf(proxy)
console.log(targetProto === Object.prototype) // true
console.log(proxyProto === Object.prototype)  // false
console.log(proxyProto)                       // null
Object.setPrototypeOf(target, {})             // 设置成功
Object.setPrototypeOf(proxy, {})              // 抛出错误

代码分析:以上代码重点强调了targetproxy的行为差异:

  • Object.getPrototypeOf()方法给target返回的是值,而给proxy返回的是null,这是因为proxy我们使用了getPrototypeOf()陷阱。
  • Object.setPrototypeOf()方法成功为target设置了原型,而在proxy中,因为我们使用了setPrototypeOf()陷阱,手动返回了false,所以设置原型不成功。

根据以上的分析,我们可以得到Object.getPrototypeOf()Object.setPrototypeOf()的默认行为:

let target = {}
let proxy = new Proxy(target, {
  getPrototypeOf(trapTarget) {
    // 必须返回对象或者null
    return Reflect.getPrototypeOf(trapTarget)
  },
  setPrototypeOf(trapTarget, proto) {
    // 只要返回的不是false的值,就代表设置原型成功。
    return Reflect.setPrototypeOf(trapTarget, proto)
  }
})
let targetProto = Object.getPrototypeOf(target)
let proxyProto = Object.getPrototypeOf(proxy)
console.log(targetProto === Object.prototype) // true
console.log(proxyProto === Object.prototype)  // true
Object.setPrototypeOf(target, {})             // 设置成功
Object.setPrototypeOf(proxy, {})              // 设置成功

两组方法的区别

Reflect.getPrototypeOf()方法和Reflect.setPrototypeOf()方法看起来和Object.getPrototypeOf()Object.setPrototypeOf()看起来执行相似的操作,但它们还是有一些不同之处的:

  1. Reflect.getPrototypeOf()方法和Reflect.setPrototypeOf()方法底层操作,其赋予开发者可以访问之前只在内部操作的[[GetPrototypeOf]][[SetPrototypeOf]]权限。而 Object.getPrototypeOf()Object.setPrototypeOf()方法是高级操作,创建伊始就是方便开发者使用的。
  2. 如果传入的参数不是对象,则Reflect.getPrototypeOf()会抛出错误,而Object.getPrototypeOf()方法则会在操作前先将参数强制转换为一个对象。
let result = Object.getPrototypeOf(1)
console.log(result === Number.prototype)  // true
Reflect.getPrototypeOf(1)                 // 抛出错误
  1. Object.setPrototypeOf()方法会通过一个布尔值来表示操作是否成功,成功时返回true,失败时返回false。而Reflect.setPrototypeOf()设置失败时会抛出错误。

使用对象可扩展陷阱

ES6之前对象已经有两个方法来修正对象的可扩展性:Object.isExtensible()Object.preventExtensions(),在ES6中可以通过代理中的isExtensible()preventExtensions()陷阱拦截这两个方法并调用底层对象。

  • isExtensible()陷阱返回一个布尔值,表示对象是否可扩展,接受唯一参数trapTarget
  • preventExtensions()陷阱返回一个布尔值,表示操作是否成功,接受唯一参数trapTarget

以下示例是isExtensible()preventExtensions()的默认行为:

let target = {}
let proxy = new Proxy(target, {
  isExtensible (trapTarget) {
    return Reflect.isExtensible(trapTarget)
  },
  preventExtensions (trapTarget) {
    return Reflect.preventExtensions(trapTarget)
  }
})
console.log(Object.isExtensible(target))  // true
console.log(Object.isExtensible(proxy))   // true
Object.preventExtensions(proxy)
console.log(Object.isExtensible(target))  // false
console.log(Object.isExtensible(proxy))   // false

现在如果有这样一种情况,我们想让Object.preventExtensions()对于proxy失效,那么可以把以上示例修改成如下的形式:

let target = {}
let proxy = new Proxy(target, {
  isExtensible(trapTarget) {
    return Reflect.isExtensible(trapTarget)
  },
  preventExtensions(trapTarget) {
    return false
  }
})
console.log(Object.isExtensible(target))  // true
console.log(Object.isExtensible(proxy))   // true
Object.preventExtensions(proxy)
console.log(Object.isExtensible(target))  // true
console.log(Object.isExtensible(proxy))   // true

两组方法的对比:

  • Object.preventExtensions()无论传入的是否为一个对象,它总是返回该参数,而Reflect.isExtensible()方法如果传入一个非对象,则会抛出一个错误。
  • Object.isExtensible()当传入一个非对象值时,返回false,而Reflect.isExtensible()则会抛出一个错误。

使用属性描述符陷阱

Object.defineProperty陷阱接受三个参数:

  • trapTarget:要定义属性的对象(代理的目标)
  • key:属性的键。
  • descriptor:属性的描述符对象。

Object.getOwnPropertyDescriptor陷阱接受两个参数:

  • trapTarget:要获取属性的对象(代理的目标)。
  • key:属性的键。

在代理中可以使用definePropertygetOwnPropertyDescriptor陷阱函数分别拦截Object.defineProperty()Object.getOwnPropertyDescriptor()方法的调用。以下示例展示了definePropertygetOwnPropertyDescriptor陷阱的默认行为。

let proxy = new Proxy({}, {
  defineProperty(trapTarget, key, descriptor) {
    return Reflect.defineProperty(trapTarget, key, descriptor)
  },
  getOwnPropertyDescriptor(trapTarget, key) {
    return Reflect.getOwnPropertyDescriptor(trapTarget, key)
  }
})
Object.defineProperty(proxy, 'name', {
  value: 'AAA'
})
console.log(proxy.name)         // AAA
const descriptor = Object.getOwnPropertyDescriptor(proxy, 'name')
console.log(descriptor.value)   // AAA

Object.defineProperty()添加限制

defineProperty陷阱返回布尔值来表示操作是否成功,返回true时,表示Object.defineProperty()执行成功;返回false时,Object.defineProperty()抛出错误。
假设我们现在有这样一个需求:一个对象的属性键不能设置为Symbol属性的,我们可以使用defineProperty陷阱来实现:

let proxy = new Proxy({}, {
  defineProperty(trapTarget, key, descriptor) {
    if (typeof key === 'symbol') {
      return false
    }
    return Reflect.defineProperty(trapTarget, key, descriptor)
  }
})
Object.defineProperty(proxy, 'name', {
  value: 'AAA'
})
console.log(proxy.name) // AAA
const nameSymbol = Symbol('name')
// 抛出错误
Object.defineProperty(proxy, nameSymbol, {
  value: 'BBB'
})

Object.getOwnPropertyDescriptor()添加限制

无论将什么对象作为第三个参数传递给Object.defineProperty()方法,都只有属性enumerableconfigurablevaluewritablegetset将出现在传递给defineProperty陷阱的描述符对象中,也意味着Object.getOwnPropertyDescriptor()方法总是返回以上几种属性。

let proxy = new Proxy({}, {
  defineProperty(trapTarget, key, descriptor) {
    console.log(descriptor.value) // AAA
    console.log(descriptor.name)  // undeinfed
    return Reflect.defineProperty(trapTarget, key, descriptor)
  },
  getOwnPropertyDescriptor(trapTarget, key) {
    return Reflect.getOwnPropertyDescriptor(trapTarget, key)
  }
})
Object.defineProperty(proxy, 'name', {
  value: 'AAA',
  name: 'custom'
})
const descriptor = Object.getOwnPropertyDescriptor(proxy, 'name')
console.log(descriptor.value) // AAA
console.log(descriptor.name)  // undeinfed

注意getOwnPropertyDescriptor()陷阱的返回值必须是一个nullundefined或者一个对象。如果返回的是一个对象,则对象的属性只能是enumerableconfigurablevaluewritablegetset,使用不被允许的属性会抛出一个错误。

let proxy = new Proxy({}, {
  getOwnPropertyDescriptor(trapTarget, key) {
    return {
      name: 'proxy'
    }
  }
})
// 抛出错误
let descriptor = Object.getOwnPropertyDescriptor(proxy, 'name')

两组方法对比:

  • Object.defineProperty()方法和Reflect.defineProperty()方法只有返回值不同,前者只返回第一个参数;而后者返回值与操作有关,成功则返回true,失败则返回false
let target = {}
let result1 = Object.defineProperty(target, 'name', {
  value: 'AAA'
})
let result2 = Reflect.defineProperty(target, 'name', {
  value: 'AAA'
})
console.log(result1 === target) // true
console.log(result2)            // true
  • Object.getOwnPropertyDescriptor()方法传入一个原始值作为参数,内部会把这个值强制转换为一个对象;而Reflect.getOwnPropertyDescriptor()方法传入一个原始值,则会抛出错误。
let descriptor1 = Object.getOwnPropertyDescriptor(2, 'name')
console.log(descriptor1)  // undefined
// 抛出错误
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, 'name')

使用ownKeys陷阱

ownKeys代理陷阱可以拦截内部方法[[OwnPropertyKeys]],我们通过返回一个数组的值来覆写其行为。这个数组被用于Object.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.assign()四个方法,其中Object.assign()方法用数组来确定需要复制的属性。ownKeys陷阱唯一接受的参数是操作的目标,返回值是一个数组或者类数组对象,否则就会抛出错误。

几种方法的区别:

  • Reflect.ownKeys():返回的数组中包含所有对象的自有属性的键名,包括字符串类型和Symbol类型。
  • Object.getOwnPropertyNames()Object.keys():返回的数组中排除了Symbol类型。
  • Object.getOwnPropertySymbols():返回的数组中排出了字符串类型。
  • Object.assign():字符串和Symbol类型都支持。

假设我们在使用以上几种方法的时候,不想要指定规则的属性键,那么可以使用Reflect.ownKeys()陷阱来实现:

let proxy = new Proxy({}, {
  ownKeys (trapTarget) {
    return Reflect.ownKeys(trapTarget).filter(key => {
      // 排除属性开头带有_的键
      return typeof key !== 'string' || key[0] !== '_'
    })
  }
})
let nameSymbol = Symbol('name')
proxy.name = 'AAA'
proxy._name = '_AAA'
proxy[nameSymbol] = 'Symbol'
let names = Object.getOwnPropertyNames(proxy)
let keys = Object.keys(proxy)
let symbols = Object.getOwnPropertySymbols(proxy)
console.log(names)    // ['name']
console.log(keys)     // ['name']
console.log(symbols)  // ['Symbol(name)']

使用apply和construct陷阱

apply陷阱接受以下几个参数:

  • trapTarget:被执行的函数(代理的目标)。
  • thisArg:函数被调用时内部this的值。
  • argumentsList:传递给函数的参数数组。

construct陷阱函数接受以下几个参数:

  • trapTarget:被执行的函数(代理的目标)。
  • argumentsList:传递给函数的参数数组。

applyconstruct陷阱函数是所有代理陷阱中,代理目标是一个函数的仅有的两个陷阱函数。我们在之前已经了解过,函数有两个内部方法[[Call]][[Construct]],当使用new调用时,执行[[Construct]]方法,不用new调用时,执行[[Call]]方法。
以下实例为apply陷阱和construct陷阱的默认行为:

let target = function () {
  return 123
}
let proxy = new Proxy(target, {
  apply (trapTarget, thisArg, argumentsList) {
    return Reflect.apply(trapTarget, thisArg, argumentsList)
  },
  construct (trapTarget, argumentsList) {
    return Reflect.construct(trapTarget, argumentsList)
  }
})
console.log(typeof proxy)               // function
console.log(proxy())                    // 123
let instance = new proxy()
console.log(instance instanceof proxy)  // true
console.log(instance instanceof target) // true

验证函数参数

假设我们有这样一个需求:一个函数,其参数只能为数字类型。可以使用apply陷阱或者construct陷阱来实现:

function sum(...values) {
  return values.reduce((prev, current) => prev + current, 0) 
}
let sumProxy = new Proxy(sum, {
  apply(trapTarget, thisArg, argumentsList) {
    argumentsList.forEach(item => {
      if (typeof item !== 'number') {
        throw new TypeError('所有参数必须是数字类型')
      }
    })
    return Reflect.apply(trapTarget, thisArg, argumentsList)
  },
  construct (trapTarget, argumentsList) {
    throw new TypeError('该函数不能通过new来调用')
  }
})
console.log(sumProxy(1, 2, 3, 4, 5))    // 15
let proxy = new sumProxy(1, 2, 3, 4, 5) // 抛出错误

不用new调用构造函数

在前面的章节中,我们已经了解到new.target元属性,它是用new调用函数时对该函数的引用,可以使用new.target的值来确定函数是否是通过new来调用:

function Numbers(...values) {
  if (typeof new.target === 'undefined') {
    throw new TypeError('该函数必须通过new来调用。')
  }
  this.values = values
}
let instance = new Numbers(1, 2, 3, 4, 5)
console.log(instance.values) // [1, 2, 3, 4, 5]
Numbers(1, 2, 3, 4)          // 报错

假设我们有以上的一个函数,其必须通过new来调用,但我们依然想让其能够使用非new调用的形式来使用,这个时候我们可以使用apply陷阱来实现:

function Numbers(...values) {
  if (typeof new.target === 'undefined') {
    throw new TypeError('该函数必须通过new来调用。')
  }
  this.values = values
}
let NumbersProxy = new Proxy(Numbers, {
  construct (trapTarget, argumentsList) {
    return Reflect.construct(trapTarget, argumentsList)
  },
  apply (trapTarget, thisArg, argumentsList) {
    return Reflect.construct(trapTarget, argumentsList)
  }
})
let instance1 = new NumbersProxy(1, 2, 3, 4, 5)
let instance2 = NumbersProxy(1, 2, 3, 4, 5)
console.log(instance1.values) // [1, 2, 3, 4, 5]
console.log(instance2.values) // [1, 2, 3, 4, 5]

覆写抽象基类构造函数

construct陷阱还接受第三个可选参数函数,其作用是被用作构造函数内部的new.target的值。

假设我们现在有这样一个场景:有一个抽象基类,其必须被继承,但我们依然想不这么做,这个时候可以使用construct陷阱还是来实现:

class AbstractNumbers {
  constructor (...values) {
    if (new.target === AbstractNumbers) {
      throw new TypeError('此函数必须被继承')
    }
    this.values = values
  }
}
let AbstractNumbersProxy = new Proxy(AbstractNumbers, {
  construct (trapTarget, argumentsList) {
    return Reflect.construct(trapTarget, argumentsList, function () {})
  }
})
let instance = new AbstractNumbersProxy(1, 2, 3, 4, 5)
console.log(instance.values)  // 1, 2, 3, 4, 5

可调用的类构造函数

我们都知道必须使用new来调用类的构造函数,因为类构造函数的内部方法[[Call]]被指定来抛出一个错误,但我们依然可以使用apply代理陷阱实现不用new就能调用构造函数:

class Person {
  constructor(name) {
    this.name = name
  }
}
let PersonProxy = new Proxy(Person, {
  apply (trapTarget, thisArg, argumentsList) {
    return new trapTarget(...argumentsList)
  }
})
let person = PersonProxy('AAA')
console.log(person.name)                    // AAA
console.log(person instanceof PersonProxy)  // true
console.log(person instanceof Person)       // true

可撤销代理

在我们之前的所有代理例子中,全部都是不可取消的代理。但有时候我们希望能够对代理进行控制,让他能在需要的时候撤销代理,这个时候可以使用Proxy.revocable()函数来创建可撤销的代理,该方法采用与Proxy构造函数相同的参数,其返回值是具有以下属性的对象:

  • proxy:可撤销的代理对象。
  • revoke:撤销代理要调用的函数。 当调用revoke()函数的时候,不能通过proxy执行进一步的操作,任何与代理对象交互的尝试都会触发代理陷阱抛出错误。
let target = {
  name: 'AAA'
}
let { proxy, revoke } = Proxy.revocable(target, {})
console.log(proxy.name) // AAA
revoke()
console.log(proxy.name) // 抛出错误

解决数组问题

我们在之前已经了解过,在ES6之前我们无法完全模拟数组的行为,就像下面的示例一样:

let colors = ['red', 'green', 'blue']
console.log(colors.length)  // 3
colors[3] = 'black'
console.log(colors.length)  // 4
console.log(colors[3])      // black
colors.length = 2
console.log(colors.length)  // 2
console.log(colors)         // ['red', 'green']

无法模拟的两个重要行为:

  • 添加新元素时增加length的值
  • 减少length的值可以删除元素

检测数组索引

判断一个属性是否为数组索引,需要满足规范条件:当且仅当ToString(ToUnit32(P))等于P,并且ToUnit32(P)不等于2³²-1

function toUnit32(value) {
  return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32)
}
function isArrayIndex(key) {
  let numbericKey = toUnit32(key)
  return String(numbericKey) === key && numbericKey < (Math.pow(2, 32) - 1)
}

代码分析:toUnit32()函数通过规范中描述的算法将给定的值转换为无符号32位整数;isArrayIndex()函数先将键转换为uint32结构,然后进行一次比较以确定这个键是否是数组索引。

添加新元素时增加length的值

function toUnit32(value) {
  return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32)
}
function isArrayIndex(key) {
  let numbericKey = toUnit32(key)
  return String(numbericKey) === key && numbericKey < (Math.pow(2, 32) - 1)
}
function createMyArray (length = 0) {
  return new Proxy({ length }, {
    set (trapTarget, key, value) {
      let currentLength = Reflect.get(trapTarget, 'length')
      if (isArrayIndex(key)) {
        let numbericKey = Number(key)
        if (numbericKey >= currentLength) {
          Reflect.set(trapTarget, 'length', numbericKey + 1)
        }
      }
      return Reflect.set(trapTarget, key, value)
    }
  })
}
let colors = createMyArray(3)
console.log(colors.length)  // 3
colors[0] = 'red'
colors[1] = 'green'
colors[2] = 'blue'
console.log(colors.length)  // 3
colors[3] = 'black'
console.log(colors.length)  // 4
console.log(colors[3])      // black 

减少length的值可以删除元素

function toUnit32(value) {
  return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32)
}
function isArrayIndex(key) {
  let numbericKey = toUnit32(key)
  return String(numbericKey) === key && numbericKey < (Math.pow(2, 32) - 1)
}
function createMyArray (length = 0) {
  return new Proxy({ length }, {
    set (trapTarget, key, value) {
      let currentLength = Reflect.get(trapTarget, 'length')
      if (isArrayIndex(key)) {
        let numbericKey = Number(key)
        if (numbericKey >= currentLength) {
          Reflect.set(trapTarget, 'length', numbericKey + 1)
        }
      } else if(key === 'length') {
        if (value < currentLength) {
          for(let index = currentLength - 1; index >= value; index--) {
            Reflect.deleteProperty(trapTarget, index)
          }
        }
      }
      return Reflect.set(trapTarget, key, value)
    }
  })
}
let colors = createMyArray(3)
console.log(colors.length)  // 3
colors[0] = 'red'
colors[1] = 'green'
colors[2] = 'blue'
colors[3] = 'black'
console.log(colors.length)  // 4
colors.length = 2
console.log(colors.length)  // 2
console.log(colors[3])      // undefined
console.log(colors[2])      // undefined
console.log(colors[1])      // green
console.log(colors[0])      // red

实现MyArray类

如果我们想要创建使用代理的类,最简单的方法是像往常一样定义类,然后在构造函数中返回一个代理,像下面这样:

class Thing {
  constructor () {
    return new Proxy(this, {})
  }
}
let myThing = new Thing()
console.log(myThing instanceof Thing) // true

在理解了以上概念后,我们可以使用代理创建一个自定义的数组类:

function toUnit32(value) {
  return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32)
}
function isArrayIndex(key) {
  let numbericKey = toUnit32(key)
  return String(numbericKey) === key && numbericKey < (Math.pow(2, 32) - 1)
}
class MyArray {
  constructor(length = 0) {
    this.length = length
    return new Proxy(this, {
      set (trapTarget, key, value) {
        let currentLength = Reflect.get(trapTarget, 'length')
        if (isArrayIndex(key)) {
          let numbericKey = Number(key)
          if (numbericKey >= currentLength) {
            Reflect.set(trapTarget, 'length', numbericKey + 1)
          }
        } else if(key === 'length') {
          if (value < currentLength) {
            for(let index = currentLength - 1; index >= value; index--) {
              Reflect.deleteProperty(trapTarget, index)
            }
          }
        }
        return Reflect.set(trapTarget, key, value)
      }
    })
  }
}
let colors = new MyArray(3)
console.log(colors instanceof MyArray)  // true
console.log(colors.length)              // 3
colors[0] = 'red'
colors[1] = 'green'
colors[2] = 'blue'
colors[3] = 'black'
console.log(colors.length)              // 4
colors.length = 2
console.log(colors.length)              // 2
console.log(colors[3])                  // undefined
console.log(colors[2])                  // undefined
console.log(colors[1])                  // green
console.log(colors[0])                  // red

代码总结:虽然从类构造函数返回代理很容易,但这也意味着每创建一个实例都要创建一个新代理。

将代理作为原型

针对上节所提到的:可以从类构造函数返回代理,但每创建一个实例都要创建一个新代理,这个问题可以使用将代理用作原型,让所有实例共享一个代理。

let target = {}
let newTarget = Object.create(new Proxy(target, {
  defineProperty(trapTarget, name, descriptor) {
    return false
  }
}))
Object.defineProperty(newTarget, 'name', {
  value: 'newTarget'
})
console.log(newTarget.name)                   // newTarget
console.log(newTarget.hasOwnProperty('name')) // true

代码分析:调用Object.defineProperty()方法并传入newTarget来创建一个名为name的自有属性,在对象上定义属性的操作不需要操作对象的原型,所以代理中的defineProperty陷阱永远不会被调用。正如你所看到的那样,这种方式限制了代理作为原型的能力,但依然有几个陷阱是十分有用的。

在原型上使用get陷阱

调用内部方法[[Get]]读取属性的操作现查找自有属性,如果未找到指定名称的自有属性,则继续到原型中查找,直到没有更多可以查找的原型过程结束,如果设置一个get陷阱,就能捕获到在原型上查找属性的陷阱。

let target = {}
let newTarget = Object.create(new Proxy(target, {
  get (trapTarget, key, receiver) {
    throw new ReferenceError(`${key}不存在。`)
  }
}))
newTarget.name = 'AAA'
console.log(newTarget.name) // AAA
console.log(newTarget.nme)  // 抛出错误

代码分析:我们使用一个代理作为原型创建了一个新对象,当调用它时,如果其上不存在给定的键,那么get陷阱会抛出错误;而name属性存在,所以读取它的时候不会调用原型上的get陷阱。

在原型上使用set陷阱

内部方法[[Set]]同样会检查目标对象中是否含有某个自有属性,如果不存在则继续在原型上查找。但现在最棘手的问题是:无论原型上是否存在同名属性,给该属性赋值时都将默认在实例中创建该属性:

let target = {}
let thing = Object.create(new Proxy(target, {
  set(trapTarget, key, value, receiver) {
    return Reflect.set(trapTarget, key, value, receiver)
  }
}))
console.log(thing.hasOwnProperty('name')) // false
thing.name = 'AAA'                        // 触发set陷阱
console.log(thing.name)                   // AAA
console.log(thing.hasOwnProperty('name')) // true
thing.name = 'BBB'                        // 不触发set陷阱
console.log(thing.name)                   // BBB

在原型上使用has陷阱

只有在搜索原型链上的代理对象时才会调用has陷阱,而当你用代理作为原型时,只有当指定名称没有对应的自有属性时才会调用has陷阱。

let target = {}
let thing = Object.create(new Proxy(target, {
  has (trapTarget, key) {
    return Reflect.has(trapTarget, key)
  }
}))
console.log('name' in thing)  // false,触发了原型上的has陷阱
thing.name = 'AAA'
console.log('name' in thing)  // true,没有触发原型上的has陷阱

将代理用作类的原型

由于类的prototype属性是不可写的,因此不能直接修改类来使用代理作为类的原型,但是可以通过继承的方法来让类误认为自己可以将代理用作自己的原型。

function NoSuchProperty () {

}
NoSuchProperty.prototype = new Proxy({}, {
  get(trapTarget, key, receiver) {
    throw new ReferenceError(`${key}不存在`)
  }
})
let thing = new NoSuchProperty()
console.log(thing.name) // 抛出错误

以上代码是一个使用ES5风格的类型定义,那么接下来,我们需要使用ES6extends语法,来让类实现继承:

function NoSuchProperty () {

}
NoSuchProperty.prototype = new Proxy({}, {
  get(trapTarget, key, receiver) {
    throw new ReferenceError(`${key}不存在`)
  }
})
class Square extends NoSuchProperty {
  constructor (width, height) {
    super()
    this.width = width
    this.height = height
  }
}
let shape = new Square(2, 5)
let area1 = shape.width * shape.height
console.log(area1)                      // 10
let area2 = shape.length * shape.height // 抛出错误

代码分析:Square类继承NoSuchProperty,所以它的原型链中包含代理,之后创建的shape对象是Square的新实例,它有两个自有属性:widthheight。当我们访问shape实例上不存在的length属性时,会在原型链中查找,进而触发get陷阱,抛出一个错误。

用模块封装代码

什么是模块

模块是自动运行在严格模式下并且没有办法退出运行的JavaScript代码,与共享一切架构相反,它有如下几个特点:

  • 在模块顶部创建的变量不会自动被添加到全局共享作用域,而是仅在模块的顶级作用域中存在。
  • 模块必须导出一些外部代码可以访问的元素,例如:变量或者函数。
  • 模块也可以从其他模块导入绑定。
  • 在模块的顶部,this的值是undefined

导出的基本语法

可以用export关键字将一部分已发布的代码暴露给其他模块。

// example.js
export let color = 'red'
export const PI = 3.1415
export function sum (num1, num2) {
  return num1 + num2
}
export class Rectangle {
  constructor (width, height) {
    this.width = width
    this.height = height
  }
}
// 模块私有的,外部无法访问
function privateFunc (num1, num2) {
  return num1 + num2
}

导入的基本语法

从模块中导入的功能可以通过import关键字在另一个模块中访问,import语句的两个部分分别是:要导入的标识符和标识符从哪个模块导入。
以下示例是导入语句的基本形式:

import { identifier1, indentifier2 } from './example.js'

注意:当从模块中导入一个绑定时,它就好像使用了const定义的一样。结果是我们不能定义另一个同名的变量,也无法在import语句前使用标识符或改变绑定的值。

导入单个绑定和导入多个绑定

// 只导入一个
import { sum } from './math.js'
sum(1, 2)

// 导入多个
import { sum, minus } from './math.js'
sum(1, 2)
minus(1, 2)

导入整个模块

特殊情况下,可以导入整个模块作为一个单一的对象,然后所有的导出都可以作为对象的属性使用:

import * as Math from './math.js'
Math.sum(1, 2)
Math.minus(1, 2)

注意:

  • 不管在import语句中把一个模块写多少次,该模块始终只执行一次,因为导入模块执行后,实例化过的模块被保存在内存中,只要另一个import语句引用它就可以重复使用。
// math.js中的代码只执行了一次
import { sum } from './math.js'
import { minus } from './math.js'
  • exportimport语句必须在其他语句和函数之外使用,在其中使用会报错。
if (flag) {
  // 报错
  export flag 
}
function tryImport() {
  // 报错
  import * as Math from './math.js'
}

导出和导入时重命名

正如上面我们所看到的那样,导出的绑定就像const定义的变量一样,我们无法更改,如果多个模块之间存在同名绑定,这种情况下我们可以使用as来给绑定取一个别名,进而可以避免重名。

// math.js 导出时别名
function sum(num1, num2) {
  return num1 + num2
}
export {
  sum as SUM
}

// math.js 导入时别名
import { SUM as sum  } from './math.js'
console.log(typeof SUM) // undefined
sum(1, 2)

模块的默认值

模块的默认值指的是通过default关键字指定的单个变量、函数或者类,只能为每个模块设置一个默认的导出值,导出时多次使用default关键字会报错。

// example.js 导出默认值
export default function (num1, num2) {
  return num1 + num2
}
// example.js 导入默认值
import sum from './example.js'
sum(1, 2)

注意:导入默认值和导入非默认值是可以混用的,例如: 导出example.js

export const colors = ['red', 'green', 'blue']
export default function (num1, num2) {
  return num1 + num2
}

导入example.js:

import sum, { colors } from './example.js'

重新导出一个绑定

有时候我们可能会重新导出我们已经导入的内容,就像下面这样:

import { sum } from './example.js'
export { sum }
// 可以简写成
export { sum } from './example.js'
// 简写+别名
export { sum as SUM } from './example.js'
// 全部重新导出
export * from './example.js'

无绑定导入

无绑定导入最有可能被应用于创建polyfillshim

尽管我们已经知道模块中的顶层管理、函数和类不会自动出现在全局作用域中,但这并不意味这模块无法访问全局作用域。
例如:如果我们想向所有数组添加pushAll()方法,可以像下面这样: 无绑定导出array.js

Array.prototype.pushAll = function (items) {
  if (!Array.isArray(items)) {
    throw new TypeError('参数必须是一个数组。')
  }
  return this.push(...items)
}

无绑定导入array.js

import './array.js'
let colors = ['red', 'green', 'blue']
let items = []
items.pushAll(colors)

加载模块

我们都知道,在Web浏览器中使用一个脚本文件,可以通过如下三种方式来实现:

  • script元素中通过src属性指定一个加载代码的地址来加载js脚本。
  • js代码内嵌到没有src属性的script元素中。
  • 通过Web Worker或者Service Worker的方式加载并执行js代码。

为了完全支持模块的功能,JavaScript扩展了script元素的功能,使其能够通过设置type/module的形式来加载模块:

// 外联一个模块文件
<script type="module" src="./math.js"></script>
// 内联模块代码
<script type="module">
  import { sum } from './example.js'
  sum(1, 2)
</script>

Web浏览器中模块加载顺序

模块和脚本不同,它是独一无二的,可以通过import关键字来指明其所依赖的其他文件,并且这些文件必须加载进该模块才能正确执行,因此为了支持该功能,<script type="module"></script>执行时自动应用defer属性。

// 最先执行
<script type="module" src="./math.js"></script>
// 其次执行
<script type="module">
  import { sum } from './math.js'
</script>
// 最后执行
<script type="module" src="./math1.js"></script>

Web浏览器中的异步模块加载

async属性也可以应用在模块上,在<script type="module"></script>元素上应用async属性会让模块以类似于脚本的方式执行,唯一的区别在于:在模块执行前,模块中的所有导入资源必须全部下载下来。

// 无法保证哪个模块先执行
<script type="module" src="./module1.js" async></script>
<script type="module" src="./module2.js" async></script>

将模块作为Worker加载

为了支持加载模块,HTML标准的开发者向Worker这些构造函数添加了第二个参数,第二个参数是一个对象,其type属性的默认值是script,可以将type设置为module来加载模块文件。

let worker = new Worker('math.js', {
  type: 'module'
})

浏览器模块说明符解析

我们可以发现,我们之前的所有示例中,模块说明符使用的都是相对路径,浏览器要求模块说明符具有以下几种格式之一:

  • /开头的解析为根目录开始。
  • ./开头的解析为当前目录开始。
  • ../开头的解析为父目录开始。
  • URL格式。
import { first } from '/example1.js'
import { second } from './example2.js'
import { three } from '../example3.js'
import { four } from 'https://www.baidu.com/example4.js'

下面这些看起来正常的模块说明符在浏览器中实际上是无效的:

import { first } from 'example1.js'
import { second } from 'example/example2.js'

如果你觉得写的不错请给一个star,如果你想阅读上、下两部分全部的笔记,请点击阅读全文

阅读《深入理解ES6》书籍,笔记整理(上)
阅读《深入理解ES6》书籍,笔记整理(下)