Deep understanding of objects in JS: how class works

Keywords: Javascript Attribute ECMAScript

catalog

  • preface
  • class is a special function
  • How class works
  • Prototype chain relation of class inheritance
  • reference resources

1. Preamble

The JavaScript class introduced in ECMAScript 2015 (ES6) is essentially the existing prototype based inherited syntax sugar of JavaScript. Class syntax does not introduce a new object-oriented inheritance model for JavaScript.


2.class It's a special function

class of ES6 mainly provides more convenient syntax to create old constructor functions. We can get its type through typeof:

class People {
    constructor(name) {
        this.name = name;
    }
}

console.log(typeof People) // function

What kind of function is the class declared? We can use online tools ES6 to ES5 To analyze the real implementation behind class.


3.class How it works

Next, through the comparison of multiple sets of codes, we will analyze what functions the class declared will be converted into.


Group 1: declare an empty class with class

Syntax of ES6:

class People {}

Here are two questions:

1.class The declared class is not the same as the function declaration and will not be promoted (that is, the use must follow the declaration). Why?

console.log(People) // ReferenceError

class People {}

Run the error report in the browser, as shown in the following figure:


2. You can't call the class People() directly like a function call. You must call the class through new, such as new People(). Why?

class People {}

People() // TypeError

Run the error report in the browser, as shown in the following figure:


Convert to ES5:

"use strict";

function _instanceof(left, right) { 
    if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { 
        return !!right[Symbol.hasInstance](left); 
    } else { 
        return left instanceof right; 
    } 
}

// Judgment Constructor.prototype  Whether it appears in the prototype chain of instance object
function _classCallCheck(instance, Constructor) { 
    if (!_instanceof(instance, Constructor)) {
         throw new TypeError("Cannot call a class as a function"); 
    } 
}

var People = function People() {
  // Check if it is called through new
  _classCallCheck(this, People);
};

For the two questions mentioned above, we can use the transformed ES5 code to solve them:

For problem 1, we can see that the class declared by class is converted into a function expression, and the value of the function expression is saved with the variable People, while the function expression can only be created in the code execution phase and does not exist in Variable object If the class is used before class declaration, it is the same as before the variable People is assigned a value. At this time, the use is meaningless, because its value is undefined, and direct use will report an error. Therefore, ES6 specifies that accessing a class before class declaration will throw a ReferenceError error (the class is undefined).

For question 2, we can see that in the expression of the People function, the_ The classCallCheck function ensures that the People function must be called through new. If People() is called directly, because it is executed in strict mode, this is undefined at this time, call_ The instanceof function checks the inheritance relationship and its return value must be false, so a TypeError error must be thrown.

Supplement: the body of class declaration and class expression are executed in Strict mode Next. For example, constructors, static methods, prototype methods, getter s, and setter s are all executed in strict mode.


Group 2: add public and private fields to the class

Syntax of ES6:

class People {
    #id = 1      // Private field. It starts with a single '#' character
    name = 'Tom' // Public fields
}

Convert to ES5:

...

// Mapping public fields of a class to properties of an instance object
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 People = function People() {
  _classCallCheck(this, People);

  // Initialize private fields
  _id.set(this, {
    writable: true,
    value: 1
  });

  // Mapping public fields of a class to properties of an instance object
  _defineProperty(this, "name", 'Tom');
};

// Private field after conversion (naming conflicts will be checked automatically)
var _id = new WeakMap();

Comparing the codes before and after transformation, we can see that:

For private fields, when class is used to declare private fields, the Convention is to start with the character '#', and replace '#' in the identifier with 'after conversion_ 'and use one alone WeakMap type In this way, the instance object of the class cannot directly access the private field through the property (the private field is not in the property of the instance object at all).

For public fields, the_ The defineproperty function maps the public fields of a class to the properties of an instance object. If it is set for the first time, it also uses the Object.defineProperty Function to initialize and set the enumerable, configurable and writable properties


Group 3: add constructor and instance properties to the class

Syntax of ES6:

class People {
    #id = 1      // Private field. It starts with a single '#' character
    name = 'Tom' // Public fields

    constructor(id, name, age) {
      this.#id = id
      this.name = name
      this.age =  age // Instance property age
    }
}

Convert to ES5:

...

// Set (modify) the private field of the class
function _classPrivateFieldSet(receiver, privateMap, value) { 
    var descriptor = privateMap.get(receiver); 
    if (!descriptor) { 
        throw new TypeError("attempted to set private field on non-instance"); 
    } 
    if (descriptor.set) { 
        descriptor.set.call(receiver, value); 
    } else { 
        if (!descriptor.writable) { 
            throw new TypeError("attempted to set read only private field"); 
        } 
        descriptor.value = value; 
    } 
    return value; 
}

var People = function People(id, name, age) {
  _classCallCheck(this, People);

  _id.set(this, {
    writable: true,
    value: 1
  });

  _defineProperty(this, "name", 'Tom');

  // constructor starts from here
  
  _classPrivateFieldSet(this, _id, id);

  this.name = name;
  this.age = age;
};

var _id = new WeakMap();

Comparing the codes before and after transformation, we can see that:

The execution time of the code in the constructor of the class is after the field definition (the field is mapped to the property of the instance object). The assignment (modification) of the private field is made through_ The classPrivateFieldSet function.


Group 4: add prototype and static methods to the class

Syntax of ES6:

class People {
    #id = 1
    name = 'Tom'

    constructor(id, name, age) {
      this.#id = id
      this.name = name
      this.age =  age 
  }
  
    // Prototype method
    getName() { return this.name }

    // Static method
    static sayHello() { console.log('hello') }
}

