3. 继承
把 JavaScript 中,继承相关的内容复制过来。
下面整理常见的继承方式:
- 经典继承 / 借用构造函数
- 组合继承
- 原型继承
- 寄生继承
- 寄生组合继承
- 最佳实践
简单整理:
// 1 原型链继承:子类原型继承父类属性
Son.prototype = new Father();
// 2 经典继承 / 借用构造函数:在Son内不用new直接调用Father构造函数
Function Son(name) {Father.call(this, arguments);}
// 1和2优缺点是互斥的,由此引出方法3:
// 「原型链继承」解决了父类 **方法复用** 的问题,出现了父类 **属性不独立** 的问题。
// 「借用构造函数」解决父类 **属性独立** 的问题,出现了父类 **方法无法复用** 的问题。
// 3 组合继承:调用2次 new Father() => Son内部调用(继承属性), Son.prototype绑定调用(继承方法)
function Son(name,age){ Father.call(this,name) }
Son.prototype = new Father()
// 4 原型式继承:自定义objet,父类子类所有方法都共用
// ES5-
function object(obj) {
function Son() {}
Son.prototype = obj;
return new Son()
}
// ES5
Student.prototype = Object.create(Person.prototype) // Student原型被重定义,constructor需要重新指向。
// ES6
Object.setPrototypeOf(Son.prototype, Father.prototype)
// 5 寄生式继承:是原型式的升级版,有object()的基础上,利用 createSon()实现Son增添新属性和方法。
function createSon(original) {
// let Son = Object.create(original);
let son = object(original);
son.sayHi = function () {
console.log('hi')
}
return son
}
// 4 和 5 都不会生成构造函数和类的概念,而是直接构造了实例对象:
let son1 = object(father); // 原型式
let son1 = createSon(father); // 寄生式
// 6 寄生组合式,继承父类方法调用inheritePrototype绑定原型链,继承父类方法用new Father()调用构造函数。
function inheritPrototype(Sub, Super) {
let subPrototype = Object.create(Super.prototype)
subPrototype.constructor = Sub
Sub.prototype = subPrototype
}
1 原型链继承
基本思想:继承父类的属性和方法,全部依赖原型链实现。
核心代码:Son.prototype = new Father();
- 得到父类的实例化对象,
- 把子类原型对象修改为第一步得到的实例化对象。
Son的原型对象 Son.prototype
不再是原配,而变成 Father的实例化对象,所以此时 Son 原型对象的构造函数属性Son.prototype.constructor
丢失,需要重新绑定。
优点:
- 方法复用,父类的方法绑定在原型链上,可以被正确的复用。
缺点:
- 属性不独立,原型链继承,父类的属性无法实例化到对象上,而是和方法一样被复用、共用。
- 不可初始化。子类实例对象无法对继承的父类属性进行初始化,只能初始化子类的属性。
- 内容修改。子类实例对象(instance1 和 instance2)会指向同一个引用属性(如 array),这导致如果其中一个实例对象对引用属性的 内容 进行修改
instance1.color.push('pink')
,instance2 的 color 也会跟着改变。 - 显式打印。继承的父类属性,无法显式通过
console.log(instance1)
打印出来。
- 子类原型对象的构造函数属性丢失,需要重新绑定
.constructor
。
// 父类构造函数、父类属性
function Father() {
this.name = "Moxy";
this.color = ['red', 'yellow', 'black'];
}
// 父类原型、父类方法
Father.prototype.sayName = function () {
console.log(this.name);
};
// 子类构造函数、子类属性
function Son(age) {
this.age = age;
};
// 子类原型、继承父类属性
Son.prototype = new Father();
// 子类方法
Son.prototype.sonFunc = function() {
console.log("Son Function");
}
// 实例化测试:
let instance1 = new Son("12");
let instance2 = new Son("18");
console.log(instance1.__proto__.color === instance2.__proto__.color)
// true,父类属性被子类实例化对象公用
instance1.sayName(); // Moxy,方法成功继承
// 关于公用属性测试:
instance1.color.push('pink'); // 4
instance2.color; // ['red', 'yellow', 'black', 'pink'] 对引用属性的操作,不会发生赋值屏蔽
instance1.name = "ninjee" // 'ninjee' 对原型链上的属性 name 赋值屏蔽,
instance2.name; // 'Moxy' 而不是直接修改原型链的原属性值
instance1.color = "hello world"; //'hello world' 这里发生了赋值屏蔽。
instance2.color; // ['red', 'yellow', 'black', 'pink']
2 经典继承 / 借用构造函数
基本思想:在子类的构造函数内,调用父类的构造函数,从而引入父类的属性和方法。
总体评价:总体来说,经典继承和原型链继承的优缺点是相反的。经典继承解决了原型链继承的缺点,原型链继承解决了经典继承的缺点。同时,父类的所有属性(方法),全部重新创建到了子类中。
优点:
- 属性独立,原型链中引用类型值独立,不被所有实例共享;
- 可传递参数,子类型创建时也能够向父类型传递参数,用于不同实例属性值的初始化。
å缺点:
- 方法无法复用。
- 要继承的方法必须在父类的构造函数中定义,调用父类构造函数后,实例化对象内部也创建了对应的方法,没有复用函数。
- 父类定义的方法(不在构造函数内部的),对子类不可见,因为此处 没有涉及到原型链。
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
function Son(name){
Father.call(this, arguments); //继承了Father,且向父类型传递参数
}
// 实例化测试:
let instance1 = new Son("Moxy");
let instance2 = new Son("Ninjee");
instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
console.log(instance2.colors); //"red,blue,green" 引用类型值是独立的
小总结:
原型链继承和借用构造函数继承,完全到了两种不同的思路,优缺点几乎可以认为是互斥的:
原型链继承解决了父类 方法复用 的问题,出现了父类 属性不独立 的问题。
借用构造函数解决了父类 属性独立 的问题出现了父类 方法无法复用 的问题。
- 尤其注意:引用数据类型的复用问题。
核心:构造函数内部,只适用于存放未来会实例化的属性;原型链,只适用于绑定复用的方法。
3 组合继承
总体评价:原型链 + 经典继承。JavaScript 中 最常用 的继承模式。
基本思想:
利用构造函数:子类借用构造函数,来实现对父类属性的继承。
Father.call(this,name);
利用原型链:子类原型指向实例化的父类,来实现对父类方法的继承。
Son.prototype = new Father();
优点
避免了原型链和借用构造函数的各自的缺陷,融合了它们的优点。
原型链优点:父类的方法得到了复用;
构造函数优点:父类属性都可以重定义在实例化对象中。
此外,
instanceof
和isPrototypeOf( )
可正确使用,表明实例化对象的类型。
缺点:
额外的开销。在定义子类继承父类方法的时候,直接调用 Son.prototype = new Father()
这造成了两个后果:
- 额外调用了 1 次
Father()
,造成了额外的开销。如果有实例化 n 个对象,一共调用了 1 + n 次。 new Father()
附带了父类构造函数的属性,这些属性也绑定在了Son.prototype
子类原型上,多了额外无用的变量。- Son 的新原型对象(Father 实例对象),需要重新指向
.constructor
。
// 父类构造函数、父类属性
function Father(name){
this.name = name;
this.colors = ["red","blue","green"];
}
// 父类原型、父类方法
Father.prototype.sayName = function(){
console.log(this.name);
};
// 子类构造函数、子类属性、继承父类属性
function Son(name,age){
Father.call(this,name); //继承父类属性。实例化1次,就调用1次Father()
this.age = age;
}
// 子类原型、继承父类方法
Son.prototype = new Father(); //只在定义子类时,调用1次Father()
Son.prototype.sayAge = function(){
console.log(this.age);
}
// 实例化测试:
let instance1 = new Son("Moxy",16);
let instance2 = new Son("Ninjee",26);
instance1.colors.push("black");
instance1 // Son {name: "Moxy", colors: Array(4), age: 16}
instance2 // Son {name: "Ninjee", colors: Array(3), age: 26}
4 原型式继承
场景:当你有一个对象,想在它的基础上再创建一个新对象,然后再对新对象进行适当的修改,使用此方式继承。
基本思想:利用一个自定义的 object()
方法,将旧对象(父对象)绑定到实例化对象(子对象)中。这里没有了父类和子类的概念,而是只有父对象和子对象。
形式一:自定义 object()
;
- 创建一个空的子类构造函数
Son
; - 把要定义的父类对象和方法,绑定在子类原型的原型链上。
- 调用子类构造函数,实例化对象。
- 返回实例化的对象。
优点:
- 弱化了子类和父类的概念,只是把"子类"的构造函数当作一个壳子,用于把原有对象绑定到新创建的对象上,相当于:
son1.__proto__
指向father
。 - 即使不自定义类型,也可以通过原型实现对象之间的属性、方法共享,就像使用原型模式一样。
- 换句话说,如果你有一个对象,想在它的基础上再创建一个新对象。则利用
object()
便可以实现。
缺点:
- 属性和方法全部共用。这其实相当于把原有对象(代码中的
father
)的全部属性和方法,全绑定到实例化对象的 原型链 上。这导致所有实例化对象(son1
,son2
)都是一模一样的,而且属性和方法全部共用。 - 父对象的方法、属性原封不动的照搬。无法在实例化子对象的过程中,创建新的属性和方法(
object
中,子对象的构造函数是空的)。
// 形式一:自定义object()
function object(obj) {
function Son() {}
Son.prototype = obj;
return new Son();
}
// or ES6
function object2(obj) {
const son = {};
Object.setPrototypeOf(son, obj);
return son;
}
// or ES5
// Object.create(obj) 就是规范化了的 object 函数,直接使用
// 定义父对象(无构造函数、无原型链)
const father = {
name: "Moxy",
colors: ["red", "blue", "green"],
sayName: function () {
console.log(this.name)
}
};
// 实例化测试:
const son1 = object(father);
const son2 = object(father);
son1.colors.push("black");
son2.colors.push("pink");
console.log(father.colors); //(5) ["red", "blue", "green", "black", "pink"]
形式二:ES5 的方法 object.create()
在 ECMAScript5 中,通过新增 object.create()
方法规范化了上面的原型式继承,在实现原理上是一摸一样的。
object.create()
会返回一个绑定好原型链的新对象,可以接收两个参数:
一个对象:新对象原型链指向的对象;
一个对象(可选):为新对象定义额外属性。
- 第二个参数,与
Object.defineProperties()
方法的第二个参数格式相同:- 每个属性都是通过自己的描述符定义的;
- 以这种方式指定的 任何属性 都会覆盖原型对象上的同名属性。
优点:
- 规范了形式一(引子),这里额外增加了类与类之间的关系。
- 实例属性和共用方法都可以正确的被定义:父类的方法可以被子类复用,父类的实例属性子类可以单独创建出来。
缺点:调用 Student.prototype = Object.create(Person.prototype)
:
Object.create()
会创建一个新对象,原有的 Student 原型对象被替换掉了。这导致 3 个问题:.constructor
丢失 ,子类新的原型对象的.constructor
属性需要重新指向其构造函数。- 子类方法顺序 ,需要先替换子类的原型对象
Student.prototype
,后在新原型对象上定义子类的公有方法。 - 性能损失。子类的旧原型对象没有实际用处,出生就被弃用,造成额外的内存和任务开销。
// 定义父类:实例属性 + 公有方法
function Father(name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
Father.prototype.printName = function () {
console.log(this.name)
}
// 定义子类:实例属性
function Son(name, age) {
Father.call(this, name)
this.age = age
};
// 方法二:原型式继承。子类重新创建一个原型对象,然后恢复constructor的正确指向。
Son.prototype = Object.create(Father.prototype);
Son.prototype.constructor = Son;
// 定义子类:公有方法,只有在更换了原型对象后,子类才能定义公有方法
Son.prototype.printAge = function () {
console.log(this.age)
}
// 实例化测试:
let instance1 = new Son("Moxy", 99); // Son {name: "Moxy", colors: Array(3), age: 99}
let instance2 = new Son("Ninjee", 5); // Son {name: "Ninjee", colors: Array(3), age: 5}
instance1.printName === instance2.printName // true
Son.prototype.__proto__ === Father.prototype // true
形式三:ES6 方法:Object.setPrototypeOf(Son, Father)
🌟最佳实践:该方法是 ES6的最佳实践,也是寄生组合式继承。
正因为 ES5 Object.create()
会有重新定义子类原型对象的问题。所以 ES6 的新方法 Object.setPrototypeOf(Son.prototype, Father.prototype)
应运而生。
它的实际作用就是:Son.prototype.__proto__ == Father.prototype
- 为什么不直接使用
__proto__
属性呢?因为直接使用__proto__
不安全,它不是 JS 官方标准,而是主流浏览器为了方便去定义的一个属性。这个属性并不能完全的兼容所有浏览器等其他环境。
优点:解决上形式二的 3 个缺点。
- 没有
Object.Object()
的性能损失。因为不需要抛弃子类的旧原型对象,而不会导致垃圾回收; - 没有
Object.Object()
的混乱代码顺序和.constroctor
的重定向问题。
缺点:
- 真看不出有什么缺点。可能最重要的缺点就是,该方法是 ES6 方法,需要 JS 支持 ES6 环境。
// 定义父类:实例属性 + 公有方法
function Father(name) {
this.name = name
this.colors = ["red", "blue", "green"]
}
Father.prototype.printName = function () {
console.log(this.name)
}
// 定义子类:实例属性 + 公有方法。现在公有方法可以紧接着实例属性去定义了
function Son(name, age) {
Father.call(this, name)
this.age = age
};
Son.prototype.printAge = function () {
console.log(this.age)
}
// 方法三:原型式继承。使用ES6方法,
Object.setPrototypeOf(Son.prototype, Father.prototype)
// 实例化测试:
let instance1 = new Son("Moxy", 99); // Son {name: "Moxy", colors: Array(3), age: 99}
let instance2 = new Son("Ninjee", 5); // Son {name: "Ninjee", colors: Array(3), age: 5}
instance1.printName === instance2.printName // true
Son.prototype.__proto__ === Father.prototype // true
5 寄生式继承
基本思想:
- “寄”:利用原型式,在有一个对象的基础上,创建出子对象。让子对象通过原型链继承父对象的全部属性和方法。
- “生”:增强子对象,为子对象创建自己的属性和方法。
优点:
- 原型式继承的全部优点:不定义类型,便可以通过原型实现对象之间的属性、方法共享。
- 避免了原型式继承的第二个缺点。寄生式继承可以在实例化子对象的过程中,创建新的属性和方法。
缺点:
- 父类的属性和方法全部复用。
- 子类的属性无法在创建时初始化
// 形式二:自定义object()
function object(obj) {
function Son() {}
Son.prototype = obj;
return new Son();
}
// 定义父对象(无构造函数、无原型链)
const father = {
name: "Moxy",
colors: ["red", "blue", "green"],
sayName: function () {
console.log(this.name)
}
};
// 【工厂函数】增强子对象、为子对象添加自己的方法
function createSon(original, age) {
// 形式一:ES5的Object.create()
// const Son = Object.create(original);
const son = object(original);
// 【寄生】增强对象,添加属于自己的属性+方法
son.age = age;
son.sayHi = function () {
console.log('hi')
};
return son
}
/// 实例化测试
const son1 = createSon(father, 12);
const son2 = createSon(father, 18);
son1 // Son {age: 12, sayHi: ƒ}
son2 // Son {age: 18, sayHi: ƒ}
son1.colors.push("black");
son2.colors.push("pink");
console.log(father.colors); // (5) ["red", "blue", "green", "black", "pink"]
son1 // Son {sayHi: ƒ}
小总结
总结1:原型式继承、寄生式继承和浅拷贝对象:
问题:上文说过,原型式继承的优点是即使不自定义类型,也可以通过原型实现对象之间的属性、方法共享。那么问题来了,新对象直接浅拷贝原对象,然后在给自己添加额外的方法,不也实现了属性和方法的共享吗?
解答:浅拷贝不是 “动态的”。新对象通过原型式继承,获得了旧对象的全部属性和方法,如果此后旧对象的属性和方法发生变动,因为原型链的关系,新对象继承的属性和方法也会 “动态” 的发生变动。而单纯的浅拷贝是静态的,旧对象的基本数据属性发生变动,新对象不会更新。(旧对象的引用数据源类型和方法都是地址值传递,新对象也会得到更新)
总结 2:原型式继承和寄生式继承的区别:
原型式继承:基于已有的对象(原型对象)创建新对象
- 原型式继承实现了
Object.create()
的第一个参数;
寄生式继承:创建一个用于封装继承过程的函数。函数内部不仅实现了 ”基于已有的对象(原型对象)创建新对象“ 的原型式继承,也实现了对新对象的增强,添加更多属性和方法
- 寄生式继承实现了
Object.create()
的第一个和第二个参数;
6 寄生组合式继承
寄生组合式的本质原理就是原型式继承,只是把原型式继承的多行代码,通过 inheritPrototype
📦 封装起来,更简洁了。
// 【📦】中间函数、传入子类、父类两个构造函数
function inheritPrototype(Sub, Super) {
//重定义一个[子类原型对象]subPrototype,此时有__proto__和constructor两个属性,即:
//Sub.prototype.__proto__ 指向 Super.prototype 父类原型对象
//Sub.prototype.constructor 指向 Sub 子类构造函数
// 这里使用了现成的Object.create,最原始应该是自定义的3行object函数
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
}
// 父类构造函数:属性+方法
function Father(name) {
this.name = name;
this.color = ["red", "green", "blue"];
}
Father.prototype.sayName = function () {
console.log(this.name);
}
// 子类构造函数:继承父类属性+子类属性
function Son(name, age) {
Father.call(this, name); // 只调用一次父类构造方法
this.age = age;
}
// 子类继承父类方法
inheritPrototype(Son, Father);
// or 方法三:原型式继承。使用ES6方法,这个方法没有顺序规定
// Object.setPrototypeOf(Son.prototype, Father.prototype)
// 子类方法,在Son.prototype被正确定义后才能添加
Son.prototype.sayAge = function () {
console.log(this.age);
}
// 实例化测试:
let instance1 = new Son("Moxy", 18)
let instance2 = new Son("Ninjee", 28)
instance1.color.push("pink")
console.log(instance1.color) // (4) ["red", "green", "blue", "pink"]
console.log(instance2.color) // (3) ["red", "green", "blue"]
instance1 // Son {name: "Moxy", color: Array(4), age: 18}
console.log(instance1.__proto__) // Father {constructor: ƒ, sayAge: ƒ}
console.log(instance1.__proto__.__proto__) // {sayName: ƒ, constructor: ƒ}
组合式继承原本需要调用两次父类的构造方法:
- 在子类的构造函数中调用,目的是把 父类的属性 复制到子类的实例化对象中;
- 在设置子类继承父类时调用,目的是继承 父类的方法 ,由此产生的代价就是在子类的原型链上,多了一组 父类的属性。
而寄生组合式,只调用一次父类构造方法,为了把 父类的属性 复制到实例化对象中。子类继承父类的方法,通过寄生方式进行。即父类方法直接设置在子类实例化对象的 prototype 原型链上。
- 父类方法的继承:通过原型链在实例化的时候绑定。
- 父类属性的继承:通过构造函数在实例化的时候重新创建。
Object.create()
Object.create()
方法的实质是:新建一个空的构造函数 F
,然后让 F.prototype
属性指向参数对象o
,最后返回一个 F
的实例,从而实现让该实例继承 obj
的属性。
实际上,Object.create()
方法可以用下面的代码代替。
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
// 因为构造函数F中没有任何代码, new F() 返回了一个空对象,所以这也相当于:
Object.create = function (o) {
let newObj = {};
nreObj.__proto__ = o;
return newObj;
}
在寄生式继承中,Object.create()
实际上做了如下操作:
let obj = Object.create(superType)
// 替换为:
let obj = {}
obj.__proto__ = superType
在寄生组合式继承中,Object.create()
实际上做了如下操作:
let subPrototype = Object.create(Super.prototype)
// 替换为:
let subPrototype = {}
subPrototype.__proto__ = Super.prototype
7 继承的最佳实践:
- ES5 版本最佳实践:寄生组合式继承,使用
Object.create()
; - ES6 版本最佳实践:原型式继承的形式三:
Object.setPrototypeOf(Son.prototype, Father.prototype)
- ES5 以下最佳实践:寄生组合式继承,使用 object 自定义函数;
(3)ES5 最原始版本寄生组合式继承:
// 📦中间函数
function inheritPrototype(Sub, Super) {
Sub.prototype = object(Super.prototype);
Sub.prototype.constructor = Sub;
function object(o) {
function F() {}
F.prototype = o;
return new F();
};
}
// 父类
function Father(name) {
this.name = name;
this.color = ["red", "green", "blue"];
}
Father.prototype.sayName = function () {
console.log(this.name);
}
// 子类
function Son(name, age) {
Father.call(this, name);
this.age = age;
}
inheritPrototype(Son, Father);
Son.prototype.sayAge = function () {
console.log(this.age);
}
// 实例化测试:
let instance1 = new Son("Moxy", 18)
let instance2 = new Son("Ninjee", 28)
instance1.color.push("pink")
console.log(instance1.color) // (4) ["red", "green", "blue", "pink"]
console.log(instance2.color) // (3) ["red", "green", "blue"]
instance1 // Son {name: "Moxy", color: Array(4), age: 18}
console.log(instance1.__proto__) // Father {constructor: ƒ, sayAge: ƒ}
console.log(instance1.__proto__.__proto__) // {sayName: ƒ, constructor: ƒ}
(2)ES5 的中间函数,使用 API:
// 📦中间函数
function inheritPrototype(Sub, Super) {
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
}
(1)ES6 的继承:
// 父类
function Father(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
Father.prototype.printName = function () {
console.log(this.name);
}
// 子类:
function Son(name, age) {
Father.call(this, name);
this.age = age;
};
Son.prototype.printAge = function () {
console.log(this.age);
}
Object.setPrototypeOf(Son.prototype, Father.prototype);
// 实例化测试:
let instance1 = new Son("Moxy", 99); // Son {name: "Moxy", colors: Array(3), age: 99}
let instance2 = new Son("Ninjee", 5); // Son {name: "Ninjee", colors: Array(3), age: 5}
instance1.printName === instance2.printName // true
Son.prototype.__proto__ === Father.prototype // true
引用: