【設計模式+原型理解】第三章:javascript五種繼承父類方式
【前言】
我們都知道,面向物件(類)的三大特徵:封裝、繼承、多型
繼承:子類繼承父類的私有屬性和公有方法
封裝:把相同的程式碼寫在一個函式中
多型:
->過載:JS嚴格意義上是沒有過載,但可以通過傳遞不同引數實現不同功能
->重寫:子類重寫父類的方法(這裡只要把父類的原型一改,父類的其他例項會受到影響,又因為子類的原型鏈繼承父類的例項,這就會導致同樣會影響到子類的例項,本質是因為在JS原型繼承中,由於它的核心原理,繼承並不是從父類中拿過一份一模一樣的東西拷貝過來,而是讓子類和父類之間增加了一個原型鏈這樣一個橋樑)
【原型繼承】
什麼是原型繼承,下面是一個非常常見的例子:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div id="#div1"></div> </body> <script> console.dir(document.getElementById('div1')) </script> </html>
當你展開id="div1"這個dom節點的時候,你會看到,它的__proto__嵌套了一層又一層,如下,這是一條非常長的原型鏈
// #div1.__proto__ -> HTMLDivElement.prototype -> HTMLElement.prototype -> Element.prototype // -> Node.prototype -> EventTarget.prototype -> Object.prototype
上面的原型鏈非常長,但是是如何將它們一級一級關聯起來的呢?實現原理如下:
// 第四層 Object // 第三層 myObject function myObject() { } myObject.prototype = { constructor: myObject, hasOwnProperty: function () {} }; // 第二層 myEventTraget function myEventTraget() { } myEventTraget.prototype = new myObject(); // 子類的原型等於父類的例項 myEventTraget.prototype.constructor = myEventTraget; myEventTraget.prototype.addEventListener = function () {}; // 第一層 myNode function myNode() { } myNode.prototype = new myEventTraget(); // 子類的原型等於父類的例項 myNode.prototype.constructor = myNode; myNode.prototype.createElment = function () {}; // 例項化 var n = new myNode; // 打印出來,看看n的結果,已經有四層__proto__了。
例項n 打印出來的樣子如下:
上面的多層繼承,是不是看起來特別想dom的原型繼承,一層套一層,下面來個簡化版的。
// 原型繼承簡化 function A() { this.x = 100; } A.prototype.getX = function () { console.log(this.x) }; function B() { this.y = 200; } // 現在,B想繼承A的私有+公有的屬性和方法 B.prototype = new A; B.prototype.constructor = B; var b = new B;
上面簡化版的原型繼承原理,可以參考下圖:
原型繼承總結:
“原型繼承”是JS最常見的一種繼承方式
子類B想要繼承父類A中的所有屬性和方法(私有+公有),只需要B.prototype = new A即可
特點:它把父類中私有+公有的都繼承到子類原型上(即子類公有的)
核心:原型繼承並不是把父類中的屬性和方法克隆一份一模一樣的給B,而是讓B和A之間增加原型鏈的連線,以後例項b想要A中的getX方法,需要一級級向上查詢來使用。
【call繼承】
-> 把父類私有屬性和方法,克隆一份一模一樣的,作為子類私有的屬性和方法
function A() { // 一個函式,它有三種角色:1、普通函式(私有作用域);2、類(new);3、普通物件(__proto__) this.x = 100; this.a = function () { console.log(this.x); } } A.prototype.getX = function () { console.log(this.x); }; function B() { this.y = 100; // this->b A.call(this);// ->A.call(b) 把A執行,讓A中的this變為了n!!! //此時的A.prototype對B類來說是沒用的,因為B沒有繼承A } var b = new B; console.log(b.x); // ->100 b.a(); // ->100 b.getX();// ->b.getX is not a function
值得注意的是,b.getX() // -> b.getX is not a function。為什麼會這樣呢,因為b實際上並沒有繼承A類,所以A.prototype對B是沒有任何作用的,此時的A,實際上只是作為函式,而不是作為一個類!!!
總結call繼承
->call繼承,把父類的私有屬性和私有方法全部都拿過來了,但是卻拿不了父類的公有方法,這是它的缺點,也是優點。
【冒充物件繼承】
->把父類私有的+公有的屬性和方法克隆一份一模一樣的,給子類私有的
function A() { this.x = 100; } A.prototype.getX = function () { console.log(this.x); }; function B() { // -> this->b var temp = new A; // 將A的例項當做普通物件來做遍歷,這就是冒充物件 for (var key in temp) {// 把父類A私有的和公有的,都複製過來給子類B私有的 this[key] = temp[key]; } temp = null; } var b = new B; b.getX(); // ->x、getX
注意:for in的寫法,可以把物件公用和私有的屬性和方法,全部都打印出來;
obj.propertyIsEnumerable(key),此方法可以判斷物件的私有屬性
obj .hasOwnProperty(key),此方法同樣也可以判斷物件的私有屬性
總結“冒充物件繼承”:
->這個繼承方法比call跟完善了一步,call繼承只是把父類的私有拿過來變成自己私有的,但是“冒充物件繼承”則是把父類的私有+公有的屬性和方法拿過來變成自己私有的。
【混合模式繼承】
->原型繼承 + call繼承
function A() { tihs.x = 100; } A.prototype.getX = function () { console.log(this.x) }; function B() { A.call(this); // ->這一步,即等於: x=100 } B.prototype = new A; // ->這一步,即等於:B.prototype: x=100 getX=.... B.prototype.constructor = B; var b = new B; b.getX();
總結:
->這種方法,缺點是將A這個類,執行了兩次
->首先父類私有的複製了兩遍,第一遍是用call把父類私有的,複製給了子類私有的;第二遍就是使用原型繼承,把父類私有+公有的屬性,給了子類公有的
->也就是說重複了父類私有的存在於子類私有上, 也存在於子類公有上。也就是說,重複了一次父類私有的複製。
【寄生組合式繼承】( 強烈推薦 )
->目的:子類繼承父類,父類私有的子類就繼承私有的,父類公有的子類就繼承公有的(注意,是在__proto__又套了一層原型)
// 寄生組合式繼承 function A() { this.x = 100; } A.prototype.getX = function () { console.log(this.x); }; function B() { // ->this->b A.call(this); } // B.prototype = Object.create(A.prototype); // 意思是把父類的原型,給了子類的原型 // Object.create建立了一個物件,並且把這個新物件的原型指向了a的原型,然後B的原型指向了這個新物件 B.prototype = objectCreate(A.prototype); B.prototype.constructor = B; var b = new B; console.log(b); // Object.create的原理如下 function objectCreate(o) { function Fn() {} Fn.prototype=o; return new Fn; }
注意,這種方式跟原型繼承是有區別的,
->原型繼承:是把父類私有+公有的屬性給了子類的公有上
-> 寄生組合式繼承:
繼承私有:A.call(this);
繼承公有:先把父類私有的清空(這裡的清空可以新建一個新物件,然後新物件的原型指向父類的原型即可),然後子類的原型指向該新物件
->比較繞,可以看看下圖,即寄生組合式繼承原理圖
總結“寄生式繼承”
-> 其實是比較完美地實現了繼承,子類繼承父類,父類私有的屬性就放在子類私有上,父類公有的屬性就放在子類公有上。
--END--