Convert to ES5:

...

// Set properties of objects
function _defineProperties(target, props) { 
    for (var i = 0; i < props.length; i++) { 
        var descriptor = props[i]; 
        descriptor.enumerable = descriptor.enumerable || false; 
        descriptor.configurable = true; 
        if ("value" in descriptor) descriptor.writable = true; 
        Object.defineProperty(target, descriptor.key, descriptor); 
    }
}

// Mapping the methods of a class to the prototype of a constructor( Constructor.prototype )On the properties of
// Map the static methods of a class to the properties of a Constructor
function _createClass(Constructor, protoProps, staticProps) { 
    if (protoProps) _defineProperties(Constructor.prototype, protoProps); 
    if (staticProps) _defineProperties(Constructor, staticProps); 
    return Constructor; 
}

var People = function () {
  function People(id, name, age) {
    // ...
  }

  // Set methods and static methods of the class
  _createClass(People, [{
    key: "getName",
    value: function getName() {
      return this.name;
    }
  }], [{
    key: "sayHello",
    value: function sayHello() {
      console.log('hello');
    }
  }]);

  return People;
}();

var _id = new WeakMap();

Comparing the converted codes of the third group and the fourth group, we can find that:

  1. Class through_ The defineProperty function maps to the properties of the instance object (this).

  2. Methods of the_ The prototype of the createClass function mapped to the constructor( Constructor.prototype )On the properties of,

  3. The static side of the class also passes_ The createClass function maps to the properties of the Constructor.


Group 5: class inheritance

Syntax of ES6:

// Superclass (superClass)
class People {}

// Subclass inherits the parent
class Man extends People {}

Convert to ES5:

...

var People = function People() {
  _classCallCheck(this, People);
};

var Man = function (_People) {
  // Man inheritance_ People
  _inherits(Man, _People);

  // Get the constructor of Man's parent class
  var _super = _createSuper(Man);

  function Man() {
    _classCallCheck(this, Man);

    // This implements the call of the constructor of the parent class. This of the child class inherits the property on this of the parent class
    return _super.apply(this, arguments);
  }

  return Man;
}(People);

At_ In inherits function, prototype chain and static attribute inheritance are realized:

// Implement inheritance relationship
function _inherits(subClass, superClass) { 
    if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } 
    // Object.create(proto, propertiesObject) method
    // Create a new object, use proto to provide the__ proto__
    // Add the propertiesObject property to the non enumeration (default) property of the newly created object (that is, the property defined by itself, not the enumeration property on its prototype chain)
    subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); 
    if (superClass) _setPrototypeOf(subClass, superClass); 
}

// Prototype object o (that is__ proto__  Property) is p
function _setPrototypeOf(o, p) { 
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; 
    return _setPrototypeOf(o, p); 
}

1. Pass Object.create The function call shows that:

(1) subClass.prototype.__proto__ === superClass.prototype , which is equivalent to the inheritance of prototype chain

(2) subClass.prototype.constructor ===Subclass, indicating that the constructor property of the subclass constructor's display prototype object points to the original constructor

2. By calling_ setPrototypeOf(subClass, superClass):

(1) subClass.__ proto__ ===Superclass, equivalent to the implementation of static attribute inheritance


In the Man constructor, by calling the constructor (_super) of its parent class, this of the child class inherits the property on this of the parent class:

// Get the constructor of the parent class
function _createSuper(Derived) { 
    var hasNativeReflectConstruct = _isNativeReflectConstruct(); 
    return function () { 
        var Super = _getPrototypeOf(Derived), result; 
        if (hasNativeReflectConstruct) { 
            var NewTarget = _getPrototypeOf(this).constructor; 
            result = Reflect.construct(Super, arguments, NewTarget); 
        } else { 
            result = Super.apply(this, arguments); 
        } 
        return _possibleConstructorReturn(this, result); 
    }; 
}

// Determine the type of call and return the appropriate Constructor
function _possibleConstructorReturn(self, call) { 
    if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } 
    return _assertThisInitialized(self); 
}

// Assert whether selft is initialized
function _assertThisInitialized(self) { 
    if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } 
    return self; 
}

// Determine whether Reflect can be used
function _isNativeReflectConstruct() { 
    if (typeof Reflect === "undefined" || !Reflect.construct) return false; 
    if (Reflect.construct.sham) return false; 
    if (typeof Proxy === "function") return true; 
    try { 
        Date.prototype.toString.call(Reflect.construct(Date, [], function () {})); 
        return true; 
    } catch (e) { 
        return false; 
    } 
}

// Get the proto type of the o object
function _getPrototypeOf(o) { 
    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; 
    return _getPrototypeOf(o); 
}

It can be seen from the above that the implementation of class inheritance mainly includes three parts:

  • Inheritance of prototype chain
  • Inheritance of static properties
  • By calling the constructor of the parent class, get the property on this constructor of the parent class

4. Prototype chain relationship of class inheritance

Instance code:

class People {
    constructor(name) {
        this.name = name
    }
}
  
class Man extends People {
    constructor(name, sex) {
        super(name)
        this.sex = sex
    }
}

var man = new Man('Tom', 'M')

According to the above analysis, we know the implementation principle of class inheritance, and combine In depth understanding of objects in JS (1): prototype, prototype chain and constructor For the prototype chain relationship of the constructor mentioned in, the complete prototype chain relationship of the example code can be obtained as follows:


5. Reference

Class - JavaScript | MDN

exploring-es6 - class

Why is the class of ES6 grammar sugar?

Deep understanding of JavaScript series (15): Functions

What does class inheritance do?

Posted by bguzel on Mon, 18 May 2020 19:24:52 -0700