As the total notes are nearly 4W words, they are divided into the first and second parts. The second part is planned to be updated in the next two days. If you think it’s a good one, please give it a star. If you want to read all the notes in the first and second parts, please click here to read the full text

Reading the book “In-depth Understanding of ES6”, sorting out notes (PART 1)

In JavaScript class

Near-class structures in ES5

There is no concept of classes in ES5 and earlier versions. The closest idea is to create a custom type: first create a constructor, then define another method and assign it to the constructor’s prototype, for example:

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
Copy the code

Many JavaScript libraries have been developed based on this pattern, thanks to the above features that approximate the structure of classes in ES5, and classes in ES6 have borrowed a similar approach.

Declaration of a class

To declare a class, you need to declare it using the class key. Note that the class declaration is just a syntactic sugar for an existing custom type declaration.

class Person {
  // corresponds to the Person constructor
  constructor (name) {
    this.name = name
  }
  // Equivalent to 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
Copy the code

Code analysis:

  • constructor(): We can see thatconstructor()The method is equivalent to what we wrote abovePersonConstructor, inconstructor()Method we defined onenameHas its own attributes. A property of its own is a property of a class instance that does not appear in the stereotype and can only be created in a constructor or method of the class.
  • sayName():sayName()The method is the same as what we wrote abovePerson.prototype.sayName. In particular, unlike functions, class attributes cannot be assigned new values, such as:Person.prototypeIt is such a read-only class property.

Differences between classes and custom types:

