JS中的继承-1【总结】

354 阅读7分钟

相关知识点

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

原型链继承

将子类的原型对象指向父类的实例

题目一

function Parent() {
  this.name = "Parent";
  this.sex = "男";
}

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

function Child() {
  this.name = "child";
}

Child.prototype = new Parent();

var child1 = new Child();
console.log(child1); // Child {name: "Child"}
child1.getName(); // child

child1 是通过子类构造函数 Child 生成的对象,自身就有属性 name,并且属性值也是自己的 child。然后子构造函数 Child 它的原型被指向了父类构造函数 Parent 创建出来的匿名实例。这样的话,child1 就可以使用这个匿名实例里的所有属性和方法了,因此,child1.getName() 有效,并且打印出 child。由于 sex、getName 都是 Child 原型对象上的属性,所以并不会表现在 child1 上。

这种方式就叫做原型链继承,将子类的原型对象指向父类的实例

写个伪代码,方便记忆:

Child.prototype = new Parent();

更加严谨一点的做法其实还有一步:Child.prototype.constructor = Child;

题目二

function Parent() {
  this.name = "Parent";
  this.sex = "男";
}

Parent.prototype.getSex = function () {
  console.log(this.sex);
};

function Child() {
  this.name = "Child";
}

Child.prototype = Parent.prototype;

var child1 = new Child();
console.log(child1); // Child {name: "Child"}
child1.getSex(); // undefined

如果原型链继承使用 Child.prototype = Parent.prototype;child1 上能使用的属性和方法只有 name、getSex,所以 getSex 打印出的会是 undefined,打印出的 child1 只有 name 属性,getSex 为原型上的方法所以并不会表现出来。

题目三

function Parent(name) {
  this.name = name;
  this.sex = "boy";
  this.colors = ["white", "black"];
}

function Child() {
  this.feature = ["cute"];
}

var parent = new Parent("parent");
Child.prototype = parent;

var child1 = new Child("child1");

// 相当于给 child1 的实例对象增加 sex
child1.sex = "girl";
child1.colors.push("yellow");
child1.feature.push("sunshine");

var child2 = new Child("child2");

console.log(child1); // Child{ feature: ['cute', 'sunshine'], sex: 'girl' }
console.log(child2); // Child{ feature: ['cute'] }

console.log(child1.name); // parent
console.log(child1.colors); // ["white", "black", "yellow"]
console.log(child2.colors); // ["white", "black", "yellow"]

console.log(parent); // Parent {name: "parent", sex: 'boy', colors: ['white', 'black', 'yellow'] }

child1 在创建完之后,就设置了 sex,并且给 colorsfeaturepush 了新的内容。child.sex = 'girl' 这段代码相当于是给 child 这个实例对象增加了一个 sex 属性。相当于是:原本我是没有 sex 这个属性的,我想要获取就得拿原型对象 parent 上的 sex ,但是现在你加了一句 child1.sex 就等于是我自己也有了这个属性了,就不需要你原型上的了,所以并不会影响到原型对象 parent 上。

但是 child1.colors 这里,注意它的操作,它是直接使用了 .push() 的,也就是说我得先找到 colors 这个属性,发现实例对象 parent 上有,然后就拿来用了,之后执行 push 操作,所以这时候改变的是原型对象 parent 上的属性,会影响到后续所有的实例对象。

这里需要注意一下,此时的 colorssex 操作方式不同,所以效果也就不同,sex 使用 = 进行赋值,不管有没有 sex 属性就直接赋值了,而 colors.push 操作是先有 colors 且类型是数组才行,如果你使用的是 child1.colors = ['yellow'] 这样才不会影响 parent )。

feature 它是属于 child1 实例自身的属性,它添加还是减少都不会影响到其他实例。

因此 child1 打印出了 featuresex 两个属性。( namecolors 属于原型对象上的属性并不会被表现出来)。child2 没有做任何操作,所以它打印出的还是它自身的一个 feature 属性。

child1.name 是原型对象 parent 上的 name,也就是 parent,虽然我们在 new Child 的时候传递了 child1,但它显然是无效的,因为接收 name 属性的是构造函数 Parent,而不是 Child

child2.colors 由于用的也是原型对象 parent 上的 colors,又由于之前被 child1 给改变了,所以打印出来的会是 ['white', 'black', 'yellow']

将最后的原型对象 parent 打印出来,namesex 没变,colors 却变了。

