JavaScript 各类继承方式优缺点对比


原型对象

不管何时,只要建立一个新函数,就会根据一组特定的规则为该函数建立一个 prototype 属性,这个属性指向函数的原型对象。默认状况下,全部原型对象都会自动得到一个 constructor(构造函数)属性,这个属性指向 prototype 属性所在的函数。数组

function Person(){
}   
复制代码

当咱们用构造函数建立一个实例时,也会为这个实例建立一个 __proto__ 属性,这个__proto__ 属性是一个指针指向构造函数的原型对象函数

let person = new Person();
person.__proto__ === Person.prototype    // true
let person1 = new Person();
person1.__proto__ === Person.prototype    // true
复制代码

因为同一个构造函数建立的全部实例对象的__proto__ 属性都指向这个构造函数的原型对象,所以全部的实例对象都会共享构造函数的原型对象上全部的属性和方法,一旦原型对象上的属性或方法发生改变,全部的实例对象都会受到影响。优化

function Person(){
}
Person.prototype.name = "Luke";
Person.prototype.age = 18;
let person1 = new Person();
let person2 = new Person();
alert(person1.name)    // "Luke"
alert(person2.name)    // "Luke"
Person.prototype.name = "Jack";
alert(person1.name)    // "Jack"
alert(person2.name)    // "Jack"
复制代码

重写原型对象

咱们常常用一个包含全部属性和方法的对象字面量来重写整个原型对象,以下面的例子所示ui

function Person(){
}
Person.prototype = {
    name : "Luke",
    age : 18,
    job : "Software Engineer",
    sayName : function(){
        alert(this.name)
    }
}
复制代码

在上面的代码中,咱们将 Person.prototype 设置为一个新对象,而这个对象中没有constructor属性,这致使 constructor 属性再也不指向 Person,而是指向 Objectthis

let friend = new Person();
alert(friend.constructor  === Person);    //false 
alert(friend.constructor  === Object);    //true
复制代码

若是 constructor 的值很重要,咱们能够像下面这样特地将它设置回设置回适当的值spa

function Person(){
}
Person.prototype = {
    constructor : Person,
    name : "Luke",
    age : 18,
    job : "Software Engineer",
    sayName : function(){
        alert(this.name)
    }
}
复制代码

原型链及原型链继承

每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针(__proto__)。那么,假如咱们让原型对象等于另外一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另外一个原型的指针,相应地,另外一个原型中也包含着一个指向另外一个构造函数的指针。假如另外一个原型又是另外一个构造函数的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。prototype

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

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

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

Sub.prototype = new Super();    //继承了 Super 

Sub.prototype.getSubValue = function (){
    return this.subproperty;
}

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

console.log(instance.__proto__ === Sub.prototype);    //true
console.log(Sub.prototype.__proto__ === Super.prototype);    //true

复制代码

上面的代码中Sub.prototype = new Super();经过建立Super的实例,并将该实例赋值给Sub.prototype来实现继承。此时存在于Super的实例和原型对象中的全部属性和方法,也都存在于Sub.prototype中。instanse的__proto__属性指向Sub的原型对象Sub.prototype,Sub原型对象的__proto__属性又指向Super的原型对象Super.prototype指针

原型链搜索机制

当访问一个实例的属性时,首先会在该实例中搜索该属性。若是没有找到该属性,则会继续搜索实例的原型。在经过原型链继承的状况下,搜索过程就得以沿着原型链继续向上查找,直到找到该属性为止,或者搜索到最高级的原型链Object.prototype中,任然没有找到则返回undefined。就拿上面的例子来讲,调用instance.getSuperValue()会经历三个搜索步骤:1)搜索实例;2)搜索Sub.prototype;3)搜索Super.prototype,最后一步才会找到该方法。在找不到属性或方法的状况下,搜索过程老是要一环一环地前行到原型链的末端才会停下。

原型链问题

原型链继承最大的问题是来自包含引用类型值的原型。引用类型值的原型属性会被全部实例共享。而这正是为何要在构造函数中,而不是原型对象中定义属性的缘由。在经过原型来实现继承时,原型实际上会另外一个类型的实例。因而,原先的实例属性也就瓜熟蒂落地变成了如今的原型属性了。

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

}
Sub.prototype = new Super();    // 继承了Super

let instance1 = new Sub();

instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"

let instance2 = new Sub();
alert(instance2.colors);    //"red, blue, green, black"
复制代码

上面的代码中,Super 构造函数定义了一个colors 属性,该属性是一个数组。Super 的每一个实例都会有各自包含本身数组的colors 属性。当Sub 经过原型链继承了Super以后,Sub.prototype 就变成了Super 的一个实例,所以它也拥有了一个它本身的colors 属性。结果是全部的Sub 实例都会共享这一个colors 属性。 原型链的第二个问题是没有办法在不影响全部对象实例的状况下,给超类的构造函数传递参数。

构造函数继承(经典继承)

即在子类构造函数的中调用父类构造函数,此时当构建一个子类实例时,此实例也会拥有父类实例的属性和方法。

function Super(){
    this.colors = ["red", "blue", "green"];
}
function Sub(){
    Super.call(this, name);    //继承了Super
}

let instance1 = new Sub();

instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"

let instance2 = new Sub();
alert(instance2.colors);    //"red, blue, green"
复制代码

上面的代码,当构建Sub的实例时,也会调用Super 的构造函数,这样就会在新Sub对象上执行Super()函数中定义的全部对象初始化代码。结果,Sub 的每一个实例就都会具备本身的colors 属性的副本了。

构造函数继承问题

