JS Foundation: four inheritance methods in JavaScript & instanceof implementation

JS Foundation: four inheritance methods in JavaScript & instanceof implementation

preface

If you don't know much about the prototype chain and type judgment of JS, it is recommended that you first read the following two articles and then come back to understand the principle and purpose of this article

At the same time, the previous article JS Foundation: see the function and implementation of new operator from five ways of creating objects We have a deep discussion on the creation of objects, and also have a little understanding of several methods of object creation. Next, this article will discuss the realization of JS inheritance, and finally give an additional implementation of instanceof keyword

text

Next, we give the inheritance implementation of four versions step by step. Finally, we give the implementation of instanceof to detect the inheritance type and deepen the understanding of the prototype chain

1. Inheritance of borrowing constructor

In the first method, we call the constructor of the parent class in the constructor of the child class to initialize the object again:

  • inherit1_borrow_constructor.js
function SuperClass() {
  this.name = 'I am super class'
}

SuperClass.prototype.greeting = function () {
  console.log('super class prototype greeting')
}

function SubClass() {
  SuperClass.call(this)
}

const sub = new SubClass()

console.log('sub', sub)
console.log(`sub.constructor === SubClass: ${sub.constructor === SubClass}`)
console.log(
  `sub.__proto__ === SubClass.prototype: ${
    sub.__proto__ === SubClass.prototype
  }`
)
console.log(
  `sub.__proto__.__proto__ === Object.prototype: ${
    sub.__proto__.__proto__ === Object.prototype
  }`
)

From the output, we can see that although the created object is of SubClass type, the object is initialized correctly; However, we found that the object does not contain the type information of the parent class at all (sub. _ proto. Proto directly points to Object.prototype), that is to say, the actual prototype (type) relationship is as follows

The subclass object only borrows the constructor of the parent class, and there is no information about the parent class on the prototype chain of the instance itself. At this time, the prototype method of the parent class (usually defined through SuperClass.prototype.Xxx = function() {/ *... * /}) can not be called at all, Obviously, this is not the inheritance we want (of course, if you want to implement simple object initialization, and the instance method is introduced in the parent class using this.Xxx = function() {})

2. Inheritance of borrowed prototype object

Since we hope to attach the parent class information (prototype chain) after construction, we can give him the prototype of the parent class. The code is as follows:

  • inherit2_borrow_prototype.js
function SuperClass() {
  this.name = 'I am super class'
}

SuperClass.prototype.greeting = function () {
  console.log('super class prototype greeting')
}

function SubClass() {}

SubClass.prototype = SuperClass.prototype

const sub = new SubClass()

console.log('sub', sub)
console.log(`sub.constructor === SuperClass: ${sub.constructor === SuperClass}`)
console.log(
  `sub.__proto__ === SubClass.prototype: ${
    sub.__proto__ === SubClass.prototype
  }`
)
console.log(
  `sub.__proto__ === SuperClass.prototype: ${
    sub.__proto__ === SuperClass.prototype
  }`
)

From the output, we can see that the prototype (_proto_ attribute) that created the instance directly points to SuperClass Prototype (indeed, we also wrote SubClass.prototype = SuperClass.prototype). It seems that it also inherits the SuperClass prototype method greeting, but the prototype object that directly borrows the parent class actually produces the following structure

If we want to add a prototype for SubClass later, the method will directly modify the SuperClass Prototype object, which in turn affects all other objects built based on the parent SuperClass type. Therefore, from this point, we find that we not only need the prototype object to be attached with the prototype information of the parent class (connected to the prototype chain of the parent class), but also hope that the SubClass can have its own prototype object independent of the prototype of the parent class. Let's take a look at the third implementation.

3. Create an instance object as the inheritance of the prototype

Since we want to keep the prototype information of the parent class and create a prototype object specific to the child class, we can choose to use the parent class to directly construct an object instance as the prototype object of the child class:

  • inherit3_instance_prototype.js
function SuperClass() {
  this.name = 'I am super class'
}

SuperClass.prototype.greeting = function () {
  console.log('super class prototype greeting')
}

function SubClass() {}

SubClass.prototype = new SuperClass()

