Starting with Prototype - class es and extends in ES6

Keywords: Javascript TypeScript

What is a class

JavaScript is known to have no classes, and classes are just grammatical sugars. This article aims to clarify what grammatical sugars we often hang around our mouths mean.

ES6 versus ES5 Writing

class Parent {
    static nation = 'China'
    
    isAdult = true
    
    get thought() {
        console.log('Thought in head is translate to Chinese.')
        return this._thought
    }
    
    set thought(newVal) {
        this._thought = newVal
    }
    
    constructor(name) {
        this.name = name
    }
    
    static live() {
        console.log('live')
    }

    talk() {
        console.log('talk')
    }
}

This is a very complete writing, we are used to writing a class so easily, so what is the corresponding writing in ES5?

function Parent(name) {
    this.name = name
    this.isAdult = true
}

Parent.nation = 'China'
Parent.live = function() {
    console.log('live')
}
Parent.prototype = {
    get thought() {
        return this._thought
    },
    set thought(newVal) {
        this._thought = newVal
    },
    talk: function() {
        console.log('talk')
    }
}

You can see it clearly

  • The constructor of the Parent class in ES6 corresponds to the constructor Parent in ES5.
  • The instance attributes name and isAdult, regardless of the way they are written in ES6, are still hung under this in ES5.
  • The static properties and methods nation and live modified by the keyword static in ES6 are directly suspended on the class Parent.
  • It is worth noting that getter and setter The tought and method talk are hung on the prototype object Parent.prototype.

How Babel is compiled

We can enter the code into the Babel website Try it out To see the compiled code, this part of our step-by-step compilation process breaks down Babel's compilation process:

Process One

We will only look at the compilation of attributes at this point.
Before compilation:

class Parent {
    static nation = 'China'
    
    isAdult = true
    
    constructor(name) {
        this.name = name
    }
}

After compilation:

'use strict'
  // Encapsulated instanceof operation
  function _instanceof(left, right) {
    if (
      right != null &&
      typeof Symbol !== 'undefined' &&
      right[Symbol.hasInstance]
    ) {
      return !!right[Symbol.hasInstance](left)
    } else {
      return left instanceof right
    }
  }
  // ES6 class, must be called with the new operation,
  // The purpose of this method is to check if the _instanceof method encapsulated above is invoked through the new operation
  function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
      throw new TypeError('Cannot call a class as a function')
    }
  }
  // Encapsulated Object.defineProperty
  function _defineProperty(obj, key, value) {
    if (key in obj) {
      Object.defineProperty(obj, key, {
        value: value,
        enumerable: true,
        configurable: true,
        writable: true
      })
    } else {
      obj[key] = value
    }
    return obj
  }

  var Parent = function Parent(name) {
    // Check to see if called through the new operation
    _classCallCheck(this, Parent)
    // Initialize isAdult
    _defineProperty(this, 'isAdult', true)
    // Initialize name based on attendance
    this.name = name
  }
  // Initialize static property nation
  _defineProperty(Parent, 'nation', 'China')

From the compiled code, you can see that Babel encapsulates some methods for its rigor, perhaps a little confused is the Symbol.hasInsance in the _instanceof(left, right) method, from MDN and Introduction to ECMAScript6 As you can see from this property, you can customize the behavior of the instanceof operator on a class.There is also a focus on the object_classCallCheck(instance, Constructor), which checks to see if it is called through the new operation.

Process Two

Before compilation:

class Parent {
    static nation = 'China'
    
    isAdult = true
    
    get thought() {
        console.log('Thought in head is translate to Chinese.')
        return this._thought
    }
    
    set thought(newVal) {
        this._thought = newVal
    }
    
    constructor(name) {
        this.name = name
    }
    
    static live() {
        console.log('live')
    }

    talk() {
        console.log('talk')
    }
}