  • Function declarations can be promoted, whereas class declarations are notletStatement similar, cannot be promoted; They remain in a temporary dead zone until the declaration is actually executed.
  • All code in a class declaration automatically runs in strict mode, and you cannot force code out of strict mode execution.
  • In custom methods, you need to passObject.defineProperty()Method manually specifies that a method is not enumerable; In a class, all methods are not enumerable.
  • Each class has a name[[Construct]]Internal method by keywordnewCall those that do not contain[[Construct]]Can cause the program to throw an error.
  • Use the except keywordnewCalling the constructor of a class in any other way causes the program to throw an error.
  • Changing the name of a class in a class results in an error.

Now that we know the difference between classes and custom types, we can write the equivalent code using syntax other than classes:

// ES5 equivalence class
let Person = (function() {
  'use strict'
  const Person = function(name) {
    if (typeof new.target === 'undefined') {
      throw new Error('This constructor must be called with the keyword new')}this.name = name
  }
  Object.defineProperty(Person.prototype, 'sayName', {
    value: function () {
      if (typeof new.target ! = ='undefined') {
        throw new Error('This method cannot be called with the keyword 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
Copy the code

Class expression

Both classes and functions exist in two forms: declarative and expressive

// Class expression form
let Person = class {
  constructor (name) {
    this.name
  }
  sayName () {
    console.log(this.name)
  }
}
Copy the code

As you can see from the above code, class declarations and class expressions do much the same thing, albeit in slightly different ways, and neither is promoted the way function declarations and function expressions are. At the top, our class declaration is an anonymous class expression. Classes, like functions, can be defined as named expressions:

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
Copy the code

Classes and singleton

There is another way to use class expressions: you can create a singleton by calling the class constructor immediately, calling the class expression with new, followed by calling the expression with a pair of parentheses:

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

Class of first class citizens

A first-class citizen is a value that can be passed into, returned from, and assigned to a variable.

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

Accessor properties

In addition to creating your own properties in the constructor, you can also define accessor properties directly on the prototype of the class.

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
Copy the code

To better understand the accessor properties of a class, we use ES5 code to rewrite the accessor part of the code:

// omit other parts
Object.defineProperty(Person.prototype, 'message', {
  enumerable: false.configurable: true.get: function () {
    return this.animal.message
  },
  set: function (val) {
    this.animal.message = val
  }
})
Copy the code

By comparison, the syntax for using ES6 classes is much cleaner than the ES5 equivalent.

Computable member name

There are also many similarities between class and object literals, and the use of computable names is supported for class methods and accessor properties.

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
Copy the code

Generator method

In a class, generators can also be defined with an asterisk (*) in front of the method name, just like object literals.

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
Copy the code

While generator methods are useful, it is more useful to define a default iterator for a class that is simply a collection of values.

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
}
Copy the code

Static members

In ES5 and earlier versions, it was a common pattern to emulate static members by adding methods directly to constructors:

function PersonType (name) {
  this.name = name
}
// Static method
PersonType.create = function (name) {
  return new PersonType(name)
}
// Instance method
PersonType.prototype.sayName = function () {
  console.log(this.name)
}
const person = PersonType.create('AAA')
person.sayName() // AAA
Copy the code

In ES6, the class syntax simplifies the process of creating static members by using the formal static annotation static before the method or accessor attribute name. Note: Static members can only be accessed in a class, not an instance

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
Copy the code

Inheritance and derived classes

Prior to ES6, implementing inheritance and custom types was no small undertaking, and strict inheritance required multiple steps.

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
Copy the code

Code analysis: To implement inheritance using the pre-ES6 syntax, we must override square. prototype with a new object created from Rectangle. Prototype and call the Rectangle. Call () method. Inheritance is easy to implement in ES6 thanks to the advent of classes, which use the familiar keyword extends to specify the functions that a class inherits. The stereotype is called automatically, and the base class constructor is accessible by calling the super() method, so we use ES6 class syntax to rewrite the example above:

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
Copy the code

Note: Classes that inherit from other classes are called derived classes. If a constructor is specified in a derived class, super() must be called or an error will be thrown. If you do not choose to use the constructor, super() is automatically called when a new instance is created, passing in all the arguments as follows:

// Omit other code
class Square extends Rectangle {
  // There is no constructor
}
/ / equivalent to the
class Square extends Rectangle {
  constructor(... args) {super(... args) } }Copy the code

Class method shadowing

Note: Methods in a derived class always override methods of the same name in the base class.

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
Copy the code

Code analysis: because the Square class have defined the getArea () method, which can not call in the instance of Square Rectangle. The prototype. The getArea () method. If we want to call a method of the same name in the base class, we can use 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
Copy the code

Static member inheritance

If there are static members in the base class, they can also be used in derived classes.

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, because square2 is an instance of Rectangle, not Square
Copy the code

A class derived from an expression

Perhaps the most powerful aspect of ES6 is the ability to derive classes from expressions. As long as an expression can be parsed into a function with a [[Construct]] property and stereotype, it can be derived using 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
Copy the code

Rectangle is a typical ES5-style Rectangle Constructor. Square is a class that can be inherited directly by the Square class because Rectangle has [[Constructor]] attributes.

Extends dynamic inheritance

The power of extends allows classes to inherit from any type of expression, creating more possibilities, such as dynamically determining the inheritance target of a class.

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(a){
  constructor (length) {
    super(length, length)
  }
}
var square = new Square(3)
console.log(square.getArea())             / / 9
console.log(square instanceof Rectangle)  // true
Copy the code

We can already see from the above example that we can use a function call to dynamically return the class to be inherited, so we can extend it to create different inherited mixin methods:

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(a)this.width = length
    this.height = length
  }
}
const square = new Square(3)
console.log(square.getArea())     / / 9
console.log(square.normalize())   // {width:3, height: 3}
Copy the code

Code analysis: Instead of returning a single object directly from the getBaseClass() method, we define a mixin() method that merges the properties of multiple objects and returns them, then extends the object using extends, The normalize() method and AreaMixin getArea() method are inherited from NormalizeMixin objects.

Inheritance of built-in objects

In ES5 and earlier versions, it was almost impossible to create our own special arrays by inheritance, for example:

// The behavior of the built-in array
const colors = []
colors[0] = 'red'
console.log(colors.length)  / / 1
colors.length = 0
console.log(colors[0])      // undefined
// Try ES5 syntactic inheritance arrays
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'
Copy the code

Code analysis: We can see that the two printable results of our own special Array are not what we expected, because Array inheritance implemented through traditional JavaScript inheritance does not inherit related functionality from array.apply () or stereotype assignment.

Since ES6 introduced the class syntax, we can easily implement our own special arrays using the ES6 class syntax:

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

Symbol. Species attributes

One utility of built-in object inheritance is that the methods of instances returned in the built-in object will automatically return instances of derived classes. For example, if we have a derived class MyArray that inherits from Array, methods like slice() will also return an instance of 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
Copy the code

The symbol. species property is one of many internal symbols that are used to define the static accessor property of the return function. The returned function is a constructor that must be used whenever an instance of the class is created in the method of the instance. The following built-in types have the symbol.species attribute defined:

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

Constructor new.target

We’ve seen before how new.target and its value change depending on how the function is called. In the constructor of a class, we can also use new.target to determine how the class is called.

class Rectangle {
  constructor (width, height) {
    this.width = width
    this.height = height
    console.log(new.target === Rectangle)
  }
}
const rect = new Rectangle(3.4)  / / output true
Copy the code

When the class is inherited, however, new.target is equal to the derived class:

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)
/ / output is false
/ / output true
Copy the code

By the nature of new.target, we can define an abstract base class: that is, it cannot be instantiated directly and must be used by inheritance.

class Shape {
  constructor () {
    if (new.target === Shape) {
      throw new Error('Cannot be instantiated directly')}}}class Rectangle extends Shape {
  constructor (width, height) {
    super(a)this.width = width
    this.height = height
  }
}
const rect = new Rectangle(3.4)
console.log(rect instanceof Shape) // true
Copy the code

Improved array functionality

The section on stereotype arrays has not been cleaned up yet.

Create an array

background

Before ES6, there were only two ways to create arrays, one using Array constructors and the other using Array literals. If we want to convert an array-like object (an object with a numeric index and a length attribute) to an array, the options are limited and we often need to write extra code. In this context, two new methods, array. of and array. from, are added to ES6.

Array.of

Before ES6, creating arrays using the Array constructor had a number of quirks that were confusing, such as:

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'
Copy the code

The act of confusion:

  • If you giveArrayThe constructor passes in a numeric value, then the array’slengthProperty is set to this value.
  • If a value of a non-numeric type is passed in, that value becomes the only entry in the target data.
  • If multiple values are passed in, they become elements of the array, regardless of whether they are numeric or not.

To address the above, ES6 introduces the array.of () method to solve this problem.

Array.of() always creates an Array containing all the arguments, no matter how many arguments there are and what type they are.

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'
Copy the code

Array.from

JavaScript does not support converting non-array objects to real arrays directly. Arguments is an array object of sorts.

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) // Output [1, 2, 3, 4]
Copy the code

The above method uses a for loop to create a new array, then iterates over the arguments and pushes them one by one into the array, and returns. In addition to the above code, we can use another way to achieve the same goal:

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

Although we provide ES5 with two different schemes to convert class arrays to arrays, ES6 gives us a new semantically clear and syntactically concise method array.from ()

The ‘array.from ()’ method takes an iterable or array-like object as its first argument.

function doSomething () {
  let args = Array.from(arguments)
  console.log(args)
}
doSomething(1.2.3.4) // Output [1, 2, 3, 4]
Copy the code

Array.from mapping conversion

As the second argument to the array.from () method, you can provide a mapping function that converts each value of an array-like object into another form, and finally stores the results in order in the corresponding index of the result Array.

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

As we saw above, we use a mapping function (value) => value +1, respectively for our argument +1, and the final result then [2, 3, 4]. Alternatively, if our mapping function is dealing with objects, we can pass an object to the third argument to array.from () to handle this pointing problems in the mapping function.

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]
Copy the code

Array.from converts an iterable

Array.from() converts all objects with symbol.iterator properties to arrays.

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

Note: If an object is both an array-like object and an iterable, array. from takes precedence in deciding which value to convert based on the iterator.

ES6 array new method

ES6 adds several new methods for arrays:

  • find()andfindIndex()Method helps us find any value in an array.
  • fill()Method to populate an array with the specified value.
  • copyWithin()Method to help us copy elements in an array, andfill()There are many similarities in the methods.

The find() and findIndex() methods

Both find() and findIndex() take two arguments: one for the callback function and an optional argument that specifies the value of this in the callback function.

Function introduction: The find() and findIndex() methods both look up the value of the callback passed in. The difference is that find() returns the found value, findIndex() returns the found index, and once found, the callback returns true. The find() and findIndex() methods immediately stop searching for the rest.

let numbers = [25.30.35.40.45]
console.log(numbers.find(item= > item >= 35))       / / 35
console.log(numbers.findIndex(item= > item === 35)) / / 2
Copy the code

The fill () method

The find() method can fill up to one array element with a specified value, and when a value is passed in, the fill() method overrides all the values in the array with that value.

let numbers = [1.2.3.4]
numbers.fill(1)
console.log(numbers.toString()) // [1, 1, 1, 1]
Copy the code

If you want to change only one part of the array, you can pass in the optional start index (second argument) and no end index (third argument), as follows:

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]
Copy the code

CopyWithin () method

The copyWithin() method takes two arguments: the index position where the method starts filling the value, and the index position where the value starts copying.

let numbers = [1.2.3.4]
numbers.copyWithin(2.0)
console.log(numbers.toString()) // 1, 2, 1, 2
Copy the code

Numbers. CopyWithin (2, 0) can be read as follows: Use the corresponding value at index 0-1, except the start copy and paste value at index 2-3. By default, if the third argument of copyWithin() is not provided, the default copy goes all the way to the end of the array, and the values of 3 and 4 are overwritten, resulting in [1, 2, 1, 2].

let numbers = [1.2.3.4]
numbers.copyWithin(2.0.1)
console.log(numbers.toString()) // 1, 2, 1, 4
Copy the code

Code analysis: According to the nature of the copyWithin() method, we pass a third argument to end the copy at position 1, i.e. only the value of 3 in the array is replaced by 1, other values remain unchanged, i.e. the result is: [1, 2, 1, 4]

Promise and asynchronous programming

Background on asynchronous programming

JavaScript engines are built around the concept of a single-threaded event loop that allows only one block of code to execute at a time, so you need to keep track of what code is about to run. That code is put in a task queue, to which it is added whenever a piece of code is ready to execute. Whenever a piece of code in the JavaScript engine finishes executing, the event loop executes the next task in the queue, which is the program in the JavaScript engine that monitors the code execution and manages the task queue.

The event model

When the user clicks a button or presses a key on a keyboard, an event like onClick is triggered, which adds a new task to the task queue in response to the user’s action. This is the most basic asynchronous programming pattern in JavaScript, and the event handler is not executed until the event is triggered, in the same context as defined.

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

The event model is well suited for handling simple interactions, but concatenating multiple independent asynchronous calls makes the program more complex because we have to keep track of the event target for each event.

The callback mode

Node.js improves the asynchronous programming model by popularizing callback functions, which are similar to the event model in that asynchronous code is executed at some point in the future. The difference is that the function called in the callback mode is passed in as an argument, as follows:

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

We can see that the callback pattern is more flexible than the event model, so it is easier to link multiple calls through the callback pattern:

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! ')})})Copy the code

We can see that the nested form of the callback can help us solve many problems, but as the module becomes more complex, the callback pattern needs to be nested more and more functions, creating a callback hell like this:

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)
      })
    })
  })
})
Copy the code

