原型链和继承

689 阅读9分钟

什么是原型链

当我们调用一个对象的属性时,如果对象没有该属性,JavaScript 解释器就会顺着__proto__(原型)上去找该属性,如果原型上也没有该属性,那就去找原型的原型。这种属性查找的机制 被称为原型链(prototype chain)

实例的__proto__属性,指向构造函数的原型对象,在原型对象中,存储所有实例公用的属性和方法。

首先需要知道的

  • js分为函数对象普通对象,每个对象都有 __proto__属性,但是只有函数对象才有prototype属性
  • ObjectFunction都是js内置的函数, 类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String

原型链

1.每个函数都有一个prototype属性,指向该函数的原型对象

2.每个对象都有一个proto属性,指向对象的原型对象

3.函数同时又是对象

4.每个原型对象,都有一个constructor属性,指向构造函数本身

5.实例proto原型对象 指向同一个地方(构造函数的原型

6.Object.prototype.proto===null

7.Function.proto === Function.prototype

8.Object created by Function: Object.proto === Function.prototype

经典原型链图 分析

avatar

function Foo()
let f1 = new Foo();
let f2 = new Foo();

f1.__proto__ = Foo.prototype; // 实例的__proto__指向构造函数的原型
f2.__proto__ = Foo.prototype; // 实例的__proto__指向构造函数的原型
Foo.prototype.__proto__ = Object.prototype; // (Foo.prototype本质也是普通对象,实例的__proto__指向构造函数的原型)
Foo.prototype.constructor = Foo; // 原型对象,都有一个constructor属性,执行函数本身
Foo.__proto__ = Function.prototype; // 实例的__proto__指向构造函数的原型
Function.prototype.__proto__ = Object.prototype; //(Function.prototype本质也是普通对象,实例的__proto__指向构造函数的原型)
Object.prototype.__proto__ = null; // 原型链到此停止
// 此处注意Foo 和 Function的区别, Foo是 Function的实例

Function Object()
let o1 = new Object();
let o2 = new Object();
o1.__proto__ = Object.prototype; // 实例的__proto__指向构造函数的原型
o2.__proto__ = Object.prototype; // 实例的__proto__指向构造函数的原型
Object.prototype.constructor = Object; //
Object.__proto__ = Function.prototype //
// 此处有点绕,Object本质是函数,Function本质是对象 (比如: new Object 构造函数)
Function.prototype.constructor = Function; // 原型对象,都有一个constructor属性,执行函数本身
Function.__proto__ = Function.prototype // 实例的__proto__指向构造函数的原型(??存疑)
  • 除了Object的原型对象(Object.prototype)的__proto__指向null,其他内置函数对象的原型对象(例如:Array.prototype)和自定义构造函数的 __proto__都指向 Object.prototype , 因为原型对象本身是普通对象。
Array.prototype.__proto__ = Object.prototype;
Foo.prototype.__proto__ = Object.prototype;

继承

1.call、apply 借用构造函数

  • 原理:在子类型构造函数内部,调用超类型构造函数
  • 也叫做 伪造对象 或 经典继承
  • 在创建子类实例的时候,把父类当做普通函数执行,让函数中的this变为当前子类的实例(使用call修改的this),此时在父类函数体中写的this.xxx=xxx这些私有的属性和方法都被子类的实例所占有了
优势: 传递参数

相对于原型链而言,借用构造函数有一个很大的优势:子类型构造函数可以向超类型构造函数传递参数。

弊端:只能让子类的实例,继承父类私有的属性和方法;
function SuperType(name){ 
   this.colors = ["red", "blue", "green"]; 
   this.name = name
} 
function SubType(){ 
   //继承了 SuperType 
   SuperType.call(this, 'Jack'); 
   this.age = 18 // 需要注意,子类型构造函数要在继承超类型SuperType 之后再定义自己的属性,以免被超类型重写
} 
var instance1 = new SubType(); 
instance1.colors.push("black"); 
console.log(instance1.colors); //"red,blue,green,black" 
var instance2 = new SubType(); 
console.log(instance2.colors); //"red,blue,green"

2.原型链继承

基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法

  • 原理:让子类的原型等于父类的一个实例(父类的实例能够拥有父类私有和公有的属性方法),这样子类的实例也同时拥有父类私有和公有的.(本质是重写原型对象)

  • 父类私有的属性和公有的属性方法都被子类继承了,而且都变成子类实例公有的属性和方法

  • 但是原型继承和遗传不太一样,遗传是把父母的基因克隆一份到自己的身上(call继承就是遗传),而原型继承仅仅是是让子类和父类之间建立了原型链的链接通道,子类实例所使用的父类的公有方法,依然在父类的原型上,使用的时候只是通过原型链查找找到的

  • 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型的内部指针

  • 实例通过__proto__指向构造函数的原型对象,原型对象__proto__指向超类的原型对象

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; 
}; 
var instance = new SubType(); 
console.log(instance.getSuperValue()); //true
console.log(instance.__proto__ === SubType.prototype) // true
console.log(SubType.prototype.__proto__ === SuperType.prototype) // true

