摘要:繼承簡介在的中的面向對象編程,繼承是給構造函數之間建立關系非常重要的方式,根據原型鏈的特點,其實繼承就是更改原本默認的原型鏈,形成新的原型鏈的過程。
JavaScript 原本不是純粹的 “OOP” 語言,因為在 ES5 規范中沒有類的概念,在 ES6 中才正式加入了 class 的編程方式,在 ES6 之前,也都是使用面向對象的編程方式,當然是 JavaScript 獨有的面向對象編程,而且這種編程方式是建立在 JavaScript 獨特的原型鏈的基礎之上的,我們本篇就將對原型鏈以及面向對象編程最常用到的繼承進行刨析。
在 JavaScript 的中的面向對象編程,繼承是給構造函數之間建立關系非常重要的方式,根據 JavaScript 原型鏈的特點,其實繼承就是更改原本默認的原型鏈,形成新的原型鏈的過程。
復制的方式進行繼承指定是對象與對象間的淺復制和深復制,這種方式到底算不算繼承的一種備受爭議,我們也把它放在我們的內容中,當作一個 “不正經” 的繼承。
1、淺復制創建一個淺復制的函數,第一個參數為復制的源對象,第二個參數為目標對象。
// 淺復制方法 function extend(p, c = {}) { for (let k in p) { c[k] = p[k]; } return c; } // 源對象 let parent = { a: 1, b: function() { console.log(1); } }; // 目標對象 let child = { c: 2 }; // 執行 extend(parent, child); console.log(child); // { c: 2, a: 1, b: ? }
上面的 extend 方法在 ES6 標準中可以直接使用 Object.assign 方法所替代。
2、深復制可以組合使用 JSON.stringify 和 JSON.parse 來實現,但是有局限性,不能處理函數和正則類型,所以我們自己實現一個方法,參數與淺復制相同。
// 深復制方法 function extendDeeply(p, c = {}) { for (let k in p) { if (typeof p[k] === "object" && typeof p[k] !== null) { c[k] = p[k] instanceof Array ? [] : {}; extendDeeply(p[k], c[k]); } else { c[k] = p[k]; } } return c; } // 源對象 let parent = { a: { b: 1 }, b: [1, 2, 3], c: 1, d: function() { console.log(1); } }; // 執行 let child = extendDeeply(parent); console.log(child); // { a: {b: 1}, b: [1, 2, 3], c: 1, d: ? } console.log(child.a === parent.a); // false console.log(child.b === parent.b); // false console.log(child.d === parent.d); // true
在上面可以看出復制后的新對象 child 的 a 屬性和 b 的引用是獨立的,與 parent 的 a 和 b 毫無關系,實現了深復制,但是 extendDeeply 函數并沒有對函數類型做處理,因為函數內部執行相同的邏輯指向不同引用是浪費內存的。
原型替換是繼承當中最簡單也是最直接的方式,即直接讓父類和子類共用同一個原型對象,一般有兩種實現方式。
// 原型替換 // 父類 function Parent() {} // 子類 function Child() {} // 簡單粗暴的寫法 Child.prototype = Parent.prototype; // 另一種種實現方式 Object.setPrototypeOf(Child.prototype, Parent.prototype);
上面這種方式 Child 的原型被替換掉,Child 的實例可以直接調用 Parent 原型上的方法,實現了對父類原型方法的繼承。
上面第二種方式使用了 Object.setPrototypeOf 方法,該方法是將傳入第一個參數對象的原型設置為第二個參數傳入的對象,所以我們第一個參數傳入的是 Child 的原型,將 Child 原型的原型設置成了 Parent 的原型,使父、子類原型鏈產生關聯,Child 的實例繼承了 Parent 原型上的方法,在 NodeJS 中的內置模塊 util 中用來實現繼承的方法 inherits,底層就是使用這種方式實現的。
缺點:父類的實例也同樣可以調用子類的原型方法,我們希望繼承是單向的,否則無法區分父、子類關系,這種方式一般是不可取的。
原型鏈繼承的思路是子類的原型的原型是父類的原型,形成了一條原型鏈,建立子類與父類原型的關系。
// 原型鏈繼承 // 父類 function Parent(name) { this.name = name; this.hobby = ["basketball", "football"]; } // 子類 function Child() {} // 繼承 Child.prototype = new Parent();
上面用 Parent 的實例替換了 Child 自己的原型,由于父類的實例原型直接指向 Parent.prototype,所以也使父、子類原型鏈產生關聯,子類實例繼承了父類原型的方法。
缺點 1:只能繼承父類原型上的方法,卻無法繼承父類上的屬性。
缺點 2:由于原型對象被替換,原本原型的 constructor 屬性丟失。
缺點 3:如果父類的構造函數中有屬性,則創建的父類的實例也會有這個屬性,用這個實例的作為子類的原型,這個屬性就變成了所有子類實例所共有的,這個屬性可能是多余的,并不是我們想要的,也可能我們希望它不是共有的,而是每個實例自己的。
構造函數繼承又被國內的開發者叫做 “經典繼承”。
// 構造函數繼承 // 父類 function Parent(name) { this.name = name; } // 子類 function Child() { Parent.apply(this, arguments); } let c = new Child("Panda"); console.log(c); // { name: "Panda" }
構造函數繼承的原理就是在創建 Child 實例的時候執行了 Child 構造函數,并借用 call 或 apply 在內部執行了父類 Parent,并把父類的屬性創建給了 this,即子類的實例,解決了原型鏈繼承不能繼承父類屬性的缺點。
缺點:子類的實例只能繼承父類的屬性,卻不能繼承父類的原型的方法。
為了使子類既能繼承父類原型的方法,又能繼承父類的屬性到自己的實例上,就有了這種組合使用的方式。
// 構造函數原型鏈組合繼承 // 父類 function Parent(name) { this.name = name; } Parent.prototype.sayName = function() { console.log(this.name); }; // 子類 function Child() { Parent.apply(this, arguments); } // 繼承 Child.prototype = new Parent(); let c = new Child("Panda"); console.log(c); // { name: "Panda" } c.sayName(); // Panda
這種繼承看似完美,但是之前 constructor 丟失和子類原型上多余共有屬性的問題還是沒有解決,在這基礎上又產生了新的問題。
缺點:父類被執行了兩次,在使用 call 或 apply 繼承屬性時執行一次,在創建實例替換子類原型時又被執行了一次。
原型式繼承主要用來解決用父類的實例替換子類的原型時共有屬性的問題,以及父類構造函數執行兩次的問題,也就是說通過原型式繼承能保證子類的原型是 “干凈的”,而保證只在繼承父類的屬性時執行一次父類。
// 原型式繼承 // 父類 function Parent(name) { this.name = name; } // 子類 function Child() { Parent.apply(this, arguments); } // 繼承函數 function create(obj) { function F() {} F.prototype = obj; return new F(); } // 繼承 Child.prototype = create(Parent.prototype); let c = new Child("Panda"); console.log(c); // { name: "Panda" }
原型式繼承其實是借助了一個中間的構造函數,將中間構造函數 F 的 prototype 替換成了父類的原型,并創建了一個 F 的實例返回,這個實例是不具備任何屬性的(干凈的),用這個實例替換子類的原型,因為這個實例的原型指向 F 的原型,F 的原型同時又是父類的原型對象,所以子類實例繼承了父類原型的方法,父類只在創建子類實例的時候執行了一次,省去了創建父類實例的過程。
原型式繼承在 ES5 標準中被封裝成了一個專門的方法 Object.create,該方法的第一個參數與上面 create 函數的參數相同,即要作為原型的對象,第二個參數則可以傳遞一個對象,會把對象上的屬性添加到這個原型上,一般第二個參數用來彌補 constructor 的丟失問題,這個方法不兼容 IE 低版本瀏覽器。
寄生式繼承就是用來解決子統一為原型式繼承中返回的對象統一添加方法的問題,只是在原型式繼承的基礎上做了小小的修改。
// 寄生式繼承 // 父類 function Parent(name) { this.name = name; } // 子類 function Child() { Parent.apply(this, arguments); } // 繼承函數 function create(obj) { function F() {} F.prototype = obj; return new F(); } // 將子類方法私有化函數 function creatFunction(obj) { // 調用繼承函數 let clone = create(obj); // 子類原型方法(多個) clone.sayName = function() {}; clone.sayHello = function() {}; return clone; } // 繼承 Child.prototype = creatFunction(Parent.prototype);
缺點:因為寄生式繼承最后返回的是一個對象,如果用一個變量直接來接收它,那相當于添加的所有方法都變成這個對象自身的了,如果創建了多個這樣的對象,無法實現相同方法的復用。
// 寄生組合式繼承 // 父類 function P(name, age) { this.name = name; this.age = age; } P.prototype.headCount = 1; P.prototype.eat = function() { console.log("eating..."); }; // 子類 function C(name, age) { P.apply(this, arguments); } // 寄生組合式繼承方法 function myCreate(Child, Parent) { function F() {} F.prototype = Parent.prototype; Child.prototype = new F(); Child.prototype.constructor = Child; // 讓 Child 子類的靜態屬性 super 和 base 指向父類的原型 Child.super = Child.base = Parent.prototype; } // 調用方法實現繼承 myCreate(C, P); // 向子類原型添加屬性方法,因為子類構造函數的原型被替換,所以屬性方法仍然在替換之后 C.prototype.language = "javascript"; C.prototype.work = function() { console.log("writing code use " + this.language); }; C.work = function() { this.super.eat(); }; // 驗證繼承是否成功 let f = new C("nihao", 16); f.work(); C.work(); // writing code use javascript // eating...
寄生組合式繼承基本規避了其他繼承的大部分缺點,應該比較強大了,也是平時使用最多的一種繼承,其中 Child.super 方法的作用是為了在調用子類靜態屬性的時候可以調用父類的原型方法。
缺點:子類沒有繼承父類的靜態方法。
在 ES6 規范中有了類的概念,使繼承變得容易,在規避上面缺點的完成繼承的同時,又在繼承時繼承了父類的靜態屬性。
// class...extends... 繼承 // 父類 class P { constructor(name, age) { this.name = name; this.age = age; } sayName() { console.log(this.name); } static sayHi() { console.log("Hello"); } } // 子類繼承父類 class C extends P { constructor(name, age) { supper(name, age); // 繼承父類的屬性 } sayHello() { P.sayHi(); } static sayHello() { super.sayHi(); } } let c = new C("jack", 18); c.sayName(); // jack c.sayHello(); // Hello C.sayHi(); // Hello C.sayHello(); // Hello
在子類的 constructor 中調用 supper 可以實現對父類屬性的繼承,父類的原型方法和靜態方法直接會被子類繼承,在子類的原型方法中使用父類的原型方法只需使用 this 或 supper 調用即可,此時 this 指向子類的實例,如果在子類的靜態方法中使用 this 或 supper 調用父類的靜態方法,此時 this 指向子類本身。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/98287.html
摘要:在創建子類實例時,不能向超類型的構造函數中傳遞參數。構造函數繼承子類傳進的值是基本思想是在子類構造函數的內部調用超類或父類型構造函數。繼承保證構造函數指針指向如果想同時繼承多個,還可使用添加屬性的方式類繼承, OOP:Object Oriented Programming 面向對象編程。 題外話:面向對象的范圍實在太大,先把這些大的東西理解理解。 1.什么是對象? 根據高程和權威指南上...
摘要:使用異步編程,有一個事件循環。它作為面向對象編程的替代方案,其中應用狀態通常與對象中的方法搭配并共享。在用面向對象編程時遇到不同的組件競爭相同的資源的時候,更是如此。 翻譯:瘋狂的技術宅原文:https://www.indeed.com/hire/i... 本文首發微信公眾號:jingchengyideng歡迎關注,每天都給你推送新鮮的前端技術文章 不管你是面試官還是求職者,里面...
摘要:深入之繼承的多種方式和優缺點深入系列第十五篇,講解各種繼承方式和優缺點。對于解釋型語言例如來說,通過詞法分析語法分析語法樹,就可以開始解釋執行了。 JavaScript深入之繼承的多種方式和優缺點 JavaScript深入系列第十五篇,講解JavaScript各種繼承方式和優缺點。 寫在前面 本文講解JavaScript各種繼承方式和優缺點。 但是注意: 這篇文章更像是筆記,哎,再讓我...
摘要:通常有這兩種繼承方式接口繼承和實現繼承。理解繼承的工作是通過調用函數實現的,所以是寄生,將繼承工作寄托給別人做,自己只是做增強工作。適用基于某個對象或某些信息來創建對象,而不考慮自定義類型和構造函數。 一、繼承的概念 繼承,是面向對象語言的一個重要概念。通常有這兩種繼承方式:接口繼承和實現繼承。接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。 《JS高程》里提到:由于函數沒有簽名,...
摘要:最常見的判斷方法它的官方解釋操作符返回一個字符串,表示未經計算的操作數的類型。另外,是判斷對象是否屬于某一類型,而不是獲取的對象的類型。多個窗口意味著多個全局環境,不同的全局環境擁有不同的全局對象,從而擁有不同的內置類型構造函數。 js中的數據類型 js中只有六種原始數據類型和一個Object: Boolean Null Undefined Number String Symbol ...
閱讀 1776·2021-10-27 14:15
閱讀 3835·2021-10-08 10:12
閱讀 1168·2021-09-22 15:55
閱讀 3230·2021-09-22 15:17
閱讀 834·2021-09-02 15:40
閱讀 1748·2019-08-29 18:33
閱讀 1099·2019-08-29 15:22
閱讀 2355·2019-08-29 11:08