JavaScript 原型与原型链详解

191 阅读5分钟

风景19.jpg

原型

基本属性说明

prototype

js中,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性,指向原型对象。prototype(对象属性)的所有属性和方法,都会被构造函数的实例继承,因此可以将公用的属性和方法,直接定义到prototype对象属性上。
tips: 只有函数才有prototype属性

function Person(){
}
console.log(Person.prototype) 
//{
//    constructor:f Person(),
//    __proto__:Object
//}
  • prototype的用法 prototype最主要的用法是将属性和方法暴露成公用的。
function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayHello = function(){
        console.log(this.name + "say hello");
    }
}
let p1 = new Person("jack",23);
let p2 = new Person("rose",23);
console.log(p1.name);  //jack
console.log(p2.name);   //rose
console.log(p1.sayHello === p2.sayHello);  //false
function Person(name,age){
    this.name = name;
    this.age = age;
    this.sayHello = function(){
        console.log(this.name + "say hello");
    }
}
Person.prototype.sayHello = function(){
    console.log(this.name + "say hello");
}
let p1 = new Person("jack",23);
let p2 = new Person("rose",23);
console.log(p1.name);  //jack
console.log(p2.name);   //rose
console.log(p1.sayHello === p2.sayHello);  //true

对比上述两种代码。第一种代码,每一个实例,都是一个与构造函数同名但独立的Object,因此两个不同的实例去访问方法的时候,并不指向同一个方法,指向的是各自Object中的方法。但是,当方法声明在构造函数的原型中时,实例对象去访问方法的时候,指向的是同一个。

constructor

如上所述,构造函数有一个prototype属性,指向原型对象。自定义构造函数的时候,原型对象默认会获得constructor属性,指向这个构造函数。二者循环引用。构造函数的实例的constructor属性,指向构造函数

function Person(){
}
let p = Person()
console.log(Person.prototype.constructor == Person)  //true
console.log(Person.constructor); //function Function(){}
//每个函数其实是通过new Function()构造的
console.log(Object.constructor); // function Function() {}
//Object也是一个函数,它是Function()构造的
console.log(p.constructor == Person) //true

__proto__

每个对象(null除外)都会有__proto__属性,指向该对象的原型对象

function Person(){
}
console.log(Person.prototype.__proto__ == Object.prototype) //true

原型对象、构造函数及实例之间的关系

如上所述,构造函数的prototype属性,指向原型对象。原型对象的constructor属性指向构造函数。形成一个小闭环。通过new()方法可以创建构造函数的实例,它是一个与构造函数同名的object,这个object是独立的,它只包含了一个__proto__指针,指向构造函数的prototype原型对象。关系图如下:

原型关系图.png tips: 实例没有prototype,强行访问则会输出undefined

function Person(){
}
let p = new Person()
console.log(p.prototype)  //undefined
console.log(p.__proto__ == Person.prototype) //true
console.log(p.__proto__.constructor == Person)  //true

原型链

原型链是ECMAScript的主要继承方式。基本思想就是通过原型继承多个引用类型的属性和方法

原型链的基本构想: 如上所述的原型、构造函数及实例之间的关系。假设原型对象是另一个类型的实例,那么意味着这个原型对象本身有一个内部指针指向另一个原型,对应的,另一个原型也有一个指针指向另一个构造函数。这样就在实例和原型之间构造了一条原型链。
实现原型链涉及的代码模式如下:

function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
};

function SubType() {
    this.subproperty = false;
}

//继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
    return this.subproperty;
};

let instance = new SubType();
console.log(instance.getSuperValue()); // true

上述代码中,定义了2个类型:SuperTypeSubType。两个类型分别定义了一个属性和一个方法。区别在于,SubTypeSuperType的实例赋给了自己的原型SubType.prototype从而实现了对SuperType的继承。这个赋值操作重写了SubType最初的原型,将其替换为SuperType的实例。意味着SuperType实例可以访问的所有属性和方法也会存在于SubType.prototype中。 案例1.jpg 在调用instance.getSuperValue()会按照instanceSubType.prototypeSuperType.prototype这三个步骤才找到这个方法。
tips:

    1. 对属性和方法的搜索会一直持续到原型链的末端
    1. SubType.prototypeconstructor属性被重写为指向SuperType,所以instance.constructor也指向SuperType

默认原型

默认情况下,所有引用类型都继承自Object。任何函数的默认原型都是一个Object的实例,意味着这个实例有一个内部指针指向Object.prototype。所以Object.prototype上的所有包括toString()、valueOf()在内的默认方法都会被这些实例所继承。 原型链.jpg

原型与继承关系

原型与实例的关系可以通过以下两种方式来确定:

    1. instanceof 操作符 如果一个实例的原型链中出现过相应的构造函数,则instanceof返回true
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
    1. isPrototypeOf() 方法
      原型链中的每个原型都可以调用这个方法
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true

关于方法

子类有增加父类没有的新方法或覆盖父类原有的方法时,必须在原型赋值后在添加到原型上。

function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
};
function SubType() {
    this.subproperty = false;
}

第二个getSuperValue()会覆盖原有的getSuperValue()方法。所以在SubType的实例上调用该方法时,调用的是第二个getSuperValue()方法。但是SuperType的实例仍会调用,第一个getSuperValue()方法

// 继承SuperType
SubType.prototype = new SuperType();

// 新方法
SubType.prototype.getSubValue = function () {
    return this.subproperty;
};

// 覆盖已有的方法
SubType.prototype.getSuperValue = function () {
    return false;
};

let instance = new SubType();
console.log(instance.getSuperValue()); // false

tips: 使用对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。覆盖后的原型是Object的实例,而不再是SuperType的实例。SubTypeSuperType的关系也就断了

// 继承SuperType
SubType.prototype = new SuperType();

// 通过对象字面量添加新方法,这会导致上一行无效
SubType.prototype = {
    getSubValue() {
        return this.subproperty;
    },
    someOtherMethod() {
        return false;
    }
};
let instance = new SubType();
console.log(instance.getSuperValue()); // 出错!

原型链的问题

    1. 实例属性变成了原型属性
      在使用原型实现继承时,原型实际上变成了另一个类型的实例。此时,原先的实例属性也变成了原型属性。
function SuperType() {
    this.colors = ["red", "blue", "green"];
}
function SubType() {}
// 继承SuperType
SubType.prototype = new SuperType();
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green,black"
    1. 子类型在实例化的时候不能给父类型的构造函数传参