JavaScript继承

39 阅读6分钟

前言

今天来复习JS的继承,主要有七类继承

  • 原型链继承
  • 构造函数继承
  • 组合式继承
  • 原型式继承
  • 寄生式继承
  • 寄生式组合继承
  • ES6的class继承

主要是借鉴的《JavaScript高级程序设计》

原型链继承

构造函数、原型和实例之间的关系:构造函数里面都有一个原型对象,原型里面有一个属性指回构造函数明实例里面又有一个内部指针指向原型。

但是如果原型是另一个类型的实例呢?意味着原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数。这样实例和原型之间就有了一条原型链。

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

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

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

SubType.prototype = new SuperType(); 

SubType.prototype.getSubValue = function() {
    return this.subproperty;
}
//进行了覆盖方法
SubType.prototype.getSuperValue = function(){
    return false
}

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

上面的代码就是将SubType的原型用SuperType实例进行了覆盖,所以SubType继承了SuperType的属性和方法。下面是它的对应关系 对应关系

注意点:以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链

SubType.prototype = new SuperType()
SubType.prototype = {
    getSubValue(){
        return this.subproperty;
    },
    someOtherMethod(){
        return false;
    }
}

这样的话原来的原型链就断了,SubTypeSuperType就没有关系了。

原型链的问题

多个实例对引用类型的操作会被篡改。

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

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"

var instance2 = new SubType(); 
alert(instance2.colors); //"red,blue,green,black"

发现创建了一个实例并且修改了内容也会影响到新建实例的相关内容。

构造函数继承

基本思路:在子类构造函数中调用父类构造函数。

function SuperType(){ 
    this.color=["red","green","blue"]; 
} 
function SubType(){ 
    //继承自SuperType
    SuperType.call(this); 
} 
var instance1 = new SubType(); 
instance1.color.push("black"); 
alert(instance1.color);//"red,green,blue,black"
var instance2 = new SubType();
alert(instance2.color);//"red,green,blue"

通过使用call()(胡后者apply方法,SuperType构造函数在位SubType的实例创建的新对象的上下文中执行了,新的SubType对象上运行了SuperType的所有初始代码)

构造函数继承的好处和缺点

好处:可以向父类构造函数传参

function SuperType(name){ 
    this.name = name
} 
function SubType(){ 
    //继承自SuperType
    SuperType.call(this,"Nucholas"); 
    this.age = 29;
} 
let instance = new SubType()
console.log(instance.name)//"Nucholas"
console.log(instance.age)//29

缺点:

  • 只能继承父类的实例属性和方法,不能继承原型属性/方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能

组合式继承

就是综合了原型链和构造函数的继承方法,将两者的优点进行了结合。

基本思路:使用原型链继承原型上的属性和方法,而使用盗用构造函数来继承实例属性。

function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

function SubType(name, age){
  // 继承属性
  //第二次调用SuperType()
  SuperType.call(this, name);
  this.age = age;
}

// 继承方法
//第一次调用SuperType()
SubType.prototype = new SuperType(); 
// 重写SubType.prototype的constructor属性,指向自己的构造函数SubType
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
    alert(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

原型式继承

创建一个里那是构造函数,将你要传入的对下那个赋值给这个构造函数的原型,返回这个构造函数的实例

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

其实这个实现可以通过Object.create()来进行取代

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"

这个函数是一个浅复制。这么做的原因:你有一个对象,想在它基础上在厨房将一个新的对象的时候推荐使用它,它非常适合不需要单独创建构造函数但是仍然需要对象间共享信息的场合。

缺点

和原型链继承缺点一样,当进行了修改那么这个修改的内容会在相关对象之间共享。

寄生式继承

基本思路:原型式继承的基础上,增强对象,返回构造函数

主要是增强构造函数,给构造函数添加了新的函数。

function createAnother(original){
  var clone = object(original); // 通过调用 object() 函数创建一个新对象
  clone.sayHi = function(){  // 以某种方式来增强对象
    alert("hi");
  };
  return clone; // 返回这个对象
}
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"

缺点

和构造函数缺点一样

寄生组合式继承

组合式继承存在着效率问题:父类构造函数会呗 调用两次,在上面组合式继承进行了标注

function inheritPrototype(subType, superType){
  var prototype = Object.create(superType.prototype); // 创建对象,创建父类原型的一个副本
  prototype.constructor = subType;                    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
  subType.prototype = prototype;                      // 指定对象,将新创建的对象赋值给子类的原型
}

// 父类初始化实例属性和原型属性
function SuperType(name){
  this.name = name;
  this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};

// 借用构造函数传递增强子类实例属性(支持传参和避免篡改)
function SubType(name, age){
  SuperType.call(this, name);
  this.age = age;
}

// 将父类原型指向子类
inheritPrototype(SubType, SuperType);

// 新增子类原型属性
SubType.prototype.sayAge = function(){
  alert(this.age);
}

var instance1 = new SubType("xyc", 23);
var instance2 = new SubType("lxy", 23);

instance1.colors.push("2"); // ["red", "blue", "green", "2"]
instance1.colors.push("3"); // ["red", "blue", "green", "3"]

inhertPrototype()函数实现了寄生式组合继承的核心逻辑。

在函数内部,第一步创建了父类原型的副本,然后给返回的prototype对象设置constructor属性,解决由于重写导致默认的constructor属性丢失的问题。最后将新创建的对象赋值给子类的原型。

优点:只调用了一次SuperType构造函数,避免了SubType.prototype上的不必要的属性。

ES6的class继承

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

类的继承是按照Object.setPrototypeOf方法的实现的,先看一下它的实现

Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

传入两个对象,第一个对象的原型就是第二个对象然后将这个对象返回出去,这样他们就构成了继承的关系。

class A {
}

class B {
}

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype;

// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
// 等同于
B.__proto__ = A;

const b = new B();

extends继承核心代码

function _inherits(subType, superType) {
  
    // 创建对象,创建父类原型的一个副本
    // 增强对象,弥补因重写原型而失去的默认的constructor 属性
    // 指定对象,将新创建的对象赋值给子类的原型
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: {
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}

总结

  1. ES5和ES6继承的区别

ES5: 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。

ES6: 因为子类要调用super,所以ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承