原型链与继承-JS高级

前言

  初学javascript的时候对原型链和继承就一知半解,上半年复习的时候感觉十分良好,直到最近又被问道的时候,还是讲不清楚。自己就又看了一遍JS高级程序设计,力求有更深的理解,彻底搞懂。

  继承是OO语言中的一个最为人津津乐道的概念。许多OO语言都支持两种继承方式:接口继承和 实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于js中方法没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其 实现继承主要是依靠原型链来实现的。本段摘自JS高级程序设计。

原型链

  原型和实例关系:

每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针,而实例(instance)都包含一个只想原型对象的内部指针。

  如果试图引用对象(instance)的某个属性,首先会在对象内部有没有这个属性,找不到时才会在该对象的原型(prototype)里去寻找这个属性。

  让一个实例的原型对象指向另一个类型的实例:

    thisConstructor.prototype = otherInstance

  我们如果要引用thisConstructor构造的实例thisInstance的属性name,

  1. 先在thisInstance自有属性查找

  2. 没有找到会在thisInstance.__proto__(thisConstructor.prototype)中找,我们上面将其指向了otherInstance,即我们是在otherInstance中寻找属性name

  3. 如果otherInstance中还是没有该属性,程序并不会终止,它将会继续向上去找otherInstance.__proto__(otherConstructor.prototype)的属性,一直到Object的原型对象,即顶端。

    查找过程:

    thisInstance >> otherInstance >> otherConstructor.prototype ··· >> Object.prototype

  这样一个查找的过程,就像链条一样,就称作原型链,prototype充当着链接的作用。

// 一个简单的例子
function Animal() {
   this.name = 'animal';
}

function Tiger() {
   this.age = 'tiger';
}

Tiger.prototype = new Animal();
const instance = new Tiger();

console.log(instance.name);// animal

  了解了原型链,上面的的结果就很明显了,Tiger没有name属性,向上查找到Animal具有name属性,输出。

  如何判断原型和实例的继承呢?一般使用instanceof或isPrototypeOf:

  1. 使用instanceof运算符:

    console.log(instance instanceof Object); // true

    console.log(instance instanceof Tiger); // true

    console.log(instance instanceof Animal); // true

  由于原型链的关系,instance可以说是Object、Animal、Tiger中的任何一个实例,所以使用instanceof都会返回true。

  1. 使用isPrototypeOf()方法:

    console.log(Object.prototype.isPrototypeOf(instance));

    console.log(Animal.prototype.isPrototypeOf(instance));

    console.log(Tiger.prototype.isPrototypeOf(instance));

  道理和上面的一样。

原型链的问题及解决

  原型链的设计并不是完美的,它也存在着一些问题:

  1. 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享。
  2. 创建子类型时,不能向父类的构造函数中传参。

  针对原型链的不足,也有解决方案。

经典继承

  为解决原型链中的这两个问题,开始我们使用一种借用构造函数(constructor stealing)的技术。其基本思想是在子类型的构造函数内部调用父类的构造函数。

function Father() {
    this.childrens = ['tom', 'bob', 'jack','lucy'];
}
function Son() {
    // 继承了Father,且向父类传递参数
    Father.call(this);
}
const instance = new Son();
instance.childrens.push('reeves');
console.log(instance.childrens); // ['tom', 'bob', 'jack','lucy', 'reeves']

const otherInstance = new Son();
console.log(otherInstance.childrens); 
// ['tom', 'bob', 'jack','lucy']; 证明引用类型值是独立的

  这种方式保证原型链中引用类型值独立,同时子类型创建时可以向父类型传递参数。但是如果仅用借用构造函数,将会存在方法都在构造函数中定义,函数服用也就不能使用了。而且父类(Father)中定义的方法对子类(Son)而言也是不可见的。所以很少会单独使用这种技术。

组合继承

  也叫做伪经典继承,意思是将原型链和借用构造函数的技术组合。其思路是使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。这样既通过在原型上定义方法实现函数服用,又能保证每个实例都又自己的属性。

function Father(name){
    this.name = name;
    this.childrens = ["tom","bob","jack","lucy"];
}
Father.prototype.sayName = function(){
    console.log(this.name);
}
function Son(name, age){
    //继承实例属性,第一次调用Father()
    Father.call(this, name);
    this.age = age;
}
//继承父类方法,第二次调用Father()
Son.prototype = new Father();
Son.prototype.sayAge = function(){
    console.log(this.age);
}
var instance1 = new Son("reeves", 18);
instance1.childrens.push("reeves");
console.log(instance1.childrens); // "tom,bob,jack,black,reeves"
instance1.sayName(); // reeves
instance1.sayAge(); // 18

var instance1 = new Son("alex",19);
console.log(instance1.childrens); // "tom,bob,jack,lucy"
instance1.sayName(); // alex
instance1.sayAge(); // 19

  组合继承避免了原型链和经典继承的缺陷,组合了二者的有点,是js中最常用的继承模式,它支持instanceof和isPropertyOf()识别实例是否为组合继承创建的对象。另外,组合继承实际上调用了两次父类构造函数,造成了不必要的消耗。

原型继承

  这个方法是由著名的大师Douglas Crockford与2006年提出的,他的想法是借助原型可以基于已有的对象创建新对象,同时不必因此创建自定义类型。其思路是在object()函数内部,先用一个临时的构造函数,再将传入的对象作为这个构造函数的原型,最后返回了这个临时的新实例。

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

  实际上来说,object()对传入的对象进行了一次浅拷贝。

var person = {
    friends : ["Van","Louis","Nick"]
};
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");
console.log(person.friends);//"Van,Louis,Nick,Rob,Style"

  可以作为另一个对象基础的是person对象,于是我们把它传入到object()函数中,然后该函数就会返回一个新对象。这个新对象将person作为原型,因此它的原型中就包含引用类型值属性。 这意味着person。friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。

  ES5中,新增了Object.create()方法规范了原型继承。Object.create()接受两个参数:

  • 一个用作新对象原型的对象
  • (可选)一个为新对象定义额外属性的对象
var person = {
    friends : ["Van","Louis","Nick"]
};
var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.friends.push("Style");
console.log(person.friends);//"Van,Louis,Nick,Rob,Style"

  object.create() 只有一个参数时功能与上述object方法相同,它的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的.以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:

var person = {
    name : "Van"
};
var anotherPerson = Object.create(person, {
    name : {
        value : "Louis"
    }
});
console.log(anotherPerson.name);//"Louis"

  支持Object.create()的浏览器IE9+, Firefox 4+, Safari 5+, Opera 12+ 和 Chrome。需要注意的是,原型式继承中,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

寄生继承

寄生继承思路和构造函数、工厂模式类似,创建一个仅用来封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

function createAnother(oriObject){
    // 通过调用object函数创建一个新对象
    var another = object(oriObject);
    // 通过某种方式增强对象
    another.sayHi = function(){
        alert("hi");
    };
    return another;//返回这个对象
}