After compilation:

 'use strict'
  // Encapsulated instanceof operation
  function _instanceof(left, right) {
    // .....
  }
  // ES6 class, must be called with the new operation,
  // The purpose of this method is to check if the _instanceof method encapsulated above is invoked through the new operation
  function _classCallCheck(instance, Constructor) {
    // ......
  }
  // Encapsulate Object.defineProperty to add properties
  function _defineProperties(target, props) {
    // Traversing props
    for (var i = 0; i < props.length; i++) {
      var descriptor = props[i]
      // enumerable defaults to false
      descriptor.enumerable = descriptor.enumerable || false
      descriptor.configurable = true
      if ('value' in descriptor) descriptor.writable = true
      Object.defineProperty(target, descriptor.key, descriptor)
    }
  }
  // Add prototype or static properties to Constructor and return
  function _createClass(Constructor, protoProps, staticProps) {
    // If it is a prototype property, add it to the prototype object
    if (protoProps) _defineProperties(Constructor.prototype, protoProps)
    // If it is a static property, add it to the constructor
    if (staticProps) _defineProperties(Constructor, staticProps)
    return Constructor
  }
  // Encapsulated Object.defineProperty
  function _defineProperty(obj, key, value) {
    // ......
  }

  var Parent =
    /*#__PURE__*/
    (function() {
      // Add getter/setter
      _createClass(Parent, [
        {
          key: 'thought',
          get: function get() {
            console.log('Thought in head is translate to Chinese.')
            return this._thought
          },
          set: function set(newVal) {
            this._thought = newVal
          }
        }
      ])

      function Parent(name) {
        // Check to see if called through the new operation
        _classCallCheck(this, Parent)
        // Initialize isAdult
        _defineProperty(this, 'isAdult', true)
        // Initialize name based on attendance
        this.name = name
      }
      // Add talk and live methods
      _createClass(
        Parent,
        [
          {
            key: 'talk',
            value: function talk() {
              console.log('talk')
            }
          }
        ],
        [
          {
            key: 'live',
            value: function live() {
              console.log('live')
            }
          }
        ]
      )

      return Parent
    })()
  // Initialize static property nation
  _defineProperty(Parent, 'nation', 'China')

Compared with procedure one, Babel generates an additional auxiliary function for _defineProperties(target, props) and _createClass(Constructor, protoProps, staticProps), which are used to add prototype and static attributes, and both data descriptors and access descriptors can be controlled through the Object.defineProperty method.
It is worth noting that all methods in the class in ES6 are non-traversable (enumerable: false), and here is a small detail: if you use TypeScript, when you set the target in compileOptions and es5, you will find that the compiled method can be traversed through Object.keys(), but not when you set it to es6.

summary

Babel parses through an AST abstract grammar tree and adds the following

  • _instanceof(left, right)//encapsulated instanceof operation
  • _classCallCheck(instance, Constructor)//Check if called through the new operation
  • _defineProperties(target, props)//Encapsulate Object.defineProperty to add properties
  • _createClass(Constructor, protoProps, staticProps)//Add prototype or static properties to Constructor and return
  • _defineProperty(obj, key, value) // /Encapsulated Object.defineProperty

Five auxiliary functions to add properties and methods to the Parent constructor and convert the syntax sugar named class to ES5 code.

What is extends

Since ES6 has no classes, how do you inherit it? Believe the wise you already know that, like classes, extends are grammatical sugars. Next, let's take this grammatical sugars apart step by step.

Parasitic combinatorial inheritance of ES5

from Starting with Prototype (above) - illustrating ES5 inheritance related As you know here, the relatively perfect implementation of inheritance is a parasitic combinatorial inheritance, and for ease of reading, the source code and schematic diagram are attached here again:

function createObject(o) {
    function F() {}
    F.prototype = o
    return new F()
}

function Parent(name) {
    this.name = name
}

function Child(name) {
    Parent.call(this, name)
}

Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child

var child = new Child('child')

ES6 versus ES5 Writing

If we refer to the inheritance implementation above, we can easily write out two versions of inheritance

class Child extends Parent {
    constructor(name, age) {
        super(name); // Call the constructor(name) of the parent class
        this.age = age;
    }
}
function Child (name, age) {
    Parent.call(this, name)
    this.age = age
}

Child.prototype = createObject(Parent.prototype)
Child.prototype.constructor = Child

How Babel is compiled

Some details

  • The subclass must call the super method in the constructor method, or an error will be reported when creating a new instance.This is because subclasses do not have their own this object, but inherit and process the parent's this object.If you do not call the super method, the subclass will not get the this object.

That's why, in the constructor of a subclass, the this keyword can only be used after super is called, otherwise an error will occur.

  • In ES6, static methods of the parent class can be inherited by subclasses.Class is the syntax sugar for the constructor and has both the prototype and u proto_u attributes, so there are two inheritance chains at the same time.

Compilation process

Similarly, we enter the code into the Babel website's Try it out To view the compiled code:

