likes
comments
collection
share

JavaScript | 继承

作者站长头像
站长
· 阅读数 13

原文:JavaScript | 继承

之前写过原型和原型链,但是在JavaScript中涉及到原型与原型链相关的知识的时候就绕不开继承这个知识点,所以打算整理一下继承相关的知识点。

在ES6推出Class关键字前JavaScript使用了非常多种方案来实现继承,下面就来看下在这之前实现继承的各种方案吧!

继承

说到继承那么不得不提到类与对象的概念,下面就来看看吧:

是抽象的,它是拥有共同的属性和行为的抽象实体。

比如:

  • 学生是一个类,而XXX同学则是一个对象。
  • 手机的设计图可以看作一个类,一台真实的手机可以看作是一个对象。

对象

现实生活中:万物皆对象,对象是一个具体的事物,看得见摸得着的实物。

例如:一本书、一辆汽车、一个人可以是“对象”,一个数据库、一张网页、一个与远程服务器的连接也可以是"对象"。

对象是具体的,它除了拥有类共同的属性和行为之外,可能还会有一些独特的属性和行为:

  • 属性:事物的特征,在对象中用属性来表示(常用名词)
  • 方法:事物的行为,在对象中用方法来表示(常用动词)

JavaScript | 继承

在JavaScript中,对象是一个无序的相关属性和方法的集合,JavaScript中所有的事物都是对象

例如:字符串、数值、数组、函数等。

什么是继承?

继承的概念来源于现实生活,比如汽车、轮船、火车飞机等都属于交通工具,可以继承于交通工具这个类,而汽车类又可以派生出新的类,比如轿车、面包车等等。

比如子类在继承父类之后,就拥有了父类的属性与方法,这里最主要的好处就是子类可以通过继承来复用父类中的代码(代码重用)。

继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实际的方法。接口继承在 ECMAScript 中是不可能的,因为函数没有签名。实现继承是 ECMAScript 唯一支持的继承方式,而这主要是通过原型链实现的。——《JavaScript高级程序设计》(第四版)

简单解释一下,JavaScript不是像Java那样是一门真正面向对象的语言,JavaScript是基于对象的,没有类的概念。所以在JavaScript中实现继承一般是使用prototype(原型)或用applycall方法去实现的。

JavaScript中继承的最佳实践是寄生组合式继承,它结合了原型继承借用构造函数继承

原型链继承

之前一篇文章有提到过JS对象成员查找机制,简单来说就是当访问Js中对象的属性或者是方法的时候,如果在当前这个对象内没有找到这个属性或者方法,就会顺着原型链,在原型上查找对应的属性与方法。

所以可以利用原型链的特性,将子构造函数的prototype(原型对象)指向父构造函数的实例对象,当子类对象中找不到对应的属性或者方法的时候,就会通过父构造函数的__proto__属性(父构造函数的原型)查找对应的属性与方法。

语法:

  • Son.prototype = new Father();

    !注意:这种写法相当于Son.prototype = {},如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数

// 父类构造函数
function Father() {
    this.name = 'father';
}

// 子类构造函数
function Son() {
    this.age = 16;
}

// 子类的prototype指向父类的实例
Son.prototype = new Father();

/**
 * 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数
 * 上面使用的方式相当于'Son.prototype = {}',导致Son.prototype中就没有constructor了
 * 需要重新给Son.prototype的constructor赋值
*/
Son.prototype.constructor = Son;

var son = new Son();
console.log(son.name); // father
console.log(son.age); // 16

小结

  • 原型链继承特点:可以继承父类的私有属性和原型属性。

  • 原型链继承的缺点:无法向父类构造函数传参。

借用构造函数继承

前置知识:了解JavaScript中callapply的作用(详细请看这里)。

核心原理:通过call或者apply方法把父类型的this指向子类型的this,这样就可以实现子类型继承父类型的属性

// 父类构造函数
function Pen(brand, manufacturer) {
    this.brand = brand;
    this.manufacturer = manufacturer;
}

// 父类原型上的write方法
Pen.prototype.write = function () {
    return 'I can write!';
}

// 子类构造函数
function MPen(brand, manufacturer, draw) {
    // 通过call方法,将父类构造函数中的this指向子类构造函数中的this,从而实现属性的继承
    Pen.call(this, brand, manufacturer);
    this.draw = draw;
}