const sub = new SubClass()

console.log('sub', sub)
console.log(`sub.constructor === SuperClass: ${sub.constructor === SuperClass}`)
console.log(
  `sub.__proto__ === SubClass.prototype: ${
    sub.__proto__ === SubClass.prototype
  }`
)
console.log(
  `sub.__proto__.__proto__ === SuperClass.prototype: ${
    sub.__proto__.__proto__ === SuperClass.prototype
  }`
)

The object relationship structure of this implementation is as follows

It seems that everything is as we think. The prototype of the parent class (inheriting the prototype method of the parent class) is retained on the prototype chain, and the prototype object exclusive to the child class retains the prototype method. However, this scheme still has two small loopholes:

  1. The constructor of the subclass prototype still points to the constructor of the parent class (SubClass.prototype.constructor === SuperClass): This is wrong. Since it is the prototype of the subclass, the constructor must point to itself
  2. The parent class instance as the prototype has been initialized: many times, our constructor will initialize some instance variables or instance methods. However, in general inheritance scenarios, unless we explicitly declare to call the constructor of the parent class for initialization (such as the super keyword in java), Otherwise, we usually want to use a pure object without instance variable initialization as our prototype object

To achieve this effect is also very simple, is to use Last Mentioned object Create method to create the prototype object

4. Create an uninitialized instance as the inheritance of the prototype

For the final inheritance implementation, we choose to use object Create method to create our subclass prototype object

  • inherit4_final.js
function SuperClass() {
  this.name = 'I am super class'
}

SuperClass.prototype.greeting = function () {
  console.log('super class prototype greeting')
}

function SubClass() {}

SubClass.prototype = Object.create(SuperClass.prototype)
SubClass.prototype.constructor = SubClass

const sub = new SubClass()

console.log('sub', sub)
console.log(`sub.constructor === SubClass: ${sub.constructor === SubClass}`)
console.log(
  `sub.__proto__ === SubClass.prototype: ${
    sub.__proto__ === SubClass.prototype
  }`
)
console.log(
  `sub.__proto__.__proto__ === SuperClass.prototype: ${
    sub.__proto__.__proto__ === SuperClass.prototype
  }`
)

The object structure is as follows

The most important changes are the following:

  • Through object Create (SuperClass.prototype) creates a prototype object in the form of superclass Prototype creates an object as a prototype and does not call the constructor on the object to initialize
  • Changing the prototype object will also change the direction of the constructor constructor, using subclass prototype. Constructor = subclass, just point the constructor back to the current type

Babel compiled inheritance

Finally, let's take a look at how the type defined by the Class compiled by Babel in ES6 is transformed into the implementation of ES5:

  • /src/index.js
class A {
  constructor() {
    this.name = 'A'
  }
}

class B extends A {
  constructor() {
    super()
    this.name = 'B'
  }
}
  • /lib/index.js
'use strict'

function _inheritsLoose(subClass, superClass) {
  subClass.prototype = Object.create(superClass.prototype)
  subClass.prototype.constructor = subClass
  _setPrototypeOf(subClass, superClass)
}

function _setPrototypeOf(o, p) {
  _setPrototypeOf =
    Object.setPrototypeOf ||
    function _setPrototypeOf(o, p) {
      o.__proto__ = p
      return o
    }
  return _setPrototypeOf(o, p)
}

var A = function A() {
  this.name = 'A'
}

var B = /*#__PURE__*/ (function (_A) {
  _inheritsLoose(B, _A)

  function B() {
    var _this

    _this = _A.call(this) || this
    _this.name = 'B'
    return _this
  }

  return B
})(A)

We defined a A A A as the parent class, a B B As a subclass, the main core of type B is_ The implementation of the inheritsLoose method (using the loose mode looks more like it is written by people and can understand hh) is actually the same as our implementation Final version Almost the same, and_ The setPrototypeOf method is additional to the constructor__ proto__ Modify the prototype (in fact, when a general function / constructor is used as an ordinary object, its _proto _prototypeactually points to Function.prototype, so there is little difference here)

