js的繼承
面向物件的繼承方式有很多種,原型鏈繼承、借用建構函式繼承、組合繼承、原型式繼承、寄生式繼承、寄生式組合繼承、深拷貝繼承等等。

繼承比較
一、原型式繼承
function createObjWithObj(obj){ // * 傳入一個原型物件 function Temp(){} Temp.prototype = obj let o = new Temp() return o } // * 把Person的原型物件當做temp的原型物件 let temp = createObjWithObj(Person.prototype) // * 也可以使用Object.create實現 // * 把Person的原型物件當做temp2的原型物件 let temp2 = Object.create(Person.prototype)
本質上 createObjWithObj() 對傳入其中的物件執行了一次淺複製,將建構函式 F 的原型直接指向傳入的物件。
缺點
1.原型中引用型別值會被修改
2.無法傳遞引數
二、寄生式繼承
// 寄生式繼承
// 我們在原型式的基礎上,希望給這個物件新增一些屬性方法
// 那麼我們在原型式的基礎上擴充套件
function createNewObjWithObj(obj) { let o = createObjWithObj(obj) o.name = "邵威儒" o.age = 28 return o }
函式的主要作用是為建構函式新增屬性和方法,以增強函式。
缺點(同原型式繼承):
1.原型中引用型別值會被修改
2.無法傳遞引數
三、藉助建構函式繼承
通過這樣的方式,會有一個問題,原型物件類似一個共享庫,所有例項共享原型物件同一個屬性方法,如果原型物件上有引用型別,那麼會被所有例項共享,也就是某個例項更改了,則會影響其他例項,我們可以看一下
function Person(name,pets){ // * 父建構函式接收name,pets引數 this.name = name // * 賦值到this上 this.pets = pets // * 賦值到this上 } Person.prototype.eat = function(){ console.log('吃飯') } function Student(num,name,pets){ // * 在子建構函式中也接收引數 Person.call(this,name,pets) // * 在這裡把name和pets傳引數 this.num = num // * 賦值到this上 } let student = new Student("030578000","邵威儒",["旺財","小黃"])
四、原型鏈繼承
利用原型鏈的特性,當在自身找不到時,會沿著原型鏈往上找。
function Person(){ this.name = '邵威儒' this.pets = ['旺財','小黃'] } Person.prototype.eat = function(){ console.log('吃飯') } function Student(){ this.num = "030578000" } // * new一個Person的例項,同時擁有其例項屬性方法和原型屬性方法 let p = new Person() // * 把Student的原型物件指向例項p Student.prototype = p // * 把Student的原型物件的constructor指向Student,解決型別判斷問題 Student.prototype.constructor = Student let student = new Student() console.log(student.num) // '030578000' console.log(student.name) // * '邵威儒' console.log(student.pets) // * '[ '旺財', '小黃' ]' student.eat() // '吃飯'
此時關係圖為

image
五、組合繼承
利用構造繼承和原型鏈組合
function Person(name,pets){ // * 父建構函式接收name,pets引數 this.name = name // * 賦值到this上 this.pets = pets // * 賦值到this上 } Person.prototype.eat = function(){ console.log('吃飯') } function Student(num,name,pets){ // * 在子建構函式中也接收引數 Person.call(this,name,pets) // * 在這裡把name和pets傳引數 this.num = num // * 賦值到this上 } let p = new Person() Student.prototype = p Student.prototype.constructor = Student let student = new Student("030578000","邵威儒",["旺財","小黃"]) let student2 = new Student("030578001","iamswr",["小紅"]) console.log(student.num) // '030578000' console.log(student.name) // '邵威儒' console.log(student.pets) // '[ '旺財', '小黃' ]' student.eat() // '吃飯' student.pets.push('小紅') console.log(student.pets) // * [ '旺財', '小黃', '小紅' ] console.log(student2.pets) // * [ '小紅' ]

image
這樣我們就可以在子建構函式中給父建構函式傳參了,而且我們也發現上圖中,2個紅圈的地方,程式碼是重複了,那麼接下來我們怎麼解決呢?
能否在子建構函式設定原型物件的時候,只要父建構函式的原型物件屬性方法呢?
當然是可以的,接下來我們講寄生式組合繼承,也是目前程式猿認為解決繼承問題最好的方案
六、 寄生式組合繼承
function Person(name,pets){ this.name = name this.pets = pets } Person.prototype.eat = function(){ console.log('吃飯') } function Student(num,name,pets){ Person.call(this,name,pets) this.num = num } // * 寄生式繼承 function Temp(){} // * 宣告一個空的建構函式,用於橋樑作用 Temp.prototype = Person.prototype // * 把Temp建構函式的原型物件指向Person的原型物件 let temp = new Temp() // * 用建構函式Temp例項化一個例項temp Student.prototype = temp // * 把子建構函式的原型物件指向temp temp.constructor = Student // * 把temp的constructor指向Student或者改成Student.prototype.constructor =Student let student1 = new Student('030578001','邵威儒',['旺財','小黃']) console.log(student1) // Student { name: '邵威儒', pets: [ '旺財', '小黃' ],num: '030578001' } let student2 = new Student('030578002','iamswr',['小紅']) console.log(student2) // Student { name: 'iamswr', pets: [ '小紅' ],num: '030578002' }
至此為止,我們就完成了寄生式組合繼承了,主要邏輯就是用一個空的建構函式,來當做橋樑,並且把其原型物件指向父建構函式的原型物件,並且例項化一個temp,temp會沿著這個原型鏈,去找到父建構函式的原型物件

image
七、Class繼承
Class 可以通過 extends 關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。
class staff { constructor(){ this.company = "ABC"; this.test = [1,2,3]; } companyName(){ return this.company; } } class employee extends staff { constructor(name,profession){ super(); this.employeeName = name; this.profession = profession; } } // 將父類原型指向子類 let instanceOne = new employee("Andy", "A"); let instanceTwo = new employee("Rose", "B"); instanceOne.test.push(4); // 測試 console.log(instanceTwo.test);// [1,2,3] console.log(instanceOne.companyName()); // ABC // 通過 Object.getPrototypeOf() 方法可以用來從子類上獲取父類 console.log(Object.getPrototypeOf(employee) === staff) // 通過 hasOwnProperty() 方法來確定自身屬性與其原型屬性 console.log(instanceOne.hasOwnProperty('test'))// true // 通過 isPrototypeOf() 方法來確定原型和例項的關係 console.log(staff.prototype.isPrototypeOf(instanceOne));// true
super 關鍵字,它在這裡表示父類的建構函式,用來新建父類的 this 物件。
- 子類必須在 constructor 方法中呼叫 super 方法,否則新建例項時會報錯。這是因為子類沒有自己的this 物件,而是繼承父類的 this 物件,然後對其進行加工。
- 只有呼叫 super 之後,才可以使用 this 關鍵字,否則會報錯。這是因為子類例項的構建,是基於對父類例項加工,只有 super 方法才能返回父類例項。
super
雖然代表了父類 A
的建構函式,但是返回的是子類 B
的例項,即 super
內部的 this
指的是 B
,因此 super()
在這裡相當於A.prototype.constructor.call(this)
ES5 和 ES6 實現繼承的區別
- ES5 的繼承,實質是先創造子類的例項物件 this,然後再將父類的方法新增到 this 上面(Parent.apply(this))。
- ES6 的繼承機制完全不同,實質是先創造父類的例項物件 this (所以必須先呼叫 super() 方法),然後再用子類的建構函式修改 this。
八、 深拷貝繼承
// 方法一:利用JSON.stringify和JSON.parse
// 這種方式進行深拷貝,只針對json資料這樣的鍵值對有效
// 對於函式等等反而無效,不好用,接著繼續看方法二、三。
// 方法二:
function deepCopy(fromObj,toObj) { // 深拷貝函式 // 容錯 if(fromObj === null) return null // 當fromObj為null if(fromObj instanceof RegExp) return new RegExp(fromObj) // 當fromObj為正則 if(fromObj instanceof Date) return new Date(fromObj) // 當fromObj為Date toObj = toObj || {} for(let key in fromObj){ // 遍歷 if(typeof fromObj[key] !== 'object'){ // 是否為物件 toObj[key] = fromObj[key] // 如果為原始資料型別,則直接賦值 }else{ toObj[key] = new fromObj[key].constructor // 如果為object,則new這個object指向的建構函式 deepCopy(fromObj[key],toObj[key]) // 遞迴 } } return toObj } let dog = { name:"小白", sex:"公", firends:[ { name:"小黃", sex:"母" } ] } let dogcopy = deepCopy(dog) // 此時我們把dog的屬性進行修改 dog.firends[0].sex = '公' console.log(dog) // { name: '小白', sex: '公', firends: [ { name: '小黃', sex: '公' }] } // 當我們列印dogcopy,會發現dogcopy不會受dog的影響 console.log(dogcopy) // { name: '小白',sex: '公',firends: [ { name: '小黃', sex: '母' } ] }