MPen.prototype.specOff = true;

let mp = new MPen('晨光', '厂家', '荧光绘画');
// console.log(mp);
// 调用父类的私有属性brand
console.log(mp.brand); //晨光
// 调用父类的原型属性write
// mp.write(); //mp.write is not a function
// 调用子类属的私有属性
console.log(mp.draw);
// 调用子类的原型属性specOff
console.log(mp.specOff);

小结

  • 借用构造函数继承的特点:

    • 在子类中可以给父类传参。
    • 可以继承父类的私有属性。
  • 借用构造函数继承的缺点:只能继承父类的私有属性,不能继承父类的原型属性。

组合继承(最常用)

核心原理:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

这样就可以解决借用构造函数继承无法继承父类的原型属性的问题了。

// 借用父构造函数继承属性
// 1. 父构造函数
function Father(uname, age) {
    // this 指向父构造函数的对象实例
    this.uname = uname;
    this.age = age;
}

// 父构造函数自己的方法 
Father.prototype.money = function () {
    console.log('get 10000$');
}

// 2. 子构造函数
function Son(uname, age, score) {
    // this 指向子构造函数的对象实例
    // 将父亲的this 改成了 孩子的this
    Father.call(this, uname, age);
    this.score = score;

}

// Son.prototype = Father.prototype; 这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会一起变化
Son.prototype = new Father();

/**
 * 如果利用对象的形式修改了原型对象,别忘了利用constructor 指回原来的构造函数
 * 上面使用的方式相当于'Son.prototype = {}',导致Son.prototype中就没有constructor了
 * 需要重新给Son.prototype的constructor赋值
*/
Son.prototype.constructor = Son;

// 子构造函数专门的方法
Son.prototype.exam = function () {
    console.log('kids need to test!');
}
var son = new Son('ldh', 18, 99);

// 调用父类的私有属性uname
console.log(son.uname); //ldh
// 调用父类的原型属性money
son.money(); //get 10000$
// 调用子类的私有属性score
console.log(son.score); //99
// 调用子类的原型属性
son.exam(); // kids need to test!

不过组合继承也有些小问题:

组合继承的父类在使用过程中会被调用两次,一次是创建子类型的时候,另一次是在子类型构造函数的内部。

// 1. 父构造函数
function Father(uname, age) {
    // this 指向父构造函数的对象实例
    this.uname = uname;
    this.age = age;
}

// 2. 子构造函数
function Son(uname, age, score) {
    Father.call(this, uname, age); // 第二次调用
    this.score = score;

}

Son.prototype = new Father(); // 第一次调用

Son.prototype.constructor = Son;

这就造成了数据的冗余,比如父类的uname属性,既存在于Father类中,作为它的私有属性。又存在于Son类的prototype中,作为它的原型属性。

小结

  • 组合继承特点:
    • 解决借用构造函数继承无法继承父类的原型属性的问题,子类可以同时继承父类原型属性与私有属性
    • 子类可以给父类传参数
  • 组合继承缺点:
    • 组合继承的父类在使用过程中会被调用两次,这对于父类的私有属性,子类继承时候同时存在于私有属性和原型属性中,进而造成了冗余。

原型式继承

这种继承借助原型并基于已有的对象创建新对象,同时还不用创建自定义类型也可以通过原型实现对象之间的信息共享的方式成为原型式继承。

核心原理:创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例,简单来说是对传入的对象执行了一次浅复制。

// obj()创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。
// 简单来说obj()是对传入的对象执行了一次浅复制。
function obj(o) {
    function F() { }
    F.prototype = o;
    return new F();
}

var box = {
    name: 'Breaking Bad',
    arr: ['Heisenberg', 'Walter Write', 'Jesse Pinkman']
};
// 对象1
var b1 = obj(box);
console.log(b1.name);//Breaking Bad

b1.name = 'Mike';
// 以这种方式添加的属性会遮蔽原型对象上的同名属性
console.log(b1.name);//Mike 

console.log(b1.arr);//Heisenberg,Walter Write,Jesse Pinkman
b1.arr.push('Hank');
console.log(b1.arr);//Heisenberg,Walter Write,Jesse Pinkman,Hank

