在之前的總結中,我們詳細分析了原型《JS 總結之原型》,原型很大作用用於模擬繼承,這一次,我們來聊原型繼承的幾種方式。
function Person (age) {
this.age = age || 18
}
Person.prototype.sleep = function () {
console.log(`sleeping`)
}
? 方式 1:原型鏈繼承(不推薦)
function Programmer() {}
Programmer.prototype = new Person ()
Programmer.prototype.code = function () {
console.log(`coding`)
}
let jon = new Programmer()
jon.code() // coding
jon.sleep() // sleeping
jon instanceof Person // true
jon instanceof Programmer // true
Object.getPrototypeOf(jon) // Person {age: 18, code: ƒ}
jon.__proto__ // Person {age: 18, code: ƒ}
缺點:
- 無法向父類建構函式傳參
- 父類的所有屬性被共享,只要一個例項修改了屬性,其他所有的子類例項都會被影響
? 方式 2:借用建構函式(經典繼承)(不推薦)
複製父類建構函式內的屬性
function Programmer(name) {
Person.call(this)
this.name = name
}
let jon = new Programmer(`jon`)
jon.name // jon
jon.age // 18
jon.sleep() // Uncaught TypeError: jon.sleep is not a function
jon instanceof Person // false
jon instanceof Programmer // true
優點:
- 可以為父類傳參
- 避免了共享屬性
缺點:
- 只是子類的例項,不是父類的例項
- 方法都在建構函式中定義,每次建立例項都會建立一遍方法
? 方式 3:組合繼承(推薦)
組合 原型鏈繼承 和 借用建構函式繼承。
function Programmer(age, name) {
Person.call(this, age)
this.name = name
}
Programmer.prototype = new Person()
Programmer.prototype.constructor = Programmer // 修復建構函式指向
let jon = new Programmer(18, `jon`)
jon.age // 18
jon.name // jon
let flash = new Programmer(22, `flash`)
flash.age // 22
flash.name // flash
jon.age // 18
jon instanceof Person // true
jon instanceof Programmer // true
flash instanceof Person // true
flash instanceof Programmer // true
優點:融合原型鏈繼承和建構函式的優點,是 JavaScript 中最常用的繼承模式
缺點:呼叫了兩次父類建構函式
? 方式 4:原型式繼承(不推薦)
function create(o) {
function F() {}
F.prototype = o
return new F()
}
let obj = {
gift: [`a`, `b`]
}
let jon = create(obj)
let xiaoming = create(obj)
jon.gift.push(`c`)
xiaoming.gift // [`a`, `b`, `c`]
缺點:共享了屬性和方法
? 方式 5:寄生式繼承(不推薦)
建立一個僅用於封裝繼承過程的函式,該函式在內部以某種形式來做增強物件,最後返回物件
function createObj (o) {
var clone = Object.create(o)
clone.sayName = function () {
console.log(`hi`)
}
return clone
}
缺點:跟借用建構函式模式一樣,每次建立物件都會建立一遍方法
? 方式 6:寄生組合繼承(最佳)
子類建構函式複製父類的自身屬性和方法,子類原型只接受父類的原型屬性和方法:
function create(prototype) {
function Super() {}
Super.prototype = prototype
return new Super()
}
function Programmer(age, name) {
Person.call(this, age)
this.name = name
}
Programmer.prototype = create(Person.prototype)
Programmer.prototype.constructor = Programmer // 修復建構函式指向
let jon = new Programmer(18, `jon`)
jon.name // jon
進階封裝:
function create(prototype) {
function Super() {}
Super.prototype = prototype
return new Super()
}
function prototype(child, parent) {
let prototype = create(parent.prototype)
prototype.constructor = child // 修復建構函式指向
child.prototype = prototype
}
function Person (age) {
this.age = age || 18
}
Person.prototype.sleep = function () {
console.log(`sleeping`)
}
function Programmer(age, name) {
Person.call(this, age)
this.name = name
}
prototype(Programmer, Person)
let jon = new Programmer(18, `jon`)
jon.name // jon
引用《JavaScript 高階程式設計》中對寄生組合式繼承的誇讚就是:
這種方式的高效率體現它只呼叫了一次 Parent 建構函式,並且因此避免了在 Parent.prototype 上面建立不必要的、多餘的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用 instanceof 和 isPrototypeOf。開發人員普遍認為寄生組合式繼承是引用型別最理想的繼承正規化。
? 方式 7:ES6 extends(最佳)
// 父類
class Person {
constructor(age) {
this.age = age
}
sleep () {
console.log(`sleeping`)
}
}
// 子類
class Programmer extends Person {
constructor(age, name) {
super(age)
this.name = name
}
code () {
console.log(`coding`)
}
}
let jon = new Programmer(18, `jon`)
jon.name // jon
jon.age // 18
let flash = new Programmer(22, `flash`)
flash.age // 22
flash.name // flash
jon instanceof Person // true
jon instanceof Programmer // true
flash instanceof Person // true
flash instanceof Programmer // true
優點:不用手動設定原型。
缺點:新語法,只要部分瀏覽器支援,需要轉為 ES5 程式碼。
? 參考
- 《JavaScript 高階程式設計(第 3 版)》6.3 繼承
- 《JavaScript 深入之繼承的多種方式和優缺點》 by 冴羽
- 《ECMAScript 6 入門》Class 的繼承 by 阮一峰