继承

简单地理解,继承就是一个对象可以访问另外一个对象中的属性和方法。在JavaScript 中,我们通过原型和原型链的方式来实现了继承特性。

image-20210518172746716

观察上图,因为 B 继承了 A,那么 B 可以直接使用 A 中的 color 属性,就像这个属性是 B 自带的一样。

\_proto\_

JavaScript中任意对象都有一个内置属性 [[Prototype]] ,但是ES5之前没有访问这个 [[Prototype]] 属性的标准方式,所以大多数浏览器会在每个对象上暴露__proto__属性,通过这个属性可以访问对象的原型

看下图:

image-20210519090101754

C.__proto__ = B

我们把 __proto__属性称之为 C 对象的原型 (prototype)__proto__ 指向了内存中的 B 对象,我们就把 __proto__ 指向的 B 对象称为 C 对象的原型对象,那么 C 对象就可以直接访问其原型对象 B 的方法或者属性。

作用

构成原型链,用于实现基于原型的继承。举个例子,当我们访问 C 这个对象中的 color 属性时,如果在 C 中找不到,那么就会沿着__proto__依次查找。

let A = {
    color: 'orange'
}
let B = {
    name: 'pipi'
}
let C = {
    type: 'dog'
}
C.__proto__ = B
B.__proto__ = A
console.log(C.color) // orange

image-20210519101816391

__proto__ 属性在 ES6 时才被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用

  1. 这是隐藏属性,并不是标准定义的;
  2. 使用该属性会造成严重的性能问题。

为了更好的支持,推荐使用 Object.getPrototypeOf()来获取 [[Prototype]] 。

那应该怎么去正确地设置对象的原型对象呢?

答案是使用构造函数来创建对象。

构造函数创建对象

看下面这段代码:

function DogFactory(type,color){
this.type = type
this.color = color
}
let dog = new DogFactory('Dog','Black')

要创建 DogFactory 的实例,应使用 new 操作符。以这种方式调用构造函数会执行如下操作。

  1. 在内存中创建一个新对象。
  2. 这个新对象内部的 [[Prototype]] 特性被赋值为构造函数的 prototype 属性。
  3. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。

我们主要看第2步,这里 dog 对象内部的 [[Prototype]] 被赋值为了DogFactory.prototype

dog.__proto__ = DogFactory.prototype

那么 prototype是什么呢?

Prototype

每个函数对象中都有一个公开的 prototype 属性,当你将这个函数作为构造函数来创建一个新的对象(实例)时,新创建对象(实例)的原型对象就指向了该函数的 prototype 属性。

当然了,如果你只是正常调用该函数,那么 prototype 属性将不起作用。

看下面这段代码:

function DogFactory(type,color){
this.type = type
this.color = color
//Mammalia
}
DogFactory.prototype.constant_temperature = 1
let dog1 = new DogFactory('Dog','Black')
let dog2 = new DogFactory('Dog','Black')
let dog3 = new DogFactory('Dog','Black')

//实例通过__proto__链接到原型对象,它实际上指向隐藏特性[[Prototype]],构造函数通过 prototype 属性链接到原型对象。
//也就是说实例与构造函数没有直接联系,与原型对象有直接联系。
console.log(dog1.__proto__ === DogFactory.prototype) // true

所以此时:

dog1.__proto__ = DogFactory. prototype
dog2.__proto__ = DogFactory. prototype
dog3.__proto__ = DogFactory. prototype

这样我们三个 dog 对象的原型对象(DogFactory)都指向了 prototype,而 prototype 又包含了constant_temperature 属性,这就是我们实现继承的正确方式。

image-20210519111757179

constructor

默认情况下,所有原型对象自动获得一个名为 constructor 的属性,指回与之关联的构造函数。

还是看上面的例子:

function DogFactory(type,color){
this.type = type
this.color = color
//Mammalia
}
DogFactory. prototype.constant_temperature = 1
let dog1 = new DogFactory('Dog','Black')

console.log(DogFactory.prototype.constructor === DogFactory) // true

image-20210519140643914

  1. 构造函数 DogFactory 有一个 prototype(__proto__) 属性引用其原型对象;
  2. 这个原型对象也有一个 constructor 属性,引用这个构造函数 DogFactory ;
  3. 构造函数 DogFactory 有一个 prototype 属性引用其原型对象;
  4. ......
  5. 换句话说,两者循环引用。