// 对象2
var b2 = obj(box);
console.log(b2.name);//Breaking Bad
// 可以看到对象1与对象2中的数组的值是共享的(因为是浅复制,记录的是内存地址,而两个实例的name都指向同一个地址)
console.log(b2.arr);//Heisenberg,Walter Write,Jesse Pinkman,Hank

JavaScript | 继承

寄生式继承

寄生式继承结合了原型式与工厂模式,目的是为了封装创建的过程。

核心原理:通过原型式继承中的方法创建一个新对象,再以某种方式增强对象,然后返回这个对象。

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

function create(o) {
    // 1.通过调用函数创建一个新对象
    var f = obj(o);
    // 2.增强对象
    f.sayFriends = function () {
        console.log(this.friends); //同样,会共享引用
    };
    // 3.返回这个对象
    return f;
}

let person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};
person.friends.push('TEST!');
let anotherPerson = create(person);
anotherPerson.sayFriends(); // ['Shelby', 'Court', 'Van']
anotherPerson.friends.push('TEST222');
// 二者会共享引用
console.log(person.friends); //['Shelby', 'Court', 'Van', 'TEST!', 'TEST222']

JavaScript | 继承

寄生组合式继承(最佳实践)

寄生组合式继承解决了组合式继承的冗余问题。

核心原理:如果我们不给子类构造函数的prototype赋值为父类对象,而是赋值为一个只有父类原型属性而没有父类私有属性的对象,那么子类就不会继承到父类的私有属性,只会继承父类的原型属性了。

那么如何创建一个只有父类原型属性而没有父类私有属性的对象呢?

这里就可以使用之前提到的原型式继承了,只要我们传入的需要拷贝的对象是父构造函数的prototypeFather.prototype,就可以创建一个只有父类原型属性而没有父类私有属性的对象了。

// 原型式继承
function obj(o) {
    function F() { }
    F.prototype = o;
    return new F();
}
// 只有父类原型属性而没有父类私有属性的对象
let newObj = obj(Father.prototype)

然后通过寄生式继承的方式实现继承即可:

// 寄生式继承
function inheritPrototype(Son, Father) {
    // 创建一个只拥有父类原型属性没有父类私有属性的实例对象
    let prototype = obj(Father.prototype); // 1. 创建对象
    // 将该对象的构造函数指回Son构造函数,因为下一步重写了Son.prototype
    prototype.constructor = Son; // 2. 增强对象 
    // 给子类构造函数的prototype赋值只有父类原型属性而没有父类私有属性的对象
    Son.prototype = prototype; // 3. 赋值对象
}

测试代码:

// 父构造函数
function Father(uname, age) {
    // this 指向父构造函数的对象实例
    this.uname = uname;
    this.age = age;
}

// 父构造函数自己的方法 
Father.prototype.money = function () {
    console.log('get 10000$');
}

// 子构造函数
function Son(uname, age, score) {
    // this 指向子构造函数的对象实例
    // 将父亲的this 改成了 孩子的this
    Father.call(this, uname, age);
    this.score = score;

}

// 原型式继承
function obj(o) {
    function F() { }
    F.prototype = o;
    return new F();
}

// 寄生式继承
function inheritPrototype(Son, Father) {
    // 创建一个只拥有父类原型属性没有父类私有属性的实例对象
    let prototype = obj(Father.prototype); // 1. 创建对象
    // 将该对象的构造函数指回Son构造函数,因为下一步重写了Son.prototype
    prototype.constructor = Son; // 2. 增强对象 
    // 给子类构造函数的prototype赋值只有父类原型属性而没有父类私有属性的对象
    Son.prototype = prototype; // 3. 赋值对象
}

// 这步实现继承
inheritPrototype(Son, Father);

// 子构造函数专门的方法
Son.prototype.exam = function () {
    console.log('kids need to test!');
}

let son = new Son('Mike', 18, 99);
// 调用父类的私有属性
console.log(son.uname, son.age); // Mike 18
// 调用父类的原型属性money
son.money(); //get 10000$
// 调用子类的私有属性score
console.log(son.score); //99
// 调用子类的原型属性exam
son.exam(); // kids need to test!

这样就实现了:

  1. 子类继承父类的私有属性和原型属性。
  2. 子类可以向父类传递参数。
  3. 继承后没有冗余属性。

总结一下,在ES6之前实现继承的最优解就是寄生组合式继承了!

参考文献