总结-原型链继承

  • 优点:
    • 继承了父类的模板,又继承了父类的原型对象;
  • 缺点:
    • 如果要给子类的原型上新增属性和方法,就必须放在 Child.prototype = new Parent(); 语句的后面;
    • 无法实现多继承,因为已经指定了原型对象了;
    • 来自原型对象的所有属性都被共享了,这样如果不小心修改了原型对象中的引用类型属性,那么所有子类创建的实例对象都会受到影响(这点从修改 child1.color 可以看出来);
    • 创建子类时,无法向父类构造函数传参数(这点从 child1.name 可以看出来);

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

在子类构造函数内部使用 callapply 来调用父类构造函数

题目一

function Parnet(name) {
  this.name = name;
}

function Child() {
  this.sex = "boy";
  Parnet.call(this, "child");
}

var child1 = new Child();
console.log(child1); // Child {sex: "boy", name: "child"}

我们使用了 Parent.call(this, 'child').call 函数是会立即执行的,而这里又用了 .call 来改变 Parent 构造函数内的指向,所以可以将它转化为伪代码:

function Child () {
  this.sex = 'boy'
  // 伪代码
  this.name = 'child'
}

可以理解为相当于是直接执行了 Parent 里的代码。

使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类。

构造继承的原理就是:在子类构造函数内部使用 callapply 来调用父类构造函数。

题目二

function Parent(name) {
  this.name = name;
}

function Child() {
  this.sex = "boy";
  Parent.call(this, "good boy");
  this.name = "bad boy";
}

var child1 = new Child();
console.log(child1); // Child {sex: "boy", name: "bad boy"}

如果换一下位置:

function Parent(name) {
  this.name = name;
}

function Child() {
  this.sex = "boy";
  this.name = "bad boy";
  Parent.call(this, "good boy");
}

var child1 = new Child();
console.log(child1); // Child {sex: "boy", name: "good boy"}

题目三

解决了原型链继承中子类共享父类引用对象的问题。

function Parent(name, sex) {
  this.name = name;
  this.sex = sex;
  this.colors = ["white", "black"];
}

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

var child1 = new Child("child1", "boy");
child1.colors.push("yellow");

var child2 = new Child("child2", "girl");
console.log(child1); // Child{ name: 'child1', sex: 'boy', colors: ['white', 'black', 'yellow'] }
console.log(child2); // Child{ name: 'child2', sex: 'girl', colors: ['white', 'black'] }

在原型链继承中,子类构造函数创建的实例是会查找到原型链上的 colors 的,而且改动它会影响到其它的实例,这是原型链继承的一大缺点。

而构造函数继承,发现修改 child1.colors 并不会影响其它的实例(child2),使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类。所以现在 child1child2 现在分别有它们各自的 colors 了,就不共享了。 而且这种拷贝属于深拷贝。

构造函数继承优点:解决了原型链继承中子类实例共享父类引用对象的问题,实现多继承,创建子类实例时,可以向父类传递参数。

题目四

function Parent(name) {
  this.name = name;
}
Parent.prototype.getName = function () {
  console.log(this.name);
};
function Child() {
  this.sex = "boy";
  Parent.call(this, "good boy");
}
Child.prototype.getSex = function () {
  console.log(this.sex);
};
var child1 = new Child();
console.log(child1); // Child {sex: "boy", name: "good boy"}
child1.getSex(); // boy
child1.getName(); // Uncaught TypeError: child1.getName is not a function

构造函数继承的缺点:构造函数只能继承父类的实例属性和方法,不能继承父类原型上的属性和方法。

题目五

function Parent(name) {
  this.name = name;
}
function Child() {
  this.sex = "boy";
  Parent.call(this, "child");
}

var child1 = new Child();

console.log(child1); // Child {sex: "boy", name: "child"}
console.log(child1 instanceof Child); // true
console.log(child1 instanceof Parent); // false
console.log(child1 instanceof Object); // true

第一个 true 很好理解,child1 本身就是 Child 的实例。

第二个输出 false,是因为 child1 只是复制 Child 的属性和方法,并不能继承它。

第三个输出 true 是必然的,实例的原型链如果没有发生改变的话最后都能找到 Object.prototype

构造函数继承的第二缺点:实例并不是父类的实例,只是子类的实例。

总结-构造函数继承

  • 优点:
    • 解决了原型链继承中子类实例共享父类引用对象的问题,实现多继承,创建子类实例时,可以向父类传递参数;
  • 缺点:
    • 构造继承只能继承父类的实例属性和方法,不能继承父类原型的属性和方法;
    • 实例并不是父类的实例,只是子类的实例;
    • 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能;

今天就先总结到这里,下一期继续总结后续内容 JS中的继承-2【总结】~~