若是仅仅是借用构造函数,那么也将没法避免构造函数模式存在的问题——方法都在构造函数中定义,所以函数服用就无从谈起。并且,在超类原型中定义的方法,对子类而已也是不可见的。

组合继承

是指将原型链和构造函数的相结合,发挥两者之长的一种继承模式。其思路是使用原型链实现对原型属性和方法的继承,而经过借用构造函数来实现对实例属性的继承。这样,即经过在原型上定义方法实现了函数复用,又可以保证每一个实例都有它本身的属性。

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

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //继承了Super 属性 (第二次调用Sup构造函数)
    this.age = age;
}

Sub.prototype = new Super();    // 继承了Super 原型链上的方法 (第一次调用Sup构造函数)
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function (){
    alert(this.age);
};

var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20

复制代码

在上面的例子中,Sup构造函数定义了两个属性:name和colors。Sup的原型定义了一个方法sayName()。Sub构造函数在调用Sup构造函数时传入了name参数,紧接着又定义了它本身的属性age。而后,将Sup的实例赋值给Sub的原型,而后又在该新原型上定义了sayAge()方法。这样就可让两个不一样的Sub 实例即分别拥有本身的属性————包括colors 属性,又可使用相同的方法了。 组合继承避免了原型链和构造函数的缺陷,融合了它们的优势,是JavaScript中最经常使用的继承模式。可是美中不足的是,上面的代码中调用了两次父类构造函数。Sub.prototype = new Super(); 第一次调用父类构造函数时,将Sup父类构造函数的实例赋值给了Sub子类的原型对象Sub.prototype。此时也会将父类构造函数实例上的属性赋值给子类的原型对象Sub.prototype。而第二次是在子类的构造函数中调用父类的构造函数 Super.call(this),此时会将父类构造函数实例上的属性赋值给子类的构造函数的实例。根据原型链搜索原则,实例上的属性会屏蔽原型链上的属性。所以咱们没有必要将父类构造函数实例的属性赋值给子类的原型对象,这是浪费资源而又没有意义的行为。

优化后的组合继承

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

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //继承了Super 属性
    this.age = age;
}

function F(){
}
F.prototype = Super.prototype; 
Sub.prototype = new F();    // 继承了Super 原型链上的方法

Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function (){
    alert(this.age);
};

var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20

复制代码

上面的例子经过将父类的原型对象直接赋值给一个中间构造函数的原型对象,而后将这个中间构造函数的实例赋值给子类的原型对象Sub.prototype,从而完成原型链继承。它的高效性体如今只调用了一个父类构造函数Super,而且原型链保持不变。还有一种简便的写法是采用ES5的Object.create()方法来替代中间构造函数,其实原理都是同样的

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

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //继承了Super 属性
    this.age = age;
}
/* function F(){ } F.prototype = Super.prototype; Sub.prototype = new F(); // 继承了Super 原型链上的方法 Sub.prototype.constructor = Sub; */
//这行代码的原理与上面注释的代码是同样的
Sub.prototype = Object.create(Super.prototype, {constructor: {value: Sub}})

Sub.prototype.sayAge = function (){
    alert(this.age);
};

var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20
复制代码

更简单的继承方式

还有一种更简单的继承方法,就是直接将子类的原型对象(prototype)上的__proto__指向父类的的原型对象(prototype),这种方式没有改变子类的原型对象,所以子类原型对象上的constructor属性仍是指向子类的构造函数,并且当子类的实例在子类的原型对象上没有搜索到对应的属性或方法时,它会经过子类原型对象上的__proto__属性,继续在父类的原型对象上搜索对应的属性或方法

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

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //继承了Super 属性
    this.age = age;
}

Sub.prototype.__proto__ = Super.prototype
Sub.prototype.sayAge = function (){
    alert(this.age);
};
var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20
复制代码

Object.setPrototypeOf()

Object.setPrototypeOf()是ECMAScript 6最新草案中的方法,相对于 Object.prototype.proto ,它被认为是修改对象原型更合适的方法

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

Super.prototype.sayName = function (){
    alert(this.name);
};

function Sub(name, age){
    Super.call(this, name);    //继承了Super 属性
    this.age = age;
}

//Sub.prototype.__proto__ = Super.prototype
Object.setPrototypeOf(Sub.prototype, Super.prototype)

Sub.prototype.sayAge = function (){
    alert(this.age);
};
var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20
复制代码

类的静态方法继承

上面全部的继承方法都没有实现类的静态方法继承,而在ES6的class继承中,子类是能够继承父类的静态方法的。咱们可经过Object.setPrototypeOf()来实现类的静态方法继承,很是简单

Object.setPrototypeOf(Sub, Super)
复制代码
function Super(name){
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

Super.prototype.sayName = function (){
    alert(this.name);
};

Super.staticFn = function(){
    alert('Super.staticFn')
}

function Sub(name, age){
    Super.call(this, name);    //继承了Super 属性
    this.age = age;
}

//Sub.prototype.__proto__ = Super.prototype
Object.setPrototypeOf(Sub.prototype, Super.prototype)
Object.setPrototypeOf(Sub, Super)    // 继承父类的静态属性或方法
Sub.staticFn()    // "Super.staticFn"

Sub.prototype.sayAge = function (){
    alert(this.age);
};
var instance1 = new Sub("Luke", 18);
instance1.colors.push("black");
alert(instance1.colors);    //"red, blue, green, black"
instance1.sayName();    //"Luke"
instance1.sayAge()    //18

var instance2 = new Sub("Jack", 20);
alert(instance2.colors);    //"red, blue, green"
instance2.sayName();    //"Jack"
instance2.sayAge()    //20
复制代码

这大概就是最终的理想继承方式吧。

相关文章
相关标签/搜索