The essence of JavaScript object-oriented is a prototype-based object system, not class-based. It’s determined by the original design, it’s a genetic trait. With the evolution of the ES Next standard and the addition of new features, JavaScript object-oriented has become closer to other traditional object-oriented languages. It’s a privilege to watch a language evolve and change, to grow up with a language.

The prototype

The prototype

Every function in JavaScript has an unenumerable property called prototype that points to an object called prototype object. The prototype object has an unenumerable property called constructor, which points to the function itself, so the function itself is a constructor.

When you prefix a function with the new keyword, it becomes a constructor. Each call to the constructor creates a new object, an instance. Constructors are templates used to generate instances, and all objects are instances generated by constructors.

The instance’s internal attribute [[Prototype]] is assigned as the constructor’s Prototype object. There’s no standard way to access the [[Prototype]] feature in JS, but mainstream browsers like Firefox, Safari, and Chrome expose a __proto__ property on each object. So this inner property is also called implicit prototype, and the corresponding Prototype property is called display prototype.

The relationship between constructors, prototype objects, and instances:

function Foo() {}
var obj = new Foo()

a.__proto__ === Foo.prototype
Foo.prototype.constructor === Foo
Copy the code

Prototype chain

The instance object inherits the prototype object of its constructor, which is also an object, which inherits the prototype object of its constructor, and so on, a link is formed between the instance object and the prototype object, the prototype chain.

The essence of JavaScript defines a chain of archetypes as a traceable list of all the archetypes of an object’s parent and ancestor classes.

All ordinary objects end up pointing to the built-in Object.prototype, which points to a null pointer at the top of the prototype chain and stops if the specified property is not found in the chain.

function Foo() {}
var obj = new Foo()

a.__proto__ === Foo.prototype
a.__proto__.__proto__ === Object.prototype
a.__proto__.__proto__.__proto__ === null
Copy the code

Operating the prototype

Built-in properties/methods

All instances have certain properties of objects because they inherit from Object.prototype, and all prototype-related properties/methods under Object.prototype are mandatory for every Object.

For a concrete constructor, which is itself an Object, it has some special members of function types, such as Prototype, in addition to the members of Object.prototype.

When an Object is a base (ancestor) class, it also holds class methods that can be used to manipulate objects.

The following table summarizes the three properties/methods associated with the stereotype:

Object. Prototype property/method Constructor (function) properties/methods The Object class method object.xxx
constructor prototype create()
hasOwnProperty() getPrototypeOf()
isPrototypeOf() setPrototypeOf()

With these built-in properties/methods, we can take the prototype a step further.

Look for the prototype

The relationship between a prototype and an instance can be determined in several ways, such as the following code:

function Foo () {}
function Bar () {}

Bar.prototype = new Foo()

var a = new Foo()
var b = new Bar()
Copy the code

instanceof

In addition to determining the type, the instanceof operator can also be used to check whether the prototype property of the constructor is present on the prototype chain of an instance object. However, this approach can only deal with the relationship between one instance and function, and can not determine whether multiple instances are related by prototype chain.

a instanceof Foo // true
a instanceof Bar // false

b instanceof Foo // true
b instanceof Bar // true

a instanceof b // error
Copy the code

In the code above, B is an instance of Foo and Bar, because obj2’s prototype chain contains the prototypes for these constructors.

Instanceof is implemented by walking through the prototype chain, looking for all display prototypes on the prototype chain equal to __proto__ until it reaches the top of the prototype chain. Let’s simulate it:

function myInstanceof(instanceObj, constructorFun) {
  const prototypeObj = constructorFun.prototype// Get the constructor's prototype object (display the prototype)instanceObj = instanceObj.__proto__// Get the prototype of the instance object (implicit prototype)while (instanceObj) {
    if (prototypeObj === instanceObj) {
      return true
    }
    instanceObj = instanceObj.__proto__ // Key: iterate through the prototype chain
  }
  return false
}

/ / test
function Person(name) {
  this.name = name
}
const p = new Person('sunshine')
myInstanceof(p, Person) // true
Copy the code

isPrototypeOf(…)

IsPrototypeOf () returns true if the implicit prototype of the passed object points to the object it calls. This method can be called for each prototype property in the prototype chain. IsPrototypeOf () can determine if the function’s prototype object is on the instance’s prototype chain.

a.__proto__ === Foo.prototype // true
Foo.prototype.isPrototypeOf(a) // true
Foo.prototype.isPrototypeOf(b) // false

a.isPrototypeOf(b) // false
Copy the code

Here we examine the father and son instances through the prototype object calls to Parent and Child

Access to the prototype

The Object type has many built-in methods, including two that can be used to access stereotypes.

Object.getPrototypeOf()