instance的__proto__指向SubType的原型,SubType的__proto__指向SuperType的原型,最终会指向Object.prototype

但是需要注意的是,instance.constructor 指向 SuperType

avatar

终极图解

avatar

  • instanceOf 操作符 检测 实例和原型的关系
instance instanceof Object  //true 
instance instanceof SuperType  //true 
instance instanceof SubType)  //true
  • isPrototypeOf()方法 检测 实例和原型的关系
Object.prototype.isPrototypeOf(instance) //true 
SuperType.prototype.isPrototypeOf(instance) //true 
SubType.prototype.isPrototypeOf(instance) //true 
  • 注意不能使用字面量创建原型方法,这样相当于重写原型,切断构造函数和原型之间的联系
弊端:
  • 1.不管父类私有的还是公有的,都是子类公有的了
  • 2.引用值共享
  • 3.在创建子类型实例的时候,无法向超类型构造函数传递参数

  function Parent() {
     this.x = 100;
  }
  Parent.prototype.getX = function () {
      console.log(++this.x);
  };
  function Child() {
      this.y = 200;
  }
  Child.prototype = new Parent(); //->写在第一步,后续再向子类的原型上增加一些属于自己的属性和方法(防止覆盖原有的属性方法)
  // console.log(Child.prototype.constructor); // Parent   constructor指向构造函数
  Child.prototype.constructor = Child;//->防止constructor改变,我们手动增加
  Child.prototype.getY = function () {
    console.log(--this.y);
  };
  var c = new Child();
  console.log(c);
// 引用值共享

function Person(){
  this.name = 'jack'
  this.subs = ['english', 'basketball', 'tennis']; 
} 
function Sub() {
}
Sub.prototype = new Person()
var p1 = new Sub()
var p2 = new Sub()

p1.subs = ['chinese']
console.log( p1.subs); // ['chinese']
console.log( p2.subs); // [ 'english', 'basketball', 'tennis' ]

p1.name = 'Ann' // 基本类型
p1.subs.push('chinese') // 引用类型
console.log(p1.name, p1.subs); // Ann [ 'english', 'basketball', 'tennis', 'chinese' ]

console.log(p2.name, p2.subs); // jack [ 'english', 'basketball', 'tennis', 'chinese' ]

3.组合继承

  • 也叫 伪经典继承
  • 思想:使用原型链实现对属性和方法的继承,借用构造函数实现对实例属性的继承。 这样既通过在原型上定义方法,实现了函数的复用,又能保证每个实例都有自己的属性
function SuperType(name){ 
    this.name = name; 
    this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function(){ 
     console.log(this.name);
 }; 
function SubType(name, age){ 
    // 继承属性
    SuperType.call(this, name); 
    this.age = age; 
} 
// 继承方法
SubType.prototype = new SuperType(); 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){ 
    console.log(this.age); 
}; 
var instance1 = new SubType("Nicholas", 29); 
instance1.colors.push("black"); 
console.log(instance1.colors); //"red,blue,green,black" 
instance1.sayName(); //"Nicholas"; 
instance1.sayAge(); //29 
var instance2 = new SubType("Greg", 27); 
console.log(instance2.colors); //"red,blue,green" 
instance2.sayName(); //"Greg"; 
instance2.sayAge(); //27
  • instanceof操作符 和 isPrototypeOf()方法,也可检测基于继承创建的对象

