继承

197 阅读5分钟

(一)继承中的属性或者方法查找原则:就近原则

  • 继承中,如果实例化子类输出一个方法,先看子类有没有这个方法,如果有就先执行子类的
  • 如果子类没有,就去查找父类有没有这个方法,如果有,就执行父类的这个方法(就近原则)

(二)JS 中的常用的继承方式及其优缺点⭐⭐⭐⭐⭐

ES5:最常见的两种继承:原型链继承、构造函数继承
ES6:extend继承(寄生组合继承的语法糖)

1. 原型继承

(1)思路:

  • 把父类的实例作为子类的原型

(2)缺点:

  • 子类的实例共享了父类构造函数的引用属性
  • 不能传参
var person = {
    friends: ['a', 'b', 'c', 'd']
};
var p1 = Object.create(person);
p1.friends.push('aaa'); //缺点:子类的实例共享了父类构造函数的引用属性
console.log(p1);
console.log(person); //缺点:子类的实例共享了父类构造函数的引用属性

2. (ES5)原型链继承(prototype)

原型链继承:将子对象的 prototype 指向父对象的 实例

(1)缺点

  • 原型中包含的引用值会在所有实例间共享
  • 子类型在实例化时不能给父类型的构造函数传参。

(2)基本思路
通过子类的prototype属性和父类进行关联继承

  • 将父对象的 实例指向 子对象的 prototype,实现了对SuperType的继承。
    SubType通过创建SuperType的实例并将其赋值给自己的原型SubTtype.prototypeSubType.prototype = new SuperType()
  • 这个赋值重写了SubType最初的原型,将其替换为SuperType的实例(关键)。
    这意味着SuperType实例可以访问的所有属性和方法也会存在于SubType.prototype。
  • 这样实现继承之后,代码紧接着又给SubType.prototype,也就是这个SuperType的实例添加了一个新方法。
  • 最后又创建了SubType的实例并调用了它继承的getSuperValue()方法

① 定义父类

// -------------- 父类 ---------------
function SuperType() {
    this.property = true;
    this.colors = ['red', 'blue', 'green'];
}
// 父类方法
SuperType.prototype.getSuperValue = function () {
    return this.property;
};

② 定义子类

// -------------- 子类 --------------
function SubType() {
    this.subproperty = false;
}
// 继承父类SuperType,通过子类的prototype属性和父类进行关联继承
// SubType通过创建SuperType的实例并将其赋值给自己的原型SubTtype.prototype实现了对SuperType的继承
SubType.prototype = new SuperType(); //☆☆☆创建SuperType的实例并将其赋值给自己的原型
SubType.prototype.getSubValue = function () {
    return this.subproperty;
};
    

③ 测试

// ---------- SuperType实例可以访问父类的所有属性和方法 ----------
let instance = new SubType();
// SuperType实例可以访问的所有属性和方法也会存在于SubType.prototyp
console.log(instance.getSuperValue()); // true,

// ------------- 缺点:原型中包含的引用值会在所有实例间共享 -------------
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

3. (ES5)构造函数继承(组合继承)(prototype + call)

构造函数继承关键在于,使用apply()或call()方法,将父对象的构造函数绑定在子对象上,可以在新创建的对象上获取父类的成员和方法。

(1)优缺点

  • 优点
    • 可传参,不共享父类引用属性
    • 是JavaScript中使用最多的继承模式。而且组合继承也保留了instanceof操作符和isPrototypeOf()方法识别合成对象的能力
  • 缺点
    • 调用了两次父类的构造函数,造成了不必要的消耗,父类方法可以复用

(2)基本思路

  • 使用原型链继承原型上的属性和方法,SubType.prototype = new SuperType();
  • 通过盗用构造函数call继承实例属性SuperType.call(this, name);

① 父类

// -------------- 父类 ---------------
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
};

② 子类继承
☆☆☆在子函数中运行父函数,但是要利用call把this的指向改变一下

// -------------- 子类 ---------------
function SubType(name, age) {
    // 通过call继承父类属性
    SuperType.call(this, name); // ☆☆☆在子函数中运行父函数,但是要利用call把this改变一下,
    this.age = age;
}
// 继承方法
SubType.prototype = new SuperType(); //☆☆☆
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

③ 测试

//测试
let instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29

let instance2 = new SubType('Greg', 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27

4. 寄生组合继承(prototype + call + constructor)

  • 通过借用构造函数来继承属性, 通过原型链来继承方法
  • 不必为了指定子类型的原型而调用父类型的构造函数,我们只需要父类型的一个副本而已(本质上就是使用寄生式继承来继承超类型的原型, 然后再讲结果指定给子类型的原型)

缺点

  • 最主要的效率问题就是父类构造函数始终会被调用两次
    • 一次在是创建子类原型时调用,
    • 另一次是在子类构造函数中调用。
// -------------- 父类 ---------------
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); // 第二次调用SuperType()
    this.age = age;
}
// 通过原型链来继承方法
SubType.prototype = new SuperType(); // 第一次调用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    console.log(this.age);
};

5. (ES6) extend(寄生组合继承的语法糖)

  • ES6中新增了class关键字来定义类,通过保留的关键字extends实现了继承。实际上这些关键字只是一些语法糖,底层实现还是通过原型链之间的委托关联关系实现继承。
  • 子类只要继承父类,可以不写constructor,一旦写了,则在constructor中的第一句话必须是super
class SubType extends SuperType {
    constructor(x, y, color) {
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
    }
    toString() {
        return this.color + ' ' + super.toString(); // 调用父类的toString()
    }
}

[延伸问题]

1. ES5/ES6 的继承除了写法以外还有什么区别?

区别于ES5的继承,ES6的继承实现在于使用super关键字调用父类,而ES5是通过call或者apply回调方法调用父类。

  • class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 letconst 声明变量。
  • class 声明内部会启用严格模式
  • class 内的所有方法(包括静态方法和实例方法)都是不可枚举的。
  • class 内的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
  • 必须使用 new 调用 class
  • class 内部无法重写类名。