Object.getprototypeof () returns the implicit prototype of the passed Object, that is, the prototype Object of the constructor it calls.

function Foo () {}
Foo.prototype.name = 'sunshine'
var a = new Foo()

Object.getPrototypeOf(a) === a.__proto__ // true
a.__proto__ === Foo.prototype // true

Object.getPrototypeOf(a) === Foo.prototype // true
Object.getPrototypeOf(a).name // 'sunshine'
Copy the code

Here object.getProtoTypeof () takes the implicit prototype of instance A, whose constructor is Foo, so foo.prototype. name is equivalent to a.name

Object.setPrototypeOf()

Object.setprototypeof () writes a new Object to the instance’s prototype Object, thus overwriting the prototype inheritance relationship.

let obj1 = { a: 1 }
let obj2 = { b: 2 }

Object.setPrototypeOf(obj1, obj2)

obj1.a / / 1
obj1.b / / 2
Object.getPrototypeOf(a) === b // true
Copy the code

__proto__

__proto__ is settable, above we use ES6 object.setProtoTypeof (…). The Settings are set. Also, __proto__ looks like a property, but it’s actually more like a getter/setter, and the __proto__ implementation works roughly like this:

Object.defineProperty(Object.prototype, '__proto__', {
    get: function () {
        return Object.getPrototypeOf(this)},set: function (o) {
        Object.setPrototypeOf(this, o)
        return o
    }
})
Copy the code

So when you access A.__proto__, you’re actually calling the getter for A.__proto__ (). The getter function exists on Object.prototype, but its this refers to a, so Object.getProtoTypeof (a) === A. __proto__ returns true.

Rewrite the prototype

As with __proto__, the prototype attribute of a function can be overridden

function Foo () {
  // ...
}

Foo.prototype = {} // Create a new prototype object

var a = new Foo()

a instanceof Foo // true
a instanceof Object // true
a.constructor === Foo // false
a.constructor === Object // true
Copy the code

Here, instanceof still returns true for both Object and Foo. But the constructor property is now equal to Object instead of Foo. If constructor’s value is important, you can set its value specifically when rewriting the prototype object as follows:

function Foo () {
  // ...
}

Foo.prototype = {
  constructor: Foo
} // Create a new prototype object
Copy the code

Note, however, that restoring Constructor in this way creates an enumerable property, whereas native Constructor is not enumerable by default, so object.defineProperty () should be used instead. In addition, constructor is a writable property, and you can add, modify, and assign a property named Constructor to any object in any prototype chain.

function Foo () {
  // ...
}

Foo.prototype = {} // Create a new prototype object
Object.defineProperty(Foo.prototype, 'constructor', {
    enumerable: false.writable: true.configurable: true.value: Foo
})

var a = new Foo()
a.constructor === Foo // true
Copy the code

The process of fixing constructor requires a lot of manual work. A. constructor is not trusted and does not necessarily point to references to default functions. Therefore, the object’s constructor property is a very unsafe reference and should be avoided as much as possible.

Create an object

To avoid performance degradation with prototype modifications, you can use object.create (…) To create a new object. When you create an Object using object.create (), you can explicitly specify the prototype of the new Object. The method takes two parameters: the first parameter is the prototype of the new object, and the second parameter describes the properties of the new object.

As follows:

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

Foo.prototype.myName = function () {
  return this.name
}

function Bar (name, label) {
  Foo.call(this, name)
  this.label = label
}

// Create a new bar. prototype object and associate it with foo. prototype
Bar.prototype = Object.create(Foo.prototype)
/ / Bar at this time. Prototype. There was now no constructor, if necessary, manually create

Bar.prototype.myLabel = function () {
  return this.label
}

var a = new Bar('a'.'obj a')
a.myName() // 'a'
a.myLabel() // 'obj a'
Copy the code

The core of this code is the statement bar.prototype = object.create (foo.prototype). When you declare function Bar, like any other function, bar.prototype points to the default Object, But this prototype object is not what we want, so we create a new object and have bar. prototype point to it, throwing away the original prototype object.

Pure object

Object.create() can also create an Object whose prototype is NULL: var obj = object.create (null). The Object obj is an Object with no prototype chain, which means that methods like toString() and valueOf that exist on an Object prototype also do not exist on that Object, and we usually create such objects as pure objects.

polyfill

Object.create(…) Is a new function in ES5, so in pre-ES5 environments (IE6, for example), to support this functionality you need to use polyfill to implement it:

if(!Object.create) {
    Object.create = function (o) {
        function F(){}
        F.prototype = o;
        return new F()
    }
}
Copy the code

This polyfill uses a one-time function F, which we overwrite with its prototype property to point to the object we want to associate with, and then uses new F() to construct a new object and return it.