Promise based

A Promise is a placeholder for the result of an asynchronous operation. Instead of subscribing to an event or passing a callback to the target function, the function returns a Promise.

The Promise lifecycle

Each Promise goes through a short life cycle: it is in a pending state, the operation is not yet complete, so it is also unprocessed, and once the operation is completed, the Promise becomes processed. After the operation is complete, the Promise may enter one of two states:

  • Fulfilled: The asynchronous operation succeeds.
  • Rejected: The asynchronous operation did not complete successfully due to a program error or some other reason.

According to the states described above, the Promise’s internal attribute [[PromiseState]] is used to represent three states: Pending, depressing, and Rejected. This property is not exposed to the Promise object, so it cannot be coded to detect the Promise state.

Promise. Then () method

We already know that the Promise will enter one of the Promise and Rejected after the operation is Fulfilled, and the Promise provides the promise.then () method. There are two parameters. The first is the function to be called when the Promise state becomes depressing, and the second is the function to be called when the Promise state becomes rejected. Both of the two parameters are optional.

If an object implements the above.then() method, it is called a ‘thenable’ object.

let Promise = readFile('example.pdf')
// Provide both completed and rejected callbacks
Promise.then(function(content) {
  console.log('complete')},function(err) {
  console.log(err.message)
})
// Only the completed callback is provided
Promise.then(function(content) {
  console.log('complete')})// Only rejected callbacks are provided
Promise.then(null.function(err) {
  console.log(err.message)
})
Copy the code

Promise. The catch () method

A Promise also has a catch() method, which is equivalent to passing it only the then() method of the rejection handler, so the catch() equivalent to the last example above looks like this:

promise.catch(function(err) {
  console.log(err.message)
})
/ / equivalent to the
Promise.then(null.function(err) {
  console.log(err.message)
})
Copy the code

The then() and catch() methods are used together to better handle the results of asynchronous operations. This system clearly indicates whether the result of an operation is a success or failure, which is better than events and callbacks. If events are used, they are not actively triggered when an error is encountered; If you use callback functions, you must remember to check for error arguments every time. If you don’t add a rejection handler to the Promise, all failures are automatically ignored.

Create unfinished promises