The second thing to note is that in ES6, subclasses must use the super() keyword to call the constructor of the parent class for object initialization. In fact, this corresponds to the object initialization after compilation_ this = _A.call(this) || this; If super is not called_ This will become undefined, causing exceptions in the creation of objects

instanceof keyword implementation

Finally, we know that in JS, type inheritance is realized through prototype chain, so the method of type checking can not bypass the implementation of instanceof keyword:

The keyword of instanceof is whether the prototype of the object exists in the prototype chain

In short, it is to constantly look up the target object__ proto__ Whether the prototype object pointed to is the same as the target type (prototype object ctor. Prototype pointed to by constructor). The code is as follows:

  • instanceof.js
function _instanceof(obj, target) {
  const targetProto = target.prototype
  let proto = obj.__proto__
  while (proto) {
    if (proto === targetProto) return true
    proto = proto.__proto__
  }
  return false
}

class A {}

class B extends A {}

class C extends B {}

class D extends A {}

group('test a', () => {
  const a = new A()
  console.log(`a instanceof A: ${a instanceof A},\t_instanceof(a, A): ${_instanceof(a, A)}`)
  console.log(`a instanceof B: ${a instanceof B},\t_instanceof(a, B): ${_instanceof(a, B)}`)
  console.log(`a instanceof C: ${a instanceof C},\t_instanceof(a, C): ${_instanceof(a, C)}`)
  console.log(`a instanceof D: ${a instanceof D},\t_instanceof(a, D): ${_instanceof(a, D)}`)
})

group('test b', () => {
  const b = new B()
  console.log(`b instanceof A: ${b instanceof A},\t_instanceof(b, A): ${_instanceof(b, A)}`)
  console.log(`b instanceof B: ${b instanceof B},\t_instanceof(b, B): ${_instanceof(b, B)}`)
  console.log(`b instanceof C: ${b instanceof C},\t_instanceof(b, C): ${_instanceof(b, C)}`)
  console.log(`b instanceof D: ${b instanceof D},\t_instanceof(b, D): ${_instanceof(b, D)}`)
})

group('test c', () => {
  const c = new C()
  console.log(`c instanceof A: ${c instanceof A},\t_instanceof(c, A): ${_instanceof(c, A)}`)
  console.log(`c instanceof B: ${c instanceof B},\t_instanceof(c, B): ${_instanceof(c, B)}`)
  console.log(`c instanceof C: ${c instanceof C},\t_instanceof(c, C): ${_instanceof(c, C)}`)
  console.log(`c instanceof D: ${c instanceof D},\t_instanceof(c, D): ${_instanceof(c, D)}`)
})

group('test d', () => {
  const d = new D()
  console.log(`d instanceof A: ${d instanceof A},\t_instanceof(d, A): ${_instanceof(d, A)}`)
  console.log(`d instanceof B: ${d instanceof B},\t_instanceof(d, B): ${_instanceof(d, B)}`)
  console.log(`d instanceof C: ${d instanceof C},\t_instanceof(d, C): ${_instanceof(d, C)}`)
  console.log(`d instanceof D: ${d instanceof D},\t_instanceof(d, D): ${_instanceof(d, D)}`)
})

We can see what we have achieved_ The instanceof method behaves the same as the native instanceof keyword. Get it!

epilogue

I've written a lot of front-end projects of JS. After I have a deep understanding of JS, I'll come back and reinterpret and implement the inheritance mechanism of JS language from the perspective of prototype chain. It's really much easier than when I was ignorant. I hope this article can help readers have a deeper understanding of JS prototype chain, prototype object relationship and type / inheritance mechanism.

Other resources

Reference connection

Six ways of js inheritancehttps://www.cnblogs.com/ranyonsue/p/11201730.html
5 ways of JS inheritance (interview)https://zhuanlan.zhihu.com/p/81266626
Object.setPrototypeOf()-MDNhttps://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/setPrototypeOf
About the loose mode of Babel 6https://blog.csdn.net/weixin_34194359/article/details/88039623

Full code reference

https://github.com/superfreeeee/Blog-code/tree/main/front_end/javascript/js_inherit_instanceof_implement

Keywords: Javascript Class prototype

Added by AliasXNeo on Thu, 10 Feb 2022 16:48:11 +0200