JavaScript中的六種繼承

lenlch發表於2018-11-29

在js中,我們可以使用繼承來實現子類擁有父類的屬性和方法

原型鏈繼承

通過子類的原型物件指向父類的例項,父例項內部又有一個[[prototype]]指向父類的原型物件(書上說子類原型直接指向父類原型了,我覺著我這樣更容易理解一點)。這樣,子類例項通過找自身,沒有,去子類原型去找,沒有,去父例項上找,沒有,去父原型上找。這樣就形成了一個原型連。

function Foo(name) {
    this.name = name
}

function Son() {}

// 子類原型擁有了一個指向父類例項的指標
Son.prototype = new Foo('len')

let son = new Son()

console.log(son.name)  // len 

這個name是在父類例項上的,我們再看
----------------------------

function Foo() {
}

function Son() {}
Foo.prototype.name = 'len'
Son.prototype = new Foo()

let son = new Son()

console.log(son.name)  // len, 這裡就是父類原型上的name屬性了。


複製程式碼

這就是一個最簡單的原型鏈繼承。子類Son擁有了父類Fooname屬性。

缺點: 所有的屬性和方法都是共享的,且子類不能向父類傳遞引數

借用建構函式

通過在子類的建構函式裡面來呼叫父類的建構函式

function Foo() {
    this.name = 'len'
}
function Son() {
    Foo.call(this)
}

let son = new Son()
console.log(son.name) // len

-------給父類傳參--------
function Foo(name) {
    this.name = name
}
function Son() {
    Foo.apply(this, arguments)
}

let son1 = new Son('len')
let son2 = new Son('lance')
console.log(son1.name) // len
console.log(son2.name) // lance

這樣即做到了引數私有化,而且還能給父類傳參
複製程式碼

缺點: 所有的屬性和方法都是寫在建構函式裡面的,這樣做不到複用

借用建構函式和原型鏈(俗稱: 組合繼承,也是目前最常用的繼承方式)

也就是將構造和原型鏈結合起來

function Foo(name) {
    this.name = name
}

function Son() {
    Foo.apply(this, arguments)
}

Son.prototype = new Foo()

Foo.prototype.sayName = function() {
    console.log('hello: ' + this.name)
}

let s1 = new Son('len')
let s2 = new Son('lance')

s1.sayName() // hello: len
s2.sayName() // hello: lance

這樣就做到了sayName的複用,如果想私有化,只需要將方法放在建構函式內即可
複製程式碼

這是目前最完美的解決方案了

原型式繼承

function clone(o) {
    function F() {}
    F.prototype = o
    return new F()
}

let person = {
    name: 'len',
    friends: ['a', 'b']
}

let p1 = clone(person) // len

let p2 = clone(person) // ['a', 'b']

console.log(p1.name) // len
console.log(p1.friends) // ['a', 'b']

console.log(p2.name)
console.log(p2.friends)

p1.friends.push('c')

console.log(p1.friends) // ['a', 'b', 'c']
console.log(p1.friends) // ['a', 'b', 'c']
複製程式碼

這種模式的前提是你必須有一個物件可以作為另一個物件的基礎。所暴露出來的問題和原型鏈繼承一樣的,所有的屬性和方法都是共享的。所以,在沒有必要興師動眾地建立建構函式,而只想讓一個物件與另一個物件保持類似的情況下,原型鏈繼承是完全可以勝任的。

上面的clone函式,在es5中,完全可以用Object.create()來替代。

寄生式繼承

思路和原型式繼承是緊密相關的

function create(o) {
    // 這裡也可以用上面的clone函式
    var obj = Object.create(o)
    obj.sayHi = function() {
        console.log('hi')
    }
    return obj
} 

let person = {
    name: 'len',
    friends: ['a', 'b']
}

let p1 = create(person)
p1.sayHi() // hi
複製程式碼

寄生組合式繼承

前面說過,組合繼承是JavaScript最常用的繼承模式;不過,它也有自己的不足。組合繼承最大的問題就是無論什麼情況下,都會呼叫兩次父類建構函式。第一次是在new Foo(),第二次是在子類的建構函式中Foo.call(this)

所謂寄生組合式繼承,即通過借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法。原理就是:不必為了指定子型別的原型而呼叫符型別的建構函式,我們所需要做的無非就是父型別原型的一個副本而已。

function inheritProperty(son, foo) {
    // 父類原型副本
    var prototype = Object.create(foo.prototype)
    // 父類原型副本指向子類的建構函式
    prototype.constructor = son
    // 子類建構函式的prototype指向了父類(副本)的原型
    son.prototype = prototype
}

function Foo(name) {
    this.name = name
    this.friends = ['a', 'b', 'c']
}

Foo.prototype.sayName = function() {
    console.log(this.name)
}

function Son(name, age) {
    Foo.call(this, name)
    this.age = age
}

// 之前這裡是 Son.prototype = new Foo(), 你可以自行比較一下
inheritProperty(Son, Foo)

Son.prototype.sayAge = function() {
    console.log(this.age)
}

let s = new Son('len', 23)

console.log(s.friends) // ['a', 'b', 'c']
console.log(s.name) // len
s.sayName() // len
console.log(s.age) // 23
s.sayAge() // 23
複製程式碼

說白了就是創鍵一個父類原型副本,將這個副本的constructor指向子類的建構函式,子類建構函式的prototype指向了父類原型副本。你可以這麼理解,就是相當於foo === son

(每一個原型都有一個constructor屬性,每一個建構函式都有一個prototype屬性)

相關文章