New promises can be created using the Promise constructor, which takes a single argument: the executor function that contains the code that initializes the Promise. The executor function takes two arguments, resolve and reject. The resolve function is called on successful execution, and the reject function is called on failure.

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)
})
Copy the code

Create the processed Promise

The promise.resolve () method takes a single argument and returns a completed Promise. The method never has a rejection state, so the Promise’s rejection handler is never called.

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

A rejected Promise can be created using the promise.reject () method, which is similar to the promise.resolve () method, except that it creates a rejected Promise.

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

Thenable objects that are not Promises

Both the promise.resolve () and promise.reject () methods can accept non-promise thenable objects as arguments. If a non-promise thenable object is passed in, these methods create a new Promise and are called in the then() function. A common object that has a then() method and accepts resolve and reject is a non-Promise Thenable object.

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

Actuator error

If an error is thrown inside the executor, the Promise rejection handler is invoked.

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

In this code, the executor intentionally throws an error. Each executor has an implicit try-catch block, so the error is caught and passed to the rejection handler.

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
})
Copy the code

Tandem Promise

Each time we call the then() or catch() methods we actually create and return another Promise, and the second Promise is resolved only when the first Promise is completed or rejected. This gives us a way to concatenate promises to implement more complex asynchronous features.

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

If we take the above example apart, this would be the case:

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

Errors are caught in concatenated promises

We already know that a Promise completion handler or rejection handler can have errors that can be caught in the Promise chain:

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

Errors can be caught not only in the then() method, but also in the catch() method:

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
})
Copy the code

The Promise chain returns a value

An important feature of the Promise chain is the ability to pass values to downstream promises.

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
})
Copy the code

Return the Promise in the Promise chain

As we saw in the example above, we can pass a value to a downstream Promise, but what if we return another Promise object? In fact, depending on whether the Promise is fulfilled or rejected, completion calls then() and rejection calls 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= > {
  // then()
  console.log(res)            / / 2
})

p1.then(res= > {
  console.log(res)            / / 1
  return p3
}).catch((error) = > {
  // p3 reject, call catch()
  console.log(error.message)  // error p3
})
Copy the code

Respond to each Promise

Promise. All () method

Features: The promise.all () method takes a single argument and returns a Promise, which must be an iterable of one or more promises (such as an array). The Promise will only be returned when all of the Promise objects in the argument have been resolved. Another point worth noting is that the Promise return values are stored in the order promised in the parameter array, so they can be accessed in the final result’s Promise array based on the index of the Promise location in the parameter.

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: corresponds to p1 result
  console.log(res[1]) // 2: corresponds to p2 result
  console.log(res[2]) // 3: result corresponding to p3
})
Copy the code

Promise. Race () method

Features: The promise.race () and promise.all () methods are identical for parameters, but differ slightly in behavior and results: The promise.race () method takes an array of parameters, and as soon as any Promise in the array is fulfilled, the promise.race () method returns, so the promise.race () method has only one result, the result of the Promise that was resolved first.

let p1 = new Promise((resolve, reject) = > {
  setTimeout((a)= > {
    resolve(1)},100)})let p2 = new Promise((resolve, reject) = > {
  resolve(2)})let p3 = new Promise((resolve, reject) = > {
  setTimeout((a)= > {
    resolve(3)},100)})let pRace = Promise.race([p1, p2, p3])
pRace.then(res= > {
  console.log(res) // 2 corresponds to p2
})
Copy the code

Since the Promise inherit

Promises, like other built-in types, can be derived from other classes as a base class.

class MyPromise extends Promise {
  // Derive Promise and add success and failure methods
  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
})
Copy the code

The Proxy and Reflection apis

An array of problems

Before ES6, we couldn’t emulate the behavior of JavaScript array objects with our own defined objects: Assigning values to specific elements of an array affected the length property of the array, and we could modify array elements with the Length property.

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']
Copy the code

Proxy and reflection

Proxies: Proxies can intercept low-level object operations on targets within the JavaScript engine, which, when intercepted, trigger trap functions in response to specific actions. Reflection: The Reflection API takes the form of a Reflect object, in which the default properties of the methods are the same as the underlying operations, and proxies can override these operations, with each proxy trap corresponding to a Reflect method with the same name and parameters.

Agent trap Overwrite the features The default features
get Read a property value Reflect.get
set Write a property Reflect.set
has The in operator Reflect.has
apply Call a function Reflect.apply()
deleteProperty The delete operator Reflect.deleteProperty()
construct Call a function with 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. The keys (), Object. GetOwnPropertyNames () and Object getOwnPropertySymbols () Reflect.ownKeys()

Create a simple proxy

Creating a Proxy with the Proxy constructor requires passing in two parameters: the target and the handler.

Handlers are objects that define one or more traps. In the agent, except for traps defined specifically for operations, all operations use default properties, which means that a handler that does not use any traps is equivalent to a simple forwarding agent.

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
Copy the code

Using set Traps

The set trap accepts four parameters:

  • trapTarget: The object used to accept attributes (the target of the proxy).
  • key: Attribute key to write (string orSymbolType).
  • value: The value to be written to the property.
  • receiver: The object on which the operation takes place.

Features: Reflect.set() is the corresponding reflection method and default feature of a set trap. It takes the same four parameters as the set proxy trap for easy use in traps. Property should return true if trap is set, false otherwise.

Example: If we want to create an object whose attribute value is a number, each new attribute in the object must be validated and an error must be thrown if it is not a number.

