天來(lái)講解javascript中實(shí)現(xiàn)繼承的幾種方式,有興趣的小伙伴們可以來(lái)看看,若有錯(cuò)誤之處,歡迎指出!
一、構(gòu)造函數(shù)模式
每個(gè)方法都要在每個(gè)實(shí)例上重新創(chuàng)建一遍。在前面的例子中,person1和person2都有一個(gè)名為sayName()的方法,但那兩個(gè)方法不是同一個(gè)Function的實(shí)例。
二、原型模式
無(wú)論什么時(shí)候,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性。在默認(rèn)情況下,所有prototype屬性都會(huì)自動(dòng)獲得一個(gè)constructor(構(gòu)造函數(shù))屬性,這個(gè)屬性包含一個(gè)指向prototype屬性所在函數(shù)的指針。Person.prototype.constructor指向Person。創(chuàng)建了自定義的構(gòu)造函數(shù)之后,其原型屬性默認(rèn)只會(huì)取得constructor屬性;至于其他方法,則都是從object繼承而來(lái)的。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例的內(nèi)部將包含一個(gè)指針(內(nèi)部屬性),指向構(gòu)造函數(shù)的原型屬性。在很多實(shí)現(xiàn)中,這個(gè)內(nèi)部屬性的名字是_proto_。不過(guò),要明確的真正重要一點(diǎn),就是這個(gè)鏈接存在于實(shí)例與構(gòu)造函數(shù)的原型屬性之間,而不是存在于實(shí)例與構(gòu)造函數(shù)之間。
三、原型鏈
原型鏈:ECMAScript中描述了原型鏈的概念,并將原型鏈作為實(shí)現(xiàn)繼承的主要方法。其基本思想是利用原型讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法。簡(jiǎn)單回顧一下構(gòu)造函數(shù)、原型、實(shí)例的關(guān)系:每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的指針,而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針。那么,假如我們讓原型對(duì)象等于另一個(gè)類型的實(shí)例,結(jié)果會(huì)怎么樣呢?顯然,此時(shí)的原型對(duì)象將包含一個(gè)指向另一個(gè)原型的指針,相應(yīng)地,另一個(gè)原型中也包含著一個(gè)指向另一個(gè)構(gòu)造函數(shù)的指針。假如另一個(gè)原型又是另一個(gè)類型的實(shí)例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條。這就是所謂原型鏈的基本概念。
四、組合繼承
組合繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮二者之長(zhǎng)的一種繼承模式。其背后的思路是使用原型鏈實(shí)現(xiàn)對(duì)原型屬性和方法的繼承,而通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。這樣,既通過(guò)在原型上定義方法實(shí)現(xiàn)了函數(shù)復(fù)用,又能夠保證每個(gè)實(shí)例都有它自己的屬性。
于有基于類的語(yǔ)言經(jīng)驗(yàn) (如 Java 或 C++) 的開發(fā)人員來(lái)說(shuō),JavaScript 有點(diǎn)令人困惑,因?yàn)樗莿?dòng)態(tài)的,并且本身不提供一個(gè)class實(shí)現(xiàn)。(在 ES2015/ES6 中引入了class關(guān)鍵字,但只是語(yǔ)法糖,JavaScript 仍然是基于原型的)。
JavaScript 只有一種繼承結(jié)構(gòu):對(duì)象。每個(gè)實(shí)例對(duì)象(object )都有一個(gè)私有屬性(稱之為[[prototype]])指向它的原型對(duì)象(prototype)。該原型對(duì)象也有一個(gè)自己的原型對(duì)象 ,層層向上直到一個(gè)對(duì)象的原型對(duì)象為 null。根據(jù)定義,null 沒(méi)有原型,并作為這個(gè)原型鏈中的最后一個(gè)環(huán)節(jié)。
幾乎所有 JavaScript 中的對(duì)象都是位于原型鏈頂端的Object的實(shí)例。
遵循ECMAScript標(biāo)準(zhǔn),someObject.[[Prototype]] 符號(hào)是用于指向 someObject的原型。從 ECMAScript 6 開始,[[Prototype]] 可以通過(guò)Object.getPrototypeOf()和Object.setPrototypeOf()訪問(wèn)器來(lái)訪問(wèn)。這個(gè)等同于 JavaScript 的非標(biāo)準(zhǔn)但許多瀏覽器實(shí)現(xiàn)的屬性 proto。
但它不應(yīng)該與構(gòu)造函數(shù) func 的 prototype 屬性相混淆。被構(gòu)造函數(shù)創(chuàng)建的實(shí)例對(duì)象的 [[prototype]] 指向 func 的 prototype 屬性。Object.prototype 屬性表示Object的原型對(duì)象。
// 讓我們從一個(gè)自身?yè)碛袑傩詀和b的函數(shù)里創(chuàng)建一個(gè)對(duì)象o: let f=function () { this.a=1; this.b=2; } /* 你要這么寫也沒(méi)區(qū)別 function f(){ this.a=1; this.b=2; } */ let o=new f(); // {a: 1, b: 2} // 在f函數(shù)的原型上定義屬性 f.prototype.b=3; f.prototype.c=4; //不要在f函數(shù)的原型上直接定義 f.prototype={b:3,c:4};這樣會(huì)直接打破原型鏈 // o.[[Prototype]] 有屬性 b 和 c (其實(shí)就是o.__proto__或者o.constructor.prototype) // o.[[Prototype]].[[Prototype]] 是 Object.prototye. // 最后o.[[Prototype]].[[Prototype]].[[Prototype]]是null // 這就是原型鏈的末尾,即 null, // 根據(jù)定義,null 沒(méi)有[[Prototype]]. // 綜上,整個(gè)原型鏈如下: // {a:1, b:2} ---> {b:3, c:4} ---> Object.prototye---> null console.log(o.a); // 1 // a是o的自身屬性嗎?是的,該屬性的值為1 console.log(o.b); // 2 // b是o的自身屬性嗎?是的,該屬性的值為2 // 原型上也有一個(gè)'b'屬性,但是它不會(huì)被訪問(wèn)到.這種情況稱為"屬性遮蔽 (property shadowing)" console.log(o.c); // 4 // c是o的自身屬性嗎?不是,那看看原型上有沒(méi)有 // c是o.[[Prototype]]的屬性嗎?是的,該屬性的值為4 console.log(o.d); // undefined // d是o的自身屬性嗎?不是,那看看原型上有沒(méi)有 // d是o.[[Prototype]]的屬性嗎?不是,那看看它的原型上有沒(méi)有 // o.[[Prototype]].[[Prototype]] 為 null,停止搜索 // 沒(méi)有d屬性,返回undefined
當(dāng)繼承的函數(shù)被調(diào)用時(shí),this 指向的是當(dāng)前繼承的對(duì)象,而不是繼承的函數(shù)所在的原型對(duì)象
var o={ a: 2, m: function(){ return this.a + 1; } }; console.log(o.m()); // 3 // 當(dāng)調(diào)用 o.m 時(shí),'this'指向了o. var p=Object.create(o); // p是一個(gè)繼承自 o 的對(duì)象 p.a=4; // 創(chuàng)建 p 的自身屬性 a console.log(p.m()); // 5 // 調(diào)用 p.m 時(shí), 'this'指向 p. // 又因?yàn)?p 繼承 o 的 m 函數(shù) // 此時(shí)的'this.a' 即 p.a,即 p 的自身屬性 'a'
使用不同的方法來(lái)創(chuàng)建對(duì)象和生成原型鏈
var o={a: 1}; // o 這個(gè)對(duì)象繼承了Object.prototype上面的所有屬性 // o 自身沒(méi)有名為 hasOwnProperty 的屬性 // hasOwnProperty 是 Object.prototype 的屬性 // 因此 o 繼承了 Object.prototype 的 hasOwnProperty // Object.prototype 的原型為 null // 原型鏈如下: // o ---> Object.prototype ---> null var a=["yo", "whadup", "?"]; // 數(shù)組都繼承于 Array.prototype // (Array.prototype 中包含 indexOf, forEach等方法) // 原型鏈如下: // a ---> Array.prototype ---> Object.prototype ---> null function f(){ return 2; } // 函數(shù)都繼承于Function.prototype // (Function.prototype 中包含 call, bind等方法) // 原型鏈如下: // f ---> Function.prototype ---> Object.prototype ---> null
在 JavaScript 中,構(gòu)造器其實(shí)就是一個(gè)普通的函數(shù)。當(dāng)使用 new 操作符 來(lái)作用這個(gè)函數(shù)時(shí),它就可以被稱為構(gòu)造方法(構(gòu)造函數(shù))。
function Graph() { this.vertices=[]; this.edges=[]; } Graph.prototype={ addVertex: function(v){ this.vertices.push(v); } }; var g=new Graph(); // g是生成的對(duì)象,他的自身屬性有'vertices'和'edges'. // 在g被實(shí)例化時(shí),g.[[Prototype]]指向了Graph.prototype.
ECMAScript 5 中引入了一個(gè)新方法:Object.create()??梢哉{(diào)用這個(gè)方法來(lái)創(chuàng)建一個(gè)新對(duì)象。新對(duì)象的原型就是調(diào)用 create 方法時(shí)傳入的第一個(gè)參數(shù):
var a={a: 1}; // a ---> Object.prototype ---> null var b=Object.create(a); // b ---> a ---> Object.prototype ---> null console.log(b.a); // 1 (繼承而來(lái)) var c=Object.create(b); // c ---> b ---> a ---> Object.prototype ---> null var d=Object.create(null); // d ---> null console.log(d.hasOwnProperty); // undefined, 因?yàn)閐沒(méi)有繼承Object.prototype
ECMAScript6 引入了一套新的關(guān)鍵字用來(lái)實(shí)現(xiàn) class。使用基于類語(yǔ)言的開發(fā)人員會(huì)對(duì)這些結(jié)構(gòu)感到熟悉,但它們是不同的。JavaScript 仍然基于原型。這些新的關(guān)鍵字包括 class, constructor,static,extends 和 super。
上面的章節(jié)中我們看到了JavaScript的對(duì)象模型是基于原型實(shí)現(xiàn)的,特點(diǎn)是簡(jiǎn)單,缺點(diǎn)是理解起來(lái)比傳統(tǒng)的類-實(shí)例模型要困難,最大的缺點(diǎn)是繼承的實(shí)現(xiàn)需要編寫大量代碼,并且需要正確實(shí)現(xiàn)原型鏈。
有沒(méi)有更簡(jiǎn)單的寫法?有!
新的關(guān)鍵字class從ES6開始正式被引入到JavaScript中。class的目的就是讓定義類更簡(jiǎn)單。
我們先回顧用函數(shù)實(shí)現(xiàn)Student的方法:
function Student(name) { this.name=name; } Student.prototype.hello=function () { alert('Hello, ' + this.name + '!'); }
如果用新的class關(guān)鍵字來(lái)編寫Student,可以這樣寫:
class Student { constructor(name) { this.name=name; } hello() { alert('Hello, ' + this.name + '!'); } }
比較一下就可以發(fā)現(xiàn),class的定義包含了構(gòu)造函數(shù)constructor和定義在原型對(duì)象上的函數(shù)hello()(注意沒(méi)有function關(guān)鍵字),這樣就避免了Student.prototype.hello=function () {...}這樣分散的代碼。
最后,創(chuàng)建一個(gè)Student對(duì)象代碼和前面章節(jié)完全一樣:
var xiaoming=new Student('小明'); xiaoming.hello();
用class定義對(duì)象的另一個(gè)巨大的好處是繼承更方便了。想一想我們從Student派生一個(gè)PrimaryStudent需要編寫的代碼量。現(xiàn)在,原型繼承的中間對(duì)象,原型對(duì)象的構(gòu)造函數(shù)等等都不需要考慮了,直接通過(guò)extends來(lái)實(shí)現(xiàn):
class PrimaryStudent extends Student { constructor(name, grade) { super(name); // 記得用super調(diào)用父類的構(gòu)造方法! this.grade=grade; } myGrade() { alert('I am at grade ' + this.grade); } }
注意PrimaryStudent的定義也是class關(guān)鍵字實(shí)現(xiàn)的,而extends則表示原型鏈對(duì)象來(lái)自Student。子類的構(gòu)造函數(shù)可能會(huì)與父類不太相同,例如,PrimaryStudent需要name和grade兩個(gè)參數(shù),并且需要通過(guò)super(name)來(lái)調(diào)用父類的構(gòu)造函數(shù),否則父類的name屬性無(wú)法正常初始化。
PrimaryStudent已經(jīng)自動(dòng)獲得了父類Student的hello方法,我們又在子類中定義了新的myGrade方法。
ES6引入的class和原有的JavaScript原型繼承有什么區(qū)別呢?實(shí)際上它們沒(méi)有任何區(qū)別,class的作用就是讓JavaScript引擎去實(shí)現(xiàn)原來(lái)需要我們自己編寫的原型鏈代碼。簡(jiǎn)而言之,用class的好處就是極大地簡(jiǎn)化了原型鏈代碼。
你一定會(huì)問(wèn),class這么好用,能不能現(xiàn)在就用上?
現(xiàn)在用還早了點(diǎn),因?yàn)椴皇撬械闹髁鳛g覽器都支持ES6的class。如果一定要現(xiàn)在就用上,就需要一個(gè)工具把class代碼轉(zhuǎn)換為傳統(tǒng)的prototype代碼,可以試試Babel這個(gè)工具。
需要瀏覽器支持ES6的class,如果遇到SyntaxError,則說(shuō)明瀏覽器不支持class語(yǔ)法,請(qǐng)換一個(gè)最新的瀏覽器試試。
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。