原型(prototype)、构造函数(constructor)、实例(instance)、类(class)之间的关系——js中的原型链详解

1,581 阅读3分钟

先来看这么一道题

class Cat{
    constructor(){
        console.log('cat');
    }
}
Cat.prototype.constructor = () => {
    console.log('dog');
}
let Tom = new Cat(); //输出是什么?

答案是cat。这个问题很奇怪,因为平常根本不会有这种写法,但不妨碍我们来理解js中面向对象编程的实现方式。要搞清楚输出为什么是是cat就要理解原型(prototype)、构造函数(constructor)、实例(instance)、类(class)之间的关系。

构造函数

js中通过构造函数生成新对象,如

function Dog(name, color){
    this.name = name;
    this.color = color;
}
let Pluto = new Dog('Pluto', 'black');

或者是用class

class Dog {
    constructor(name, color){
        this.name = name;
        this.color = color;
    }
}
let Pluto = new Dog('Pluto', 'black');

class是一个语法糖,其本质上还是一个函数

typeof Dog //"function"

原型

在生成新对象时发生了什么呢?这里就要说到prototype了。prototype是每个函数都有的属性,它定义了由该函数创建的实例所基于的对象,而构造函数定义了在创建每个实例的时候需要执行的内容。画个图来表示。

创建的实例时,构造函数先生成一个新对象,将this指向这个新对象,新对象的原型(浏览器环境下是__proto__)指向构造函数的原型,然后执行函数。 执行构造函数时定义的属性实际上是定义在实例上的,因此不同实例之间各有一份,互不影响。

Pluto.hasOwnProperty('color') //true
Dog.hasOwnProperty('color') //false
Dog.prototype.hasOwnProperty('color') //false

如果希望所有实例共享一个属性,则应该定义在原型上

Dog.prototype.bark = () => {
    console.log('bark');
}

//class中的属性也是定义在原型上的,上面的写法等同于下面
class Dog{
    constructor(name, color){
        //...
    }
    bark(){
        console.log('bark');
    }
}

要注意的是,实例本身是没有constructor属性的,调用constructor时实际调用的是原型的constructor

Pluto.hasOwnProperty('constructor') //false
Pluto.__proto__.hasOwnProperty('constructor') //true

另外,所有对象都有构造函数,构造函数本身也是一个对象,也有构造函数。

继承

js中的继承,就是把子类(构造函数)的原型指向父类的一个实例,并把原型的构造函数指向自己。extends关键字做的也是同样的事。

let Husky = function() {};
Husky.prototype = new Dog();
Husky.prototype.constructor = Husky;

原型链

读取对象的属性时,先查找对象本身的属性,如果没有,就到它的原型以及原型的原型去找。如果直到最顶层的null还是找不到,则返回undefined。这条查找链上的所有对象构成原型链

instanceof 运算符

A instanceof B即是查找B.prototype是否在A的原型链上

let Coco = new Husky();
Coco instanceof Dog; //true
Husky instanceof Dog //false

开头的问题

要搞懂这个问题现在应该已经很简单了。声明Cat的时候,是这样的:

修改Cat.prototype.constructor的时候,其实是改变了原型的constructor属性的指向。

执行new Cat()的时候,执行的还是Cat构造函数,但是新实例的constructor属性却指向新函数。

因此new Cat()的时候,输出的是cat

还有一点要注意的是,如果用Object.create来继承的话,新对象只是简单的把原型指向父对象,不会有新的构造函数。

let Rita = Object.create(Tom);
Rita.__proto__ === Tom; //true
Rita.__proto__.hasOwnProperty('constructor'); //false
Rita.constructor === Tom.__proto__.constructor; //true