閱讀《深刻理解ES6》書籍,筆記整理(下)

因爲所有筆記有接近4W的字數,所以分開爲上、下兩部分,第二部份內容計劃於明後兩天更新。
若是你以爲寫的不錯請給一個star,若是你想閱讀上、下兩部分所有的筆記,請點擊閱讀全文javascript

閱讀《深刻理解ES6》書籍,筆記整理(上)
閱讀《深刻理解ES6》書籍,筆記整理(下)html

JavaScript中的類

ES5中的近類結構

ES5及早期版本中沒有類的概念,最相近的思路建立一個自定義類型:首先建立一個構造函數,而後定義另外一個方法並賦值給構造函數的原型,例如:java

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中的類也借鑑了相似的方法。git

類的聲明

要聲明一個類,須要使用class關鍵來聲明,注意:類聲明僅僅只是對已有自定義類型聲明的語法糖而已。es6

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
複製代碼

代碼分析:github

  • 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調用類表達式,緊接着經過一對小括號調用這個表達式:promise

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》書籍,筆記整理(下)

相關文章
相關標籤/搜索