4.原型式继承

  • 这种方法没有严格意义上的构造函数。
  • 主要思想是:借助原型可以基于已有的对象,创建新对象,同时还不必因此创建自定义类型
function object(o){ 
    function F(){} 
    F.prototype = o; 
    return new F(); 
}
var person = {
    name: "Nicholas", 
    friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = object(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 
var yetAnotherPerson = object(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

Object.create()和 object()方法的行为相同

ES5 增加了Object.create(新对象原型的对象(可选),为新对象定义额外属性的对象(可选))方法规范了原型式继承。

var person = { 
    name: "Nicholas", 
    friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = Object.create(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 
var yetAnotherPerson = Object.create(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"

var anotherPerson1 = Object.create(person, { 
    name: { 
        value: "Greg" 
    } 
}); 
alert(anotherPerson1.name); //"Greg"

5.寄生式继承

  • 思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象
 var clone = object(original); // 通过调用函数创建一个新对象
 clone.sayHi = function(){ // 以某种方式增强这个对象
    alert("hi"); 
 }; 
 return clone; //返回这个对象
}

var person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = createAnother(person); 
anotherPerson.sayHi(); //"hi" 

在主要考虑对象不是自定义类型和构造函数情况下,寄生式继承也是一种有用的方式。任何能够返回新对象的函数,都适用于该模式。

6.寄生组合式继承

  • 思想:通过借用构造函数来继承属性,通过原型链的混成形式,来继承方法。
  • 基本思路:不必为指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已
  • 本质上是,使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型
// subType 子类型构造函数
// superType 超类型构造函数
function inheritPrototype(subType, superType){ 
    var prototype = object(superType.prototype); // 创建对象
    prototype.constructor = subType; // 指定对象
    subType.prototype = prototype; // 增强对象
}

function SuperType(name){ 
    this.name = name; 
    this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function(){ 
    console.log(this.name); 
}; 
function SubType(name, age){ 
    SuperType.call(this, name); 
    this.age = age; 
} 
inheritPrototype(SubType, SuperType); 
SubType.prototype.sayAge = function(){ 
    console.log(this.age); 
};

avatar

总结

  • 工厂模式:使用简单的函数来创建对象,为对象添加属性和方法,然后返回对象。这个模式最终被构造函数所替代

  • 构造函数:可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符。缺点:每个成员都无法得到复用,包括函数。

  • 原型模式:使用构造函数的prototype属性指定共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,使用原型定义共享的属性和方法

  • JavaScript主要通过原型链实现继承。原型链的构建是通过将一个类型的实例,赋值给另一个构造函数的原型 来实现的。如此,子类型就能够访问超类型的所有属性和方法,这点与基于类的继承很相似。

  • 原型链的问题在于 所有对象实例,共享属性和方法。解决这个问题的办法是 借用构造函数,即:在子类型构造函数的内部,调用超类型构造函数。这样就可以保证每个实例都有自己的属性,同时还能保证只使用构造函数模式来定义类型。

  • 使用最多的是 组合继承:使用原型链共享属性和方法,通过借用构造函数继承实例属性

  • 原型式继承:可以在不必预先定义构造函数的情况下实现继承。其本质是 执行对指定对象的浅复制。得到的副本还可以进一步改造。

  • 寄生式继承:与原型式继承非常类似。也是 基于某个对象或者某些信息创建一个对象,增强对象,返回对象。为了解决组合继承模式由于多次调用超类型构造函数导致的低效率问题,可以将该模式和组合模式继承一起使用。

  • 集寄生式继承和组合继承的优点于一身,是实现基于类型继承的最有效的方式