原型链

ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。

上面我们知道了原型链的实现主要是靠__proto__,当然,实际使用中还是要用 prototype 。

function DogFactory() {
    this.type = 'toypoodle'
    this.color = 'black'
}
DogFactory.prototype.getInfo = function () {
    return `Type is: ${this.type},color is ${this.color}.`
}

function Dog() {
    this.name = 'doudou'
}
// 继承 DogFactory
Dog.prototype = new DogFactory();
Dog.prototype.getDogInfo = function () {
    return this.name
}
let dog1 = new Dog()
console.log(dog1.getInfo()) // Type is: toypoodle,color is black.

下面补充几个知识点。

默认原型

默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的。任何函数的默认原型都是一个 Object 的实例,这意味着这个实例有一个内部指针指向
Object.prototype。这也是为什么自定义类型能够继承包括 toString()、valueOf()在内的所有默认方法的原因。

还是用上面的代码举例:

function DogFactory(type, color) {
    this.type = type
    this.color = color
    //Mammalia
}

console.log(DogFactory.prototype.__proto__ === Object.prototype); // true
console.log(DogFactory.prototype.__proto__.constructor === Object); // true
console.log(DogFactory.prototype.__proto__.__proto__ === null); // true

也就是说,正常的原型链都会终止于 Object 的原型对象,Object 原型的原型是 null 。

instanceof

如果一个实例的原型链中出现过相应的构造函数,则 instanceof 返回 true。

console.log(dog1 instanceof Object); // true
console.log(dog1 instanceof DogFactory); // true
console.log(dog1 instanceof Dog); // true

isPrototypeOf()

原型链中的每个原型都可以调用这个方法,只要原型链中包含这个原型,这个方法就返回 true。

console.log(Object.prototype.isPrototypeOf(dog1)); // true
console.log(DogFactory.prototype.isPrototypeOf(dog1)); // true
console.log(Dog.prototype.isPrototypeOf(dog1)); // true

关于方法

子类覆盖父类方法

function DogFactory() {
    this.type = 'toypoodle'
    this.color = 'black'
}
DogFactory.prototype.getInfo = function () {
    return `Type is: ${this.type},color is ${this.color}.`
}

function Dog() {
    this.name = 'doudou'
}
// 继承 DogFactory
Dog.prototype = new DogFactory();
Dog.prototype.getDogInfo = function () {
    return this.name
}
//子类重写新方法
Dog.prototype.getInfo = function(){
    return `Color is ${this.color}.`
}
let dog1 = new Dog()
console.log(dog1.getInfo()) // Color is black.

Dog 实例上调用 getInfo() 时调用的是这个方法。而 DogFactory 的实例仍然会调用最初的方法。

重点在于上述两个方法都是在把原型赋值为 DogFactory 的实例之后定义的。

对象字面量方式创建原型

以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。

function DogFactory() {
    this.type = 'toypoodle'
    this.color = 'black'
}
DogFactory.prototype.getInfo = function () {
    return `Type is: ${this.type},color is ${this.color}.`
}

function Dog() {
    this.name = 'doudou'
}
// 继承 DogFactory
Dog.prototype = new DogFactory();
Dog.prototype.getDogInfo = function () {
    return this.name
}
// 通过对象字面量添加新方法,这会导致上一行无效
Dog.prototype = {
    getDoggInfo() {
        return this.color
    }
}
let dog1 = new Dog()
console.log(dog1.getInfo()) // Uncaught TypeError: dog1.getInfo is not a function

原型链的问题

原型链的第一个问题是,原型中包含的引用值会在所有实例间共享。

看下面的代码:

function DogFactory() {
    this.colors = ["red", "blue", "green"]
}

function Dog() {}
// 继承 DogFactory
Dog.prototype = new DogFactory();

let dog1 = new Dog()
dog1.colors.push('black')
console.log(dog1.colors) // ["red", "blue", "green", "black"]

let dog2 = new Dog()
console.log(dog2.colors) // ["red", "blue", "green", "black"]

通过 dog1 改动 colors 属性也会反映到 dog2 上,而这往往不是我们想要的。这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。

原型链的第二个问题是,子类型在实例化时不能给父类型的构造函数传参。

参考

[javascript高级程序设计第四版]

图解 Google V8

js中__proto__和prototype的区别和关系?


人丑就要多读书_
197 声望17 粉丝