摘要:因?yàn)椴僮鞣麆?chuàng)建的對(duì)象都繼承自構(gòu)造函數(shù)的屬性。繼承的實(shí)現(xiàn)中常用的繼承方式是組合繼承,也就是通過構(gòu)造函數(shù)和原型鏈繼承同時(shí)來模擬繼承的實(shí)現(xiàn)。
原文發(fā)布在我的博客
我們都知道 JavaScript 是一門基于原型的語言。當(dāng)我們調(diào)用一個(gè)對(duì)象本身沒有的屬性時(shí),JavaScript 就會(huì)從對(duì)象的原型對(duì)象上去找該屬性,如果原型上也沒有該屬性,那就去找原型的原型,一直找原型鏈的末端也就是 Object.prototype 的原型 null。這種屬性查找的方式我們稱之為原型鏈。
類的實(shí)現(xiàn)由于 JavaScript 本身是沒有的類的感念的。所以我們?nèi)绻獙?shí)現(xiàn)一個(gè)類,一般是通過構(gòu)造函數(shù)來模擬類的實(shí)現(xiàn):
function Person(name,age){ //實(shí)現(xiàn)一個(gè)類 this.name = name; this.age = age; } var you = new Person("you",23); //通過 new 來新建實(shí)例
首先新建一個(gè) Person 的構(gòu)造函數(shù),為了和一般的函數(shù)區(qū)別,我們會(huì)使用 CamelCase 方式來命名構(gòu)造函數(shù)。
然后通過 new 操作符來創(chuàng)建實(shí)例,new 操作符其實(shí)干了這么幾件事:
創(chuàng)建一個(gè)繼承自 Person.prototype 的新對(duì)象
構(gòu)造函數(shù) Person 執(zhí)行時(shí),相應(yīng)的參數(shù)傳入,同時(shí)上下文被指定為這個(gè)新建的對(duì)象。
如果構(gòu)造函數(shù)返回了一個(gè)對(duì)象,那么這個(gè)對(duì)象會(huì)取代 new 的結(jié)果。如果構(gòu)造函數(shù)返回的不是對(duì)象,則會(huì)忽略這個(gè)返回值。
返回值不是對(duì)象 function Person(name){ this.name = name; return "person" } var you = new Person("you"); // you 的值: Person {name: "you"} 返回值是對(duì)象 function Person(name){ this.name = name; return [1,2,3] } var you = new Person("you"); // you的值: [1,2,3]
如果類的實(shí)例需要共享類的方法,那么就需要給構(gòu)造函數(shù)的 prototype 屬性添加方法了。因?yàn)?new 操作符創(chuàng)建的對(duì)象都繼承自構(gòu)造函數(shù)的 prototype 屬性。他們可以共享定義在類 prototype 上的方法和屬性。
function Person(name,age){ this.name = name; this.age = age; } Person.prototype = { sayName: function(){ console.log("My name is",this.name); } } var you = new Person("you",23); var me = new Person("me",23); you.sayName() // My name is you. me.sayName() // My name is me.繼承的實(shí)現(xiàn)
JavaScript 中常用的繼承方式是組合繼承,也就是通過構(gòu)造函數(shù)和原型鏈繼承同時(shí)來模擬繼承的實(shí)現(xiàn)。
//Person 構(gòu)造函數(shù)如上 function Student(name,age,clas){ Person.call(this,name,age) this.clas = clas; } Student.prototype = Object.create(Person.prototype); // Mark 1 Student.constructor = Student; //如果不指明,則 Student 會(huì)找不到 constructor Student.prototype.study = function(){ console.log("I study in class",this.clas) }; var liming = new Student("liming",23,7); liming instanceof Person //true liming instanceof Student //true liming.sayName(); // My name is liming liming.study(); // I study in class 7
代碼中 Mark 1 用到了 Object.create 方法。這個(gè)是 ES5 中新增的方法,用來創(chuàng)建一個(gè)擁有指定原型的對(duì)象。如果環(huán)境不兼容,可以用下面這個(gè) Polyfill 來實(shí)現(xiàn)(僅實(shí)現(xiàn)第一個(gè)參數(shù))。
if(!Object.create){ Object.create = function(obj){ function F(){}; F.prototype = obj; return new F(); } }
其實(shí)就是把 obj 賦值給臨時(shí)函數(shù) F ,然后返回一個(gè) F 的實(shí)例。這樣通過代碼 Mark 1 Student 就得到了 Person.prototype 上的所有屬性。有人會(huì)問了,那么為什么不干脆把 Person.prototype 直接賦值給 Student.prototype 呢?
是的,直接賦值是可以達(dá)到子類共享父類 prototype 的目的,但是它破壞了原型鏈。即:子類和父類共用了同一個(gè) prototype,這樣當(dāng)某一個(gè)子類修改 prototype 的時(shí)候,其實(shí)同時(shí)也修改了父類的 prototype,那么就會(huì)影響到所有基于這個(gè)父類創(chuàng)建的子類,這并不是我們想要的結(jié)果??蠢樱?/p>
//Person 同上 //Student 同上 Student.prototype = Person.prototype; Student.prototype.sayName = function(){ console.log("My name is",this.name,"my class is",this.clas) } var liming = new Student("liming",23,7) liming.sayName() //My name is liming,my class is 7; //另一個(gè)子類 function Employee(name,age,salary){ Person.call(name,age); this.salary = salary; } Employee.prototype = Person.prototype; var emp = new Employee("emp",23,10000); emp.sayName() //Mark 2
你們猜 Mark 2 會(huì)輸出什么?
我們期望的 Mark 2 應(yīng)該會(huì)輸出 "My name is emp". 但實(shí)際上報(bào)錯(cuò),為什么呢?因?yàn)槲覀兏膶?Student.prototype 的時(shí)候,也同時(shí)修改了 Person.prototype,最終導(dǎo)致 emp 繼承的 prototype 是我們所不期望的,它的 sayName 方法是 My name is",this.name,"my class is",this.clas,這樣自然是會(huì)報(bào)錯(cuò)的。
ES6 的繼承隨著 ECMAScript 6 的發(fā)布,我們有了新的方法來實(shí)現(xiàn)繼承。也就是通過 class 關(guān)鍵字。
類的實(shí)現(xiàn)class Person { constructor(name,age){ this.name = name; this.age = age; } sayHello(){ console.log(`My name is ${this.name},i"m ${this.age} years old`) } } var you = new Person("you",23); you.sayHello() //My name is you,i"m 23 years old.繼承
ES6 里面的繼承也很方便,通過 extends 關(guān)鍵字來實(shí)現(xiàn)。
class Student extends Person{ constructor(name,age,cla){ super(name,age); this.class = cla; } study(){ console.log(`I"m study in class ${this.class}`) } } var liming = new Student("liming",23,7) liming.study() // I"m study in class 7.
這個(gè)繼承相比上面的 ES5 里面實(shí)現(xiàn)的繼承要方便了很多,但其實(shí)原理是一樣的,提供的這些關(guān)鍵字方法只是語法糖而已,并沒有改變 Js 是基于原型這么一個(gè)事實(shí)。不過 extends 這樣實(shí)現(xiàn)的繼承有一個(gè)限制,就是不能定義屬性,只能定義方法。要新添屬性,還是得通過修改 prototype 來達(dá)到目的。
Student.prototype.teacher = "Mr.Li" var liming = new Student("liming",23,7) var hanmeimei = new Student("hanmeimei",23,7) liming.teacher //Mr.Li hanmeimei.teacher //Mr.Li靜態(tài)方法
ES6 還提供了 static 關(guān)鍵字,來實(shí)現(xiàn)靜態(tài)方法。靜態(tài)方法可以繼承,但只能由類本身調(diào)用,不能被實(shí)例調(diào)用。
class Person{ constructor(name,age){ this.name = name; this.age = age; } static say(){ console.log("Static") } } class Student extends Person{} Person.say() // Static Student.say() // Static var you = new Person("you",23); you.say() // TypeError: liming.say is not a function
可以看到,在實(shí)例上調(diào)用的時(shí)候會(huì)直接報(bào)錯(cuò)。
Super關(guān)鍵字在子類中可以通過 super 來調(diào)用父類,根據(jù)調(diào)用位置的不同,行為也不同。在 constructor 中調(diào)用,相當(dāng)于調(diào)用父類的 constructor 方法,而在普通方法里面調(diào)用則相當(dāng)與調(diào)用父類本身。
class Person { constructor(name,age){ this.name = name; this.age = age; } sayHello(){ console.log(`My name is ${this.name},i"m ${this.age} years old`) } } class Student extends Person{ constructor(name,age,cla){ super(name,age); // 必須在子類調(diào)用 this 前執(zhí)行,調(diào)用了父類的 constructor this.class = cla; } sayHello(){ super.sayHello; // 調(diào)用父類方法 console.log("Student say") } } var liming = new Student("liming",23,7); liming.say() // My name is liming,i"m 23 years old. Student say.總結(jié)
至此,我們可以看到:在 ES6 發(fā)布以后,JavaScript 中實(shí)現(xiàn)繼承有了一個(gè)標(biāo)準(zhǔn)的方法。雖然它們只是語法糖,背后的本質(zhì)還是通過原型鏈以及構(gòu)造函數(shù)實(shí)現(xiàn)的,不過在寫法上更易于我們理解而且也更加清晰。
參考:
JavaScript繼承方式詳解
JavaScript 原型系統(tǒng)的變遷,以及 ES6 class
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/79034.html
摘要:屬性每個(gè)函數(shù)默認(rèn)有屬性方法返回的函數(shù)除外,其值為構(gòu)造函數(shù)創(chuàng)建對(duì)象繼承的對(duì)象。其思路使用原型鏈實(shí)現(xiàn)原型屬性和方法的繼承通過借用構(gòu)造函數(shù)實(shí)現(xiàn)實(shí)例屬性繼承。 1 類和模塊 每個(gè)獨(dú)立的JavaScript對(duì)象都是一個(gè)屬性的集合,獨(dú)立對(duì)象間沒有任何關(guān)系 ES5中的類是基于原型繼承實(shí)現(xiàn)的:如果兩個(gè)對(duì)象從同一個(gè)原型對(duì)象繼承屬性,稱兩個(gè)對(duì)象為同一個(gè)類的實(shí)例。r instanceof Range.pr...
摘要:使用新的易用的類定義,歸根結(jié)底也是要?jiǎng)?chuàng)建構(gòu)造函數(shù)和修改原型。首先,它把構(gòu)造函數(shù)當(dāng)成單獨(dú)的函數(shù)且包含類屬性集。該節(jié)點(diǎn)還儲(chǔ)存了指向父類的指針引用,該父類也并儲(chǔ)存了構(gòu)造函數(shù),屬性集和及父類引用,依次類推。 原文請(qǐng)查閱這里,略有刪減,本文采用知識(shí)共享署名 4.0 國(guó)際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原理的第...
摘要:使用新的易用的類定義,歸根結(jié)底也是要?jiǎng)?chuàng)建構(gòu)造函數(shù)和修改原型。首先,它把構(gòu)造函數(shù)當(dāng)成單獨(dú)的函數(shù)且包含類屬性集。該節(jié)點(diǎn)還儲(chǔ)存了指向父類的指針引用,該父類也并儲(chǔ)存了構(gòu)造函數(shù),屬性集和及父類引用,依次類推。 原文請(qǐng)查閱這里,略有刪減,本文采用知識(shí)共享署名 4.0 國(guó)際許可協(xié)議共享,BY Troland。 本系列持續(xù)更新中,Github 地址請(qǐng)查閱這里。 這是 JavaScript 工作原理的第...
摘要:下面是用實(shí)現(xiàn)轉(zhuǎn)成抽象語法樹如下還支持繼承以下是轉(zhuǎn)換結(jié)果最終的結(jié)果還是代碼,其中包含庫(kù)中的一些函數(shù)。可以使用新的易于使用的類定義,但是它仍然會(huì)創(chuàng)建構(gòu)造函數(shù)和分配原型。 這是專門探索 JavaScript 及其所構(gòu)建的組件的系列文章的第 15 篇。 想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你! 如果你錯(cuò)過了前面的章節(jié),可以在這里找到它們: JavaScript 是...
閱讀 2141·2023-04-25 17:23
閱讀 2948·2021-11-17 09:33
閱讀 2551·2021-08-21 14:09
閱讀 3669·2019-08-30 15:56
閱讀 2634·2019-08-30 15:54
閱讀 1650·2019-08-30 15:53
閱讀 2160·2019-08-29 13:53
閱讀 1174·2019-08-29 12:31