let target = {
  name: 'target'
}
let proxy = new Proxy(target, {
  // Existing attributes are not detected
  set (trapTarget, key, value, receiver) {
    if(! trapTarget.hasOwnProperty(key)) {if (isNaN(value)) {
        throw new TypeError('Property value must be a number')}}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' // Attribute value is not a number, an error is thrown
Copy the code

Use get traps

The GET trap accepts three arguments:

  • trapTarget: The source object from which the property is read (the target of the proxy).
  • key: Property key to read (string orSymbol).
  • receiver: The object on which the operation takes place.

One of the most common features of JavaScript is that when we try to access a property of an object that doesn’t exist, we don’t get an error but return undefined. If this is not what you want, then you can use get traps to verify the object structure.

let proxy = new Proxy({}, {
  get (trapTarget, key, receiver) {
    if(! (keyin trapTarget)) {
      throw new Error(` properties${key}There is no `)}return Reflect.get(trapTarget, key, receiver)
  }
})
proxy.name = 'proxy'
console.log(proxy.name)  // proxy
console.log(proxy.nme)   // Attribute value does not exist, an error is thrown
Copy the code

Using the HAS trap

The HAS trap takes two arguments:

  • trapTarget: The object from which the property is read (the target of the proxy)
  • key: The property key to check (string orSymbol)

The in operator can be used to check whether an object contains an attribute, returning true if its own attribute or stereotype attribute matches the name or Symbol, and false otherwise.

let target = {
  value: 123
}
console.log('value' in target)    // Own property returns true
console.log('toString' in target) // Prototype property, inherited from Object, also returns true
Copy the code

This shows the properties of the IN operator, which can be changed using the HAS trap:

let target = {
  value: 123.name: 'AAA'
}
let proxy = new Proxy(target, {
  has (trapTarget, key) {
    // Mask the value attribute
    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
Copy the code

Use the deleteProperty trap

The deleteProperty trap takes two arguments:

  • trapTarget: The object to which the attribute is to be deleted (the target of the proxy).
  • key: Attribute key to delete (string orSymbol).

As we all know, the delete operator removes an attribute from an object, returning true on success and false on failure. If there is an object property that cannot be deleted, we can use the deleteProperty trap method to handle it:

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
Copy the code

Use prototype proxy traps

SetPrototypeOf traps accept two arguments:

  • trapTarget: The object that receives the stereotype setting (the target of the proxy).
  • proto: Objects used as prototypes.getPrototypeOfThe trap accepts one argument:
  • trapTarget: Accepts the object that gets the stereotype (the target of the proxy).

As we’ve seen before, ES6 has added the Object.setPrototypeof () method, which complements the object.getPrototypeof () method in ES5. SetPrototypeOf () traps and getPrototypeOf() traps can be used when we want to do something when an object is prototyped or read.

let target = {}
let proxy = new Proxy(target, {
  getPrototypeOf(trapTarget) {
    // Must return object or null
    return null
  },
  setPrototypeOf(trapTarget, proto) {
    // As long as the return value is not false, the prototype is set successfully.
    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, {})             // The setting succeeded
Object.setPrototypeOf(proxy, {})              // Throw an error
Copy the code

Code analysis: The above code highlights the behavior differences between target and proxy:

  • Object.getPrototypeOf()Approach totargetReturns the value, while givesproxyReturns thenullThis is becauseproxyWe usegetPrototypeOf()Trap.
  • Object.setPrototypeOf()Method success istargetSet up the prototype while inproxyBecause we’re usingsetPrototypeOf()Trap, manual returnfalseSo setting the prototype was unsuccessful.

From the above analysis, we can get the default behavior of Object.getPrototypeof () and object.setPrototypeof () :

let target = {}
let proxy = new Proxy(target, {
  getPrototypeOf(trapTarget) {
    // Must return object or null
    return Reflect.getPrototypeOf(trapTarget)
  },
  setPrototypeOf(trapTarget, proto) {
    // As long as the return value is not false, the prototype is set successfully.
    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, {})             // The setting succeeded
Object.setPrototypeOf(proxy, {})              // The setting succeeded
Copy the code

The difference between the two groups of methods

The reflect.getPrototypeof () and reflect.setPrototypeof () methods appear to perform similar operations to object.getPrototypeof () and object.setPrototypeof (), But there are a few differences:

  1. Reflect.getPrototypeOf()Methods andReflect.setPrototypeOf()Method, which gives developers access to operations previously operated only internally[[GetPrototypeOf]]and[[SetPrototypeOf]]Permissions. whileObject.getPrototypeOf()andObject.setPrototypeOf()Methods are advanced operations that were created to be easy for developers to use.
  2. If the parameter passed is not an objectReflect.getPrototypeOf()Will throw an error, andObject.getPrototypeOf()Method casts the parameter to an object before operation.
let result = Object.getPrototypeOf(1)
console.log(result === Number.prototype)  // true
Reflect.getPrototypeOf(1)                 // Throw an error
Copy the code
  1. Object.setPrototypeOf()Method uses a Boolean value to indicate whether the operation was successful and returns on successtrueOn failurefalse. whileReflect.setPrototypeOf()An error is thrown when the setting fails.

Use object extensible traps

Prior to ES6 objects had two methods to fix object extensibility: Object.isextensible () and Object.preventExtensions(), which in ES6 can be intercepted by isExtensible() and preventExtensions() traps in the proxy and call the underlying Object.

  • isExtensible()The trap returns a Boolean value indicating whether the object is extensible and takes a unique argumenttrapTarget
  • preventExtensions()The trap returns a Boolean value indicating whether the operation was successful and takes a unique argumenttrapTarget

The following example is the default behavior for isExtensible() and 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
Copy the code

Now if there is a case where we want to disable object.preventExtensions () for the proxy, we can modify the above example to look like this:

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
Copy the code

Comparison of the two groups of methods:

  • Object.preventExtensions()Regardless of whether the object passed in is an object, it always returns the argument, andReflect.isExtensible()Method throws an error if passed a non-object.
  • Object.isExtensible()Returns when a non-object value is passedfalseAnd theReflect.isExtensible()An error is thrown.

Use property descriptor traps

The Object.defineProperty trap accepts three arguments:

  • trapTarget: The object for which attributes are to be defined (the target of the proxy)
  • key: Key of the property.
  • descriptor: The descriptor object for the property.

Object. GetOwnPropertyDescriptor trap accepts two arguments:

  • trapTarget: The object from which the property is to be obtained (the target of the proxy).
  • key: Key of the property.

Can be used in the proxy defineProperty and intercept getOwnPropertyDescriptor trap function Object. The defineProperty () and Object. The getOwnPropertyDescriptor () method call. The following example shows the default behavior of the defineProperty and getOwnPropertyDescriptor traps.

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
Copy the code

Object.defineproperty () adds a restriction

DefineProperty trap returns a Boolean value indicating whether the operation was successful, or true indicating that Object.defineProperty() was successfully executed; Object.defineproperty () throws an error when false is returned. Suppose we now have a requirement that the attribute key of an object cannot be set to the Symbol attribute. We can use the defineProperty trap to do this:

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')
// Throw an error
Object.defineProperty(proxy, nameSymbol, {
  value: 'BBB'
})
Copy the code

Object. GetOwnPropertyDescriptor () to add restrictions

Whatever Object is passed to the object.defineProperty () method as the third argument, Only the properties Enumerable, 64x, value, Writable, GET and set will appear in the descriptor object passed to the defineProperty trap. Also means that the Object. GetOwnPropertyDescriptor () method always returns the above several attributes.

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
Copy the code

Note: The return value of the getOwnPropertyDescriptor() trap must be null, undefined, or an object. If an object is returned, the properties of the object can only be Enumerable, 64x, value, writable, GET, and set. Any property that is not allowed can cause an error.

let proxy = new Proxy({}, {
  getOwnPropertyDescriptor(trapTarget, key) {
    return {
      name: 'proxy'}}})// Throw an error
let descriptor = Object.getOwnPropertyDescriptor(proxy, 'name')
Copy the code

Comparison of two groups of methods:

  • Object.defineProperty()Methods andReflect.defineProperty()The only difference between methods is the return value, which only returns the first parameter. The latter returns a value dependent on the operation or on successtrueOn failure, returnfalse.
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
Copy the code
  • Object.getOwnPropertyDescriptor()The method passes in a raw value as a parameter, which is internally cast to an object. whileReflect.getOwnPropertyDescriptor()Method passes in a raw value and an error is thrown.
let descriptor1 = Object.getOwnPropertyDescriptor(2.'name')
console.log(descriptor1)  // undefined
// Throw an error
let descriptor2 = Reflect.getOwnPropertyDescriptor(2.'name')
Copy the code

Use the ownKeys trap

The ownKeys proxy trap intercepts the internal method [[OwnPropertyKeys]], whose behavior we override by returning the value of an array. The array is used for the Object. The keys (), Object, getOwnPropertyNames (), Object. GetOwnPropertySymbols () and the Object. The assign () four methods, The object.assign () method uses an array to determine which properties to copy. The only argument the ownKeys trap takes is the target of the operation, and the return value is an array or array-like object, otherwise an error will be thrown.

Differences between several methods:

  • Reflect.ownKeys(): Returns an array containing the key names of all objects’ own properties, including string types andSymbolType.
  • Object.getOwnPropertyNames()andObject.keys(): is removed from the array returnedSymbolType.
  • Object.getOwnPropertySymbols(): Returns an array with string types excluded.
  • Object.assign(): String andSymbolAll types are supported.

Assuming we don’t want to specify the rule’s attribute key when using the above methods, we can use the reflect.ownkeys () trap to do so:

let proxy = new Proxy({}, {
  ownKeys (trapTarget) {
    return Reflect.ownKeys(trapTarget).filter(key= > {
      // Exclude keys with _ at the beginning of attributes
      return typeofkey ! = ='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)']
Copy the code

Use the Apply and Construct traps

The Apply trap accepts the following parameters:

  • trapTarget: The function being executed (the target of the agent).
  • thisArg: Inside when a function is calledthisThe value of the.
  • argumentsList: An array of arguments passed to the function.

The construct trap function takes the following parameters:

  • trapTarget: The function being executed (the target of the agent).
  • argumentsList: An array of arguments passed to the function.

The Apply and Construct trap functions are the only two of all proxy trap functions whose proxy target is a function. As we have seen before, functions have two internal methods [[Call]] and [[Construct]]. When new is called, the [[Construct]] method is executed, and when new is not called, the [[Call]] method is executed. The following example is the default behavior for apply and Construct traps:

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
Copy the code

Validate function parameters

Suppose we have a requirement for a function whose arguments can only be numeric. This can be implemented using either the Apply trap or the Construct trap:

function sum(. values) {
  return values.reduce((prev, current) = > prev + current, 0)}let sumProxy = new Proxy(sum, {
  apply(trapTarget, thisArg, argumentsList) {
    argumentsList.forEach(item= > {
      if (typeofitem ! = ='number') {
        throw new TypeError('All arguments must be numeric')}})return Reflect.apply(trapTarget, thisArg, argumentsList)
  },
  construct (trapTarget, argumentsList) {
    throw new TypeError('This function cannot be called by new')}})console.log(sumProxy(1.2.3.4.5))    / / 15
let proxy = new sumProxy(1.2.3.4.5) // Throw an error
Copy the code

The constructor is not called with new

In the previous section, we learned about the new.target meta-attribute, which is a reference to a function called with new. You can use the value of new.target to determine if the function is called with new:

function Numbers(. values) {
  if (typeof new.target === 'undefined') {
    throw new TypeError('This function must be called with 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)          / / an error
Copy the code

Suppose we have one of the above functions that must be called by new, but we still want it to be used as a non-new call. In this case, we can use the Apply trap:

function Numbers(. values) {
  if (typeof new.target === 'undefined') {
    throw new TypeError('This function must be called with 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]
Copy the code

Overrides the abstract base class constructor

The construct trap also accepts a third optional argument function that is used as the value of new. Target inside the constructor.

Suppose we now have a scenario where we have an abstract base class that must be inherited, but we still don’t want to. We can use the Construct trap to implement this:

class AbstractNumbers {
  constructor(... values) {if (new.target === AbstractNumbers) {
      throw new TypeError('This function must be inherited')}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
Copy the code

Callable class constructor

We all know that we must use new to Call the constructor of a class because the inner method [[Call]] of the class constructor is specified to throw an error, but we can still use the Apply proxy trap to Call the constructor without using new:

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

Revocable agency

In all of our previous proxy examples, all were non-cancelable proxies. Sometimes, however, we want to control the Proxy so that it can be revoked if needed. In this case, we can use proxy.revocable () to create a revocable Proxy. This method takes the same parameters as the Proxy constructor and returns an object with the following properties:

  • proxy: revocable proxy object.
  • revoke: Undoes the function to be called by the proxy. When callingrevoke()Function, cannot passproxyWith further operations, any attempt to interact with the proxy object triggers the proxy trap to throw an error.
let target = {
  name: 'AAA'
}
let { proxy, revoke } = Proxy.revocable(target, {})
console.log(proxy.name) // AAA
revoke()
console.log(proxy.name) // Throw an error
Copy the code

Solving array problems

As we’ve seen before, we couldn’t fully simulate the behavior of arrays prior to ES6, as in the following example:

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']
Copy the code

Two important behaviors that cannot be simulated:

  • Increases when a new element is addedlengthThe value of the
  • To reducelengthCan delete elements

Check array index

If ToString(ToUnit32(P)) is equal to P and ToUnit32(P) is not equal to 2³²-1, the canonical conditions are met to determine whether an attribute is an array index.

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)}Copy the code

Code analysis: The toUnit32() function converts a given value to an unsigned 32-bit integer using the algorithm described in the specification; The isArrayIndex() function converts the key to a uint32 structure and then performs a comparison to determine whether the key is an array index.

Increment length when adding a new element

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 
Copy the code

Reducing the value of length removes elements

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
Copy the code

Implement MyArray on class

If we want to create a class that uses proxies, the simplest way is to define the class as usual and return a proxy in the constructor, like this:

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

With the above concepts in mind, we can use proxies to create a custom array class:

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
Copy the code

Code summary: While it is easy to return a proxy from the class constructor, it also means that a new proxy is created for every instance created.

Use the agent as a prototype

As mentioned in the previous section: you can return a proxy from a class constructor, but create a new proxy for each instance you create. This problem can be solved by using the proxy as a prototype so that all instances share a single proxy.

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
Copy the code

Code analysis: Call the object.defineProperty () method and pass newTarget to create a proprietary property named name. The operation to define a property on an Object does not need to operate on the Object’s prototype, so the defineProperty trap in the proxy is never called. As you can see, this approach limits the power of the agent as a prototype, but there are several pitfalls that can be very useful.

Use get traps on prototypes

The operation that calls the internal method [[Get]] to read the property now looks for its own property, and if it does not find its own property with the specified name, it continues to look in the prototype until there is no more prototype to look for. If you set a Get trap, you can catch the trap of looking for properties on the prototype.

let target = {}
let newTarget = Object.create(new Proxy(target, {
  get (trapTarget, key, receiver) {
    throw new ReferenceError(`${key}Does not exist. `)
  }
}))
newTarget.name = 'AAA'
console.log(newTarget.name) // AAA
console.log(newTarget.nme)  // Throw an error
Copy the code

Code analysis: We use a proxy as a prototype to create a new object. When it is called, if the given key does not exist on it, then the GET trap will throw an error; The name attribute exists, so it is read without invoking the get trap on the prototype.

Use set traps on prototypes

The inner method [[Set]] also checks to see if the target object has a property of its own, and continues to look for it on the prototype if it does not. But here’s the tricky part: assigning an attribute with the same name, regardless of whether it exists on the stereotype, defaults to creating it in the instance:

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'                        // Trigger a set trap
console.log(thing.name)                   // AAA
console.log(thing.hasOwnProperty('name')) // true
thing.name = 'BBB'                        // Set traps are not triggered
console.log(thing.name)                   // BBB
Copy the code

Use the HAS trap on the prototype

The HAS trap is called only when you search for proxy objects on the stereotype chain, and when you use a proxy as a stereotype, it is called only when the specified name has no corresponding property of its own.

let target = {}
let thing = Object.create(new Proxy(target, {
  has (trapTarget, key) {
    return Reflect.has(trapTarget, key)
  }
}))
console.log('name' in thing)  // false triggers the has trap on the prototype
thing.name = 'AAA'
console.log('name' in thing)  // true, does not trigger the has trap on the prototype
Copy the code

Use the proxy as a prototype for the class

Since the prototype property of a class is not writable, you can’t directly modify a class to use a proxy as a prototype, but you can use inherited methods to trick a class into thinking it can use a proxy as its prototype.

function NoSuchProperty () {

}
NoSuchProperty.prototype = new Proxy({}, {
  get(trapTarget, key, receiver) {
    throw new ReferenceError(`${key}There is no `)}})let thing = new NoSuchProperty()
console.log(thing.name) // Throw an error
Copy the code

This code is an ES5-style type definition. Next, we need to use the extends syntax of ES6 to make the class implement inheritance:

function NoSuchProperty () {

}
NoSuchProperty.prototype = new Proxy({}, {
  get(trapTarget, key, receiver) {
    throw new ReferenceError(`${key}There is no `)}})class Square extends NoSuchProperty {
  constructor (width, height) {
    super(a)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 // Throw an error
Copy the code

The Square class inherits NoSuchProperty, so it has a proxy in its prototype chain, and then creates a Shape object that is a new instance of Square and has two properties of its own: Width and height. When we access a nonexistent Length property on our Shape instance, we look it up in the prototype chain, which triggers a GET trap and throws an error.

Encapsulate code with modules

What is a module

Modules are JavaScript code that runs automatically in strict mode and has no way to exit. As opposed to a shared-everything architecture, it has the following characteristics:

  • Variables created at the top of a module are not automatically added to the global sharing scope, but exist only in the top-level scope of the module.
  • Modules must export elements that can be accessed by external code, such as variables or functions.
  • A module can also import bindings from other modules.
  • At the top of the module,thisThe value isundefined.

Basic syntax for exporting

You can use the export keyword to expose a portion of published code to other modules.

// 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
  }
}
// The module is private and cannot be accessed externally
function privateFunc (num1, num2) {
  return num1 + num2
}
Copy the code

Basic syntax for imports

Functions imported from a module can be accessed from another module through the import keyword. The two parts of the import statement are the identifier to be imported and the module from which the identifier is imported. The following example is the basic form of an import statement:

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

Note: When importing a binding from a module, it behaves as if it were defined using const. As a result, we cannot define another variable with the same name, use an identifier before an import statement, or change the value of the binding.

Import a single binding and import multiple bindings

// Import only one
import { sum } from './math.js'
sum(1.2)

// Import multiple
import { sum, minus } from './math.js'
sum(1.2)
minus(1.2)
Copy the code

Import the entire module

In particular, the entire module can be imported as a single object, and all exports can then be used as properties of the object:

import * as Math from './math.js'
Math.sum(1.2)
Math.minus(1.2)
Copy the code

Note:

  • No matter inimportThe number of times a module is written in the statement, the module is always executed only once, because after the import module is executed, the instantiated module is kept in memory as long as the other oneimportStatements that reference it can be reused.
// The code in math.js executes only once
import { sum } from './math.js'
import { minus } from './math.js'
Copy the code
  • exportandimportStatements must be used in addition to other statements and functions, where an error is reported.
if (flag) {
  / / an error
  export flag 
}
function tryImport() {
  / / an error
  import * as Math from './math.js'
}
Copy the code

Rename when exporting and importing

As we saw above, an exported binding is like a variable defined by const and cannot be changed. If there is a binding with the same name between multiple modules, in this case we can use AS to alias the binding to avoid the same name.

// math.js alias when exporting
function sum(num1, num2) {
  return num1 + num2
}
export {
  sum as SUM
}

// math.js alias at import time
import { SUM as sum  } from './math.js'
console.log(typeof SUM) // undefined
sum(1.2)
Copy the code

The default value for the module

The default value of a module refers to a single variable, function, or class specified by the default keyword. Only one default value can be set for each module. If the default keyword is used multiple times, an error will be reported.

// example.js exports the default value
export default function (num1, num2) {
  return num1 + num2
}
// example.js imports default values
import sum from './example.js'
sum(1.2)
Copy the code

Note: Importing default and non-default values can be used interchangeably, for example, exporting example.js:

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

Import example. Js:

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

Re-export a binding

Sometimes we might re-export content that we have already imported, as follows:

import { sum } from './example.js'
export { sum }
// Can be shortened to
export { sum } from './example.js'
// Abbreviate + alias
export { sum as SUM } from './example.js'
// Re-export all
export * from './example.js'
Copy the code

Unbound import

Unbound imports are most likely to be used to create polyfills and shim.

Although we already know that the top-level management, functions, and classes in a module do not automatically appear in the global scope, this does not mean that the module cannot access the global scope. For example, if we wanted to add the pushAll() method to all arrays, we could export array.js without binding:

Array.prototype.pushAll = function (items) {
  if (!Array.isArray(items)) {
    throw new TypeError(The 'argument must be an array. ')}return this.push(... items) }Copy the code

Import array.js without binding:

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

Load module

As we all know, using a script file in a Web browser can be done in one of three ways:

  • inscriptElement throughsrcProperty specifies an address for the load code to loadjsThe script.
  • willjsDoes the code embedsrcProperties of thescriptElement.
  • throughWeb WorkerorService WorkerLoad and executejsThe code.

To fully support module functionality, JavaScript extends the script element to load modules by setting type/module:

<script type="module" SRC ="./math.js"></script> // Inline module code <script type="module"> import {sum} from './example.js' sum(1, 2) </script>Copy the code

Order of module loading in Web browser

A module, unlike a script, is unique, and you can use the import keyword to indicate other files that it depends on, which must be loaded into the module to execute properly, so to support this, automatically applies the defer attribute when it executes.

<script type="module" SRC ="./math.js"></script> import {sum} from './math.js' </script> </script> // Execute <script type="module" SRC ="./math1.js">Copy the code

Asynchronous module loading in a Web browser

Async properties can also be applied to modules. Applying async properties to elements causes modules to execute in a script-like manner, with the only difference that all imported resources in the module must be downloaded before the module can execute.

// There is no guarantee which module will be executed first
<script type="module" src="./module1.js" async></script>
<script type="module" src="./module2.js" async></script>
Copy the code

Load the module as the Worker

To support module loading, the developers of the HTML standard added a second parameter to the Worker constructors. The second parameter is an object whose type attribute defaults to script. Type can be set to module to load module files.

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

Browser module specifier parsing

We can see that in all of our previous examples, module specifiers used relative paths, and browsers require module specifiers in one of the following formats:

  • In order to/The resolution at the beginning starts with the root directory.
  • In order to. /Parsing at the beginning starts with the current directory.
  • In order to../Parsing at the beginning starts with the parent directory.
  • URLFormat.
import { first } from '/example1.js'
import { second } from './example2.js'
import { three } from '.. /example3.js'
import { four } from 'https://www.baidu.com/example4.js'
Copy the code

The following seemingly normal module specifiers are actually invalid in the browser:

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

If you think it’s a good one, please give it a star. If you want to read all the notes in the first and second parts, please click here to read the full text

Reading the book “In-depth Understanding of ES6”, sorting out notes (PART 1)