聊聊JS继承那些事

389 阅读5分钟

先谈谈这篇文章的来源,其实我个人在工作当中几乎没有用到继承,但是这块的内容也算是面试中必考的内容,所以在每个阶段准备面试的时候都会看下继承的内容,之前在网上看了很多大神的博客,也没看懂,可能是因为自己的水平问题,只能靠硬性的去记忆。

不死心的我昨晚静下心来看了下红宝书第四版的继承部分,才恍然大悟,感觉自己终于可以理解一点,不需要再死记硬背了。所以在这篇文章记录下自己在理解继承中遇到的困境以及理解,仅作为记录学习使用。

注:以下的代码部分都是来自mqyqingfeng大佬的博客

1.原型继承

function Parent (age) {
    this.name = 'kevin';
    this.age = age;
    this.list = [1,2,3]
}

Parent.prototype.getName = function () {
    console.log(this.name);
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

console.log(child1.getName()) // kevin

原型继承的好处就是书写方便容易理解,直接将子类的原型指向父类的实例就行。

所以子类的原型上包含了,父类实例上拥有的所有的属性。我的理解就是,在父类中所有的属性(不管是构造函数内部还是原型上)都被挂载在了子类的原型上。

这样就会造成的一个问题就是,当子类创建实例的时候,全都拥有了这些属性。突出的缺点就是当一个实例对于父类一些复杂数据类型(比如父类中的list)做修改时 ,其他实例都会发生改变,看下面代码:

var child1 = new Child()
var child2 = new Child()
child1.list.push(4)
console.log(child1.list) // [1,2,3,4]
console.log(child2.list) // [1,2,3,4]

在这里可能会问了,为什么要强调复杂数据类型呢?基础数据类型不会有这个问题吗?其实这也是我最开始的疑问。简单来说,只有复杂的数据类型(比如数组、对象)你才可以对他们进行一些属性的添加或者删除而不是去改变它的引用,如果是基础数据类型的话,只能去修改这个值,这时候就相当于在实例上添加了一个同名的属性,那么就会覆盖原型上的属性啦,看代码:

var child1 = new Child()
var child2 = new Child()
child1.name = 'zhaosi' // 这里相当于为实例child1添加了一个name属性,就不会去读原型上的name了
console.log(child1.name) // 'zhaosi'
console.log(child2.name) // 'kevin'

缺点2:无法向构造函数传参。可以回头看下Parent函数中有个age属性是需要实例传参的,但是你在子类的实例中找不到传参的入口

2.构造函数继承

function Parent (age) {
    this.names = ['kevin', 'daisy'];
    this.age = age;
}

function Child (age) {
    Parent.call(this, age);
}

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child(22);

console.log(child2.names); // ["kevin", "daisy"]
console.log(child2.age); // 22

构造函数继承的时候就是在子类构造函数的内部执行了Parent.call(this),将父类的this指向了子类。所以从这里就可以看出缺陷了,这种方式只能继承父类构造函数定义的属性或者方法,不能继承父类原型上的属性,你可能会说了,这有什么问题呢?我把所有的函数和属性就定义在父类构造函数的内部不就行了,不在原型上写。

道理上来讲,这种是完全可行的,完全可以实现继承。但是回头想想我们写代码不就是为了精益求精吗?我们的目标不就是复用吗?如果将函数也定义在构造函数中,那我们每次new一个实例,就要创建一个函数,岂不是浪费资源?

所以啊,构造函数继承也不是最理想的方式,它虽然解决了原型继承里的传参和复合类型数据共享的问题,但也带来的新的问题就是不能继承父类原型上的内容,换句话说就是一些可以公用的东西,在实例中不能公用。

3.组合继承

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

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {

    Parent.call(this, name); // 第二次调用
    
    this.age = age;

}

Child.prototype = new Parent(); // 第一次调用
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');

child1.colors.push('black');

console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');

console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

组合继承解决了原型继承和构造函数继承的问题,将两种继承方式进行完美的结合。但是呢这种方式也会带来一个问题就是,在代码中可以看到,一共调用了两次父类的构造函数,这会导致一个什么问题呢?

在第一次调用Child.prototype = new Parent()Child的原型继承了Parent也就是说Parent的属性已经赋值到子类的原型上了,但是在第二次调用的时候,又将这些属性赋值到实例上了,也就是说子类的实例和原型都包含这些属性,是不是重复了?我们看下代码:

console.log(child2.colors) // ["red", "blue", "green"]
console.log(Child.prototype.colors) // ["red", "blue", "green"]

好了,我们的目标是只调用一次,这就来了寄生组合继承

4.寄生组合继承

我们只需要将Child.prototype = new Parent()这一步替换掉就行了,改造后如下

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

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {

    Parent.call(this, name); // 第二次调用
    
    this.age = age;

}

Child.prototype = Parent.prototype;
Child.prototype.constructor = Child; // 修正构造函数的指向

这样真的就万事大吉了吗?我们来打印看下子类实例的构造函数吧?

var p = new Parent()
console.log(p.constructor) // Child

这里为什么Parent实例的构造函数居然是Child呢?我们来改下我们刚才做的修改

Child.prototype = Parent.prototype;
Child.prototype.constructor = Child;
// 等价于
var Parent = {
	name: 'zhaosi',
	constrcutor: 'Parent'
}
var Child = Parent
Child.constrcutor = 'Child'

其实就是一个对象赋值的问题,赋值后一个对象的属性发生改变,另外一个自然也变了,所以在执行Child.prototype.constructor = Child的时候父类的constructor也指向Child了,那就修改下呗,不让两个原型直接关联,最终如下:

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

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}

// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();


var child1 = new Child('kevin', '18');

console.log(child1);

我还想到两种方式,供参考不知道可不可行:

Child.prototype = Parent.prototype;
方式1Child.prototype = Object.assign({}, Parent.prototype)
方式2Child.prototype = Object.create(Parent.prototype)