Property to check

hasOwnProperty()

The hasOwnProperty() method can be used to determine whether an attribute is an instance attribute. This method inherits from Object and returns true if the property exists on the instance of the Object calling it.

function Foo () {}
Foo.prototype.name = 'sunshine'
let a = new Foo()

console.log(a.name) // 'sunshine' comes from the prototype
console.log(a.hasOwnProperty('name')) // false 

a.name = 'colorful'
console.log(a.name) // 'colorful' comes from an example
console.log(a.hasOwnProperty('name')) // true

delete a.name
console.log(a.name) // 'sunshine' comes from the prototype
console.log(a.hasOwnProperty('name')) // false
Copy the code

In this example, you can see that a.name (‘name’) returns true only when a.name is overridden, indicating that the name property is an instance property, not a stereotype property.

In fact, masking occurs if the attribute name appears at the top of the stereotype chain of the instance. Properties created on such instances with the same name as the prototype object are called masked properties. Attributes contained in an instance mask all attributes of the same name in the upper layer, because the value of the instance attribute always selects the attribute at the bottom of the stereotype chain.

In the process of shielding, there are three situations:

  1. If a non-read-only property of the same name exists at the top of the stereotype chain, the property is added to the object
  2. If a read-only attribute of the same name exists at the top of the stereotype chain, it cannot be modified in strict mode, but will be added to the object in non-strict mode
  3. If a property of the same name exists at the top of the stereotype chain, but it is reactive, call its setter method, the property is not added to the object, nor is the setter for the property redefined

Once a masking property is created, the property of the same name on the prototype chain cannot be accessed, and even if you set the property to NULL, access will not be restored. Unless the delete operator is used.

The process for adding attributes in the above example is as follows:

The in operator

The IN operator returns true when a specified property is accessed through an object, whether it is on an instance or stereotype. Consider the following example:

function Foo () {}
Foo.prototype.name = 'sunshine'
let a = new Foo()

console.log(a.name) // 'sunshine' comes from the prototype
console.log(a.hasOwnProperty('name')) // false 
console.log('name' in a) // true 

a.name = 'colorful'
console.log(a.name) // 'colorful' comes from an example
console.log(a.hasOwnProperty('name')) // true
console.log('name' in a) // true 

delete a.name
console.log(a.name) // 'sunshine' comes from the prototype
console.log(a.hasOwnProperty('name')) // false
console.log('name' in a) // true 
Copy the code

In the example above, the name property is always accessible through the instance or stereotype. Therefore, a call to name’ in a always returns true, regardless of whether the property is on the instance.

If we want to determine whether an attribute is on a stereotype, we can use both the hasOwnProperty() and in operators. To do this, we can encapsulate a method:

function hasPrototypeProperty (obj, name) {
  return! obj.hasOwnProperty(name) && (namein obj)
}

function Foo () {}
Foo.prototype.name = 'sunshine'
let a = new Foo()

console.log(hasPrototypeProperty(a, 'name')) // True comes from the prototype
console.log(a.hasOwnProperty('name')) // false 

a.name = 'colorful'
console.log(a.hasOwnProperty('name')) // true from the instance
console.log(hasPrototypeProperty(a, 'name')) // false
Copy the code

Simulation of the new

MDN definition of the new operator: The new operator creates an instance of a user-defined object type or of a built-in object with a constructor.

The new keyword has an interesting history:

Brendan Eich, the creator of JavaScript, implemented New in order to achieve greater popularity. It was a relic of the forced learning of Java, and he wanted JavaScript to be Java’s “little brother.” Many people think that this design hides the true nature of prototypal inheritance in JS, but looks more like class-based inheritance on the surface. This misunderstanding prevents Java developers from understanding JavaScript well.

In fact, front-end engineers should know exactly what the new keyword does. Start with examples:

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

Foo.prototype.myName = function () {
  return this.name
}

var a = new Foo('sunshine')
a.name // "sunshine"
a.myName() // "sunshine"
a.__proto__ === Foo.prototype // true

function Bar () {
  return { name: 'colorful'}}var b = new Bar()
console.log(b) // { name: 'colorful' }
Copy the code

This. Name is equivalent to a.name, where a inherits the methods on Foo’s prototype object. So a.myname is sunshine. The Bar function returns an object, so instance B after new is equal to that object.