'use strict'
  // Encapsulated typeof
  function _typeof(obj) {
    if (
      typeof Symbol === 'function' &&
      typeof Symbol.iterator === 'symbol'
    ) {
      _typeof = function _typeof(obj) {
        return typeof obj
      }
    } else {
      _typeof = function _typeof(obj) {
        return obj &&
          typeof Symbol === 'function' &&
          obj.constructor === Symbol &&
          obj !== Symbol.prototype
          ? 'symbol'
          : typeof obj
      }
    }
    return _typeof(obj)
  }
  // Call the constructor() of the parent class and return this of the child class
  function _possibleConstructorReturn(self, call) {
    if (
      call &&
      (_typeof(call) === 'object' || typeof call === 'function')
    ) {
      return call
    }
    return _assertThisInitialized(self)
  }
  // Check if super() of subclass is called
  function _assertThisInitialized(self) {
    if (self === void 0) {
      throw new ReferenceError(
        "this hasn't been initialised - super() hasn't been called"
      )
    }
    return self
  }
  // Encapsulated getPrototypeOf
  function _getPrototypeOf(o) {
    _getPrototypeOf = Object.setPrototypeOf
      ? Object.getPrototypeOf
      : function _getPrototypeOf(o) {
          return o.__proto__ || Object.getPrototypeOf(o)
        }
    return _getPrototypeOf(o)
  }
  // Implement Inherited Auxiliary Functions
  function _inherits(subClass, superClass) {
    if (typeof superClass !== 'function' && superClass !== null) {
      throw new TypeError(
        'Super expression must either be null or a function'
      )
    }
    subClass.prototype = Object.create(superClass && superClass.prototype, {
      constructor: { value: subClass, writable: true, configurable: true }
    })
    if (superClass) _setPrototypeOf(subClass, superClass)
  }
  // Encapsulated setPrototypeOf
  function _setPrototypeOf(o, p) {
    _setPrototypeOf =
      Object.setPrototypeOf ||
      function _setPrototypeOf(o, p) {
        o.__proto__ = p
        return o
      }
    return _setPrototypeOf(o, p)
  }
  // Check to see if called through the new operation
  function _classCallCheck(instance, Constructor) {
    if (!_instanceof(instance, Constructor)) {
      throw new TypeError('Cannot call a class as a function')
    }
  }

  var Child =
    /*#__PURE__*/
    (function(_Parent) {
      // Inheritance operation
      _inherits(Child, _Parent)

      function Child(name, age) {
        var _this

        _classCallCheck(this, Child)
        // Call the constructor() of the parent class and return this of the child class
        _this = _possibleConstructorReturn(
          this,
          _getPrototypeOf(Child).call(this, name)
        )
        // Initialize the subclass's own properties based on participation
        _this.age = age
        return _this
      }

      return Child
    })(Parent)

_inherits(subClass, superClass)

Let's take a closer look at the details of this auxiliary function that implements inheritance:

function _inherits(subClass, superClass) {
    // 1. Check that the inheritance target of extends (that is, the parent class) must be a function or null
    if (typeof superClass !== 'function' && superClass !== null) {
      throw new TypeError(
        'Super expression must either be null or a function'
      )
    }
    // 2. Parasitic combinatorial inheritance similar to ES5, using Object.create,
    //    Setting the u proto_u property of the subclass prototype property points to the prototype property of the parent class
    subClass.prototype = Object.create(superClass && superClass.prototype, {
      constructor: { value: subClass, writable: true, configurable: true }
    })
    // 3. Set the u proto_u property of the subclass to point to the parent class
    if (superClass) _setPrototypeOf(subClass, superClass)
  }

This method is divided into three main steps, the second step is to achieve inheritance through parasitic combinatorial inheritance with the addition of a new non-enumerable property called constructor; the third step is to achieve the second prototype chain mentioned above, so that the static method can also be inherited.

_possibleConstructorReturn(self, call)

This auxiliary function is mainly used to achieve the effect of super(), and in the case of parasitic combinatorial inheritance, it is the part inherited by the constructor, unlike this, which returns a this and assigns it to the subclass.Details can be found in How Babel of the ES6 series compiles Class (below) See.

summary

Like class, Babel uses AST abstract grammar tree analysis and then adds a set of auxiliary functions, which I think can be divided into two categories, the first one:

  • _typeof(obj)//encapsulated typeof
  • _getPrototypeOf(o)//encapsulated getPrototypeOf
  • _setPrototypeOf(o, p)//Encapsulated setPrototypeOf

This robust functional auxiliary function
Category 2:

  • _assertThisInitialized(self)//Check whether super() of the subclass is called
  • _possibleConstructorReturn(self, call)//Call constructor() of the parent class and return this of the child class
  • _classCallCheck(instance, Constructor)//Check if called through the new operation
  • _inherits(subClass, superClass)//Implements inherited auxiliary functions

This process assistant function is designed to achieve the main functions, thereby achieving a more complete parasitic combined inheritance.

Postnote

Starting with Prototype, there are two articles about JavaScript prototypes from two perspectives.

Reference material

Posted by bolerophone on Sun, 17 Nov 2019 10:44:15 -0800