全网最详细的js继承讲解

68 阅读6分钟

第⼀部分:预备知识

1、构造函数的属性

funcion A(name) { 
this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
this.arr = [1]; // 实例引⽤属性 (该属性,强调私⽤,不共享) 
this.say = function() { // 实例引⽤属性 (该属性,强调复⽤,需要共享) console.log('hello') } }

注意:数组和⽅法都属于‘实例引⽤属性’,但是数组强调私有、不共享的。⽅法需要复⽤、共享。 在构造函数中,⼀般很少有数组形式的引⽤属性,⼤部分情况都是:基本属性 + ⽅法。

2、什么是原型对象

简单来说,每个函数都有prototype属性,它就是原型对象,通过函数实例化出来的对象有个 __proto__属性,指向原型对象。

let a = new A() 
a.__proto__ == A.prototype // prototype的结构如下
A.prototype = { constructor: A, ...其他的原型属性和⽅法 }

3、原型对象的作⽤

原型对象的⽤途是为每个实例对象存储共享的⽅法和属性,它仅仅是⼀个普通对象⽽已。并且所有 的实例是共享同⼀个原型对象,因此有别于实例⽅法或属性,原型对象仅有⼀份。⽽实例有很多 份,且实例属性和⽅法是独⽴的。在构造函数中:为了属性(实例基本属性)的私有性、以及⽅法(实 例引⽤属性)的复⽤、共享。我们提倡: 将属性封装在构造函数中 将⽅法定义在原型对象上

funcion A(name) { 
this.name = name; // (该属性,强调私有,不共享) } 
A.prototype.say = function() { // 定义在原型对象上的⽅法 (强调复⽤,需要共享) console.log('hello') } 
// 不推荐的写法:[原因](https://blog.csdn.net/kkkkkxiaofei/article/details/46474303) A.prototype = { 
say: function() 
{ console.log('hello') } }

第⼆部分:五种js 继承⽅式

⽅式1、原型链继承

核⼼:将⽗类实例作为⼦类原型

优点:⽅法复⽤

  • 由于⽅法定义在⽗类的原型上,复⽤了⽗类构造函数的⽅法。⽐如say⽅法。

缺点:

  • 创建⼦类实例的时候,不能传⽗类的参数(⽐如name)。
  • ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性。
  • ⽆法实现多继承。
function Parent(name) {
this.name = name || '⽗亲'; // 实例基本属性 (该属性,强调私有,不共享) 
this.arr = [1]; // (该属性,强调私有) } 
Parent.prototype.say = function() {
// -- 将需要复⽤、共享的⽅法定义在⽗类原型上
console.log('hello')
} 
function Child(like) { 
this.like = like; } 
Child.prototype = new Parent() // 核⼼,但此时Child.prototype.constructor==Parent
Child.prototype.constructor = Child // 修正constructor指向 
let boy1 = new Child() let boy2 = new Child() 
// 优点:共享了⽗类构造函数的say⽅法 
console.log(boy1.say(), boy2.say(), boy1.say === boy2.say); // hello , hello , true
// 缺点1:不能向⽗类构造函数传参
console.log(boy1.name, boy2.name, boy1.name===boy2.name); // ⽗亲,⽗亲,true
// 缺点2: ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性 
boy1.arr.push(2);
// 修改了boy1的arr属性,boy2的arr属性,也会变化,因为两个实例的原型上(Child.prototype)有了⽗类构造函数的实例属性arr;
所以只要修改了boy1.arr,boy2.arr的属性也会变化。
console.log(boy2.arr); // [1,2] 
注意1:修改boy1的name属性,是不会影响到boy2.name。因为设置boy1.name相当于在⼦类实例新增了name属性。
注意2console.log(boy1.constructor); // Parent 你会发现实例的构造函数居然是Parent。
⽽实际上,我们希望⼦类实例的构造函数是Child,所以要记得修复构造函数指向。 
修复如下:Child.prototype.constructor = Child;

⽅式2、借⽤构造函数

  • 核⼼:借⽤⽗类的构造函数来增强⼦类实例,等于是复制⽗类的实例属性给⼦类。
  • 优点:实例之间独⽴。
    • 创建⼦类实例,可以向⽗类构造函数传参数。
    • ⼦类实例不共享⽗类构造函数的引⽤属性。如arr属性
    • 可实现多继承(通过多个call或者apply继承多个⽗类)
  • 缺点:
    • ⽗类的⽅法不能复⽤ 由于⽅法在⽗构造函数中定义,导致⽅法不能复⽤(因为每次创建⼦类实例都要创建⼀遍⽅法)。 ⽐如say⽅法。(⽅法应该要复⽤、共享)

    • ⼦类实例,继承不了⽗类原型上的属性。(因为没有⽤到原型)

function Parent(name) { 
this.name = name; // 实例基本属性 (该属性,强调私有,不共享) 
this.arr = [1]; // (该属性,强调私有)
this.say = function() { 
// 实例引⽤属性 (该属性,强调复⽤,需要共享)
console.log('hello') }
}
function Child(name,like) { 
Parent.call(this,name); // 核⼼ 拷⻉了⽗类的实例属性和⽅法 
this.like = like; 
} 
let boy1 = new Child('⼩红','apple');
let boy2 = new Child('⼩明', 'orange '); 
// 优点1:可向⽗类构造函数传参
console.log(boy1.name, boy2.name); // ⼩红, ⼩明 // 
优点2:不共享⽗类构造函数的引⽤属性
boy1.arr.push(2); 
console.log(boy1.arr,boy2.arr);// [1,2] [1]
// 缺点1:⽅法不能复⽤ 
console.log(boy1.say === boy2.say) // false (说明,boy1和boy2的say⽅法是独⽴,不是共享的) 
// 缺点2:不能继承⽗类原型上的⽅法 
Parent.prototype.walk = function () { // 在⽗类的原型对象上定义⼀个walk⽅法。
console.log('我会⾛路')
}
boy1.walk; // undefined (说明实例,不能获得⽗类原型上的⽅法)

⽅式3、组合继承

  • 核⼼:通过调⽤⽗类构造函数,继承⽗类的属性并保留传参的优点;然后通过将⽗类实例作为 ⼦类原型,实现函数复⽤。
  • 优点:
  1. 保留构造函数的优点:创建⼦类实例,可以向⽗类构造函数传参数。
  2. 保留原型链的优点:⽗类的⽅法定义在⽗类的原型对象上,可以实现⽅法复⽤。
  3. 不共享⽗类的引⽤属性。⽐如arr属性
  • 缺点:
  1. 由于调⽤了2次⽗类的构造⽅法,会存在⼀份多余的⽗类实例属性,具体原因⻅⽂末。
  2. 注意:'组合继承'这种⽅式,要记得修复Child.prototype.constructor指向
  3. 第⼀次Parent.call(this);从⽗类拷⻉⼀份⽗类实例属性,作为⼦类的实例属性,第⼆次 Child.prototype = new Parent();创建⽗类实例作为⼦类原型,Child.protype中的⽗类属性和⽅法 会被第⼀次拷⻉来的实例属性屏蔽掉,所以多余。
function Parent(name) {
this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
this.arr = [1]; // (该属性,强调私有) } 

Parent.prototype.say = function() { // --- 将需要复⽤、共享的⽅法定义在⽗类原型上
console.log('hello') 
} 
function Child(name,like) { Parent.call(this,name,like) // 核⼼ 第⼆次 
this.like = like;
}
Child.prototype = new Parent() // 核⼼ 第⼀次
Child.prototype.constructor = Child // 修正constructor指向
let boy1 = new Child('⼩红','apple') 
let boy2 = new Child('⼩明','orange')
// 优点1:可以向⽗类构造函数传参数 
console.log(boy1.name,boy1.like); // ⼩红,apple
// 优点2:可复⽤⽗类原型上的⽅法
console.log(boy1.say === boy2.say) // true
// 优点3:不共享⽗类的引⽤属性,如arr属性
boy1.arr.push(2)
console.log(boy1.arr,boy2.arr); // [1,2] [1] 可以看出没有共享arr属性。
// 缺点1:由于调⽤了2次⽗类的构造⽅法,会存在⼀份多余的⽗类实例属性

其实Child.prototype = new Parent() console.log(Child.prototype.proto === Parent.prototype); // true 因为Child.prototype等于Parent的实例,所以__proto__指向Parent.prototype

⽅式4、组合继承优化 ⼜称 寄⽣组合继承 --- 完美⽅式

function Parent(name) { 
this.name = name; // 实例基本属性 (该属性,强调私有,不共享) 
this.arr = [1]; // (该属性,强调私有) } 
Parent.prototype.say = function() { 
// --- 将需要复⽤、共享的⽅法定义在⽗类原型上
console.log('hello')
} 
function Child(name,like) { 
Parent.call(this,name,like) // 核⼼ this.like = like;
} 
// 核⼼ 通过创建中间对象,⼦类原型和⽗类原型,就会隔离开。不是同⼀个啦,有效避免了⽅式4的缺点。 
Child.prototype = Object.create(Parent.prototype)
// 这⾥是修复构造函数指向的代码 Child.prototype.constructor = Child 
let boy1 = new Child('⼩红','apple') 
let boy2 = new Child('⼩明','orange') 
let p1 = new Parent('⼩爸爸') 
注意:这种⽅法也要修复构造函数的 修复代码:
Child.prototype.constructor = Child