So, the logic inside the new function is as follows:

  1. Creates an empty object that will be returned as an instance of the object after the constructor is executed
  2. The implicit stereotype of this empty object (__proto__Display prototype pointing to constructor
  3. Assign the empty object to this inside the constructor and execute the constructor logic
  4. According to the constructor execution logic, if the constructor returns an object, that object will replace the result of new
  5. If the function returns no other object, the new function automatically returns the new object

Since new is the keyword, we can’t overwrite it directly. Here’s a function to simulate it:

function newFunction() {
  var constructor = Array.prototype.shift.call(arguments) / *if(!Object.create) {
    Object.create = function (o) {
        function F(){}
        F.prototype = o;
        return new F()
    }
	} */
  
  var obj = Object.create(constructor.prototype)

  obj.__proto__ = constructor.prototype

  var res = constructor.apply(obj, arguments)

  return res instanceof Object ? res : obj
}
Copy the code

The code above is not complicated, but the key points are as follows:

  • Use object.create to point the implicit prototype of obj to the prototype of the constructor
  • Use the apply method to point this in the constructor to obj
  • When newFunction returns, use instanceof to determine if the returned result is of an object type

Modify the prototype

At this point, we can compare the previous ways to modify the prototype, as shown in the following code:

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

Foo.prototype.myName = function () {
  return this.name
}

function Bar (name, label) {
  Foo.call(this, name)
  this.label = label
}

// Bar.prototype = Foo.prototype
// Bar.prototype = new Foo()
// Object.setPrototypeOf(Bar.prototype, Foo.prototype)
Bar.prototype = Object.create(Foo.prototype)

Bar.prototype.myLabel = function () {
  return this.label
}

var a = new Bar('a'.'obj a')
a.myName() // 'a'
a.myLabel() // 'obj a'
Copy the code
  1. Bar.prototype = Foo.prototype: This does not specify the Bar prototype, we know that the modified prototype is the implicit prototype of the modified object (__proto__) point to the display prototype, so this will only modify the display prototype, and it is meaningless, because you can use it directlyFoo.prototype
  2. Bar.prototype = new Foo()The: new keyword does create a new object and modify the implicit stereotype, but if function Foo has some side effects, it can affect the descendants of Bar() with disastrous consequences.
  3. Object.setPrototypeOf(Bar.prototype, Foo.prototype): object. setPrototypeOf is a new helper function in ES6, which can be used to modify associations in a standard way, but it is not readable, and improper use still affects inheritance relationships as new.
  4. Bar.prototype = Object.create(Foo.prototype)The disadvantage of this approach is that you need to create a new object and then discard the old object. You can’t modify the existing default object directly, which leads to a slight performance loss — garbage collection is required after the old object is discarded.

Here’s a warning from the Mozilla documentation:

In the JavaScript engines of all browsers, the effects of changing inheritance relationships are subtle and profound. This effect is not as simple as executing object.setPrototypeof () statements, but involves all access to the code of the Object whose prototype has been modified

conclusion

At this point, the operation of the prototype and the various modes of operation of the prototype is finished, and now the knowledge points are sorted out as follows.

Prototype and prototype chain

  1. Only functions display stereotype attributes (prototype
  2. All objects have implicit stereotypes (__proto__)
  3. The default prototype for all functions is an instance of Object
  4. Object all parent and ancestor classes of the prototype formed by the traceability of the linked list is called the prototype chain

The prototype operation

  1. Three ways to find prototypes:Example the instanceof functionFunction. Prototype. isPrototypeOf(instance),Object.getprototypeof (instance) === function
  2. Three ways to change a stereotype: constructors,Object.setPrototypeOf(...),Object.create(...)
  3. Objects with null archetypes have no archetype chain, objects without archetype chain are called pure objects,Object.create(null)You can create pure objects
  4. Check if the property is on the instance:Instance. HasOwnProperty
  5. Check if the property is on the prototype of the instance:Instance. HasOwnProperty && (property in instance)
  6. Several ways to create objects: Object literals {}, new Object(), new Fn(), object.create ()

The interview questions

Why is NULL the end of the prototype chain?

  • For a normal function function fn() {}, it is generated by the function function

    fn.__proto__ === Function.prototype  // true
    fn instanceof Function  // true
    fn instanceof Object    // true 
    Copy the code
  • Function is also generated by Function

    Function.__proto__ === Function.prototype  // true
    Copy the code
  • The Object Function is also a Function Object, which is also generated by Function

    Object.__proto__ === Function.prototype  //true
    Copy the code
  • Function.prototype is generated by Object.prototype

    Function.prototype.__proto__ === Object.prototype
    Copy the code
  • Object. Prototype is the end of the prototype chain

    Object.prototype.__proto__ === null
    Copy the code

So, the Function contains __proto__ and prototype properties, __proto__ pointing Function. The prototype, the prototype pointing to the Object. The prototype. The [[Prototype]] property of all types, i.e. the __proto__ property, refers to function. Prototype, and the [[Prototype]] property of function. Prototype, The __proto__ attribute points to Object.prototype, and the __proto__ attribute of Object.prototype points to null, the end of the prototype chain.