摘要:接下來(lái)我們來(lái)聊一下的原型鏈繼承和類(lèi)。組合繼承為了復(fù)用方法,我們使用組合繼承的方式,即利用構(gòu)造函數(shù)繼承屬性,利用原型鏈繼承方法,融合它們的優(yōu)點(diǎn),避免缺陷,成為中最常用的繼承。
JavaScript是一門(mén)面向?qū)ο蟮脑O(shè)計(jì)語(yǔ)言,在JS里除了null和undefined,其余一切皆為對(duì)象。其中Array/Function/Date/RegExp是Object對(duì)象的特殊實(shí)例實(shí)現(xiàn),Boolean/Number/String也都有對(duì)應(yīng)的基本包裝類(lèi)型的對(duì)象(具有內(nèi)置的方法)。傳統(tǒng)語(yǔ)言是依靠class類(lèi)來(lái)完成面向?qū)ο蟮睦^承和多態(tài)等特性,而JS使用原型鏈和構(gòu)造器來(lái)實(shí)現(xiàn)繼承,依靠參數(shù)arguments.length來(lái)實(shí)現(xiàn)多態(tài)。并且在ES6里也引入了class關(guān)鍵字來(lái)實(shí)現(xiàn)類(lèi)。
接下來(lái)我們來(lái)聊一下JS的原型鏈、繼承和類(lèi)。
有時(shí)我們會(huì)好奇為什么能給一個(gè)函數(shù)添加屬性,函數(shù)難道不應(yīng)該就是一個(gè)執(zhí)行過(guò)程的作用域嗎?
var name = "Leon"; function Person(name) { this.name = name; this.sayName = function() { alert(this.name); } } Person.age = 10; console.log(Person.age); // 10 console.log(Person); /* 輸出函數(shù)體: ? Person(name) { this.name = name; } */
我們能夠給函數(shù)賦一個(gè)屬性值,當(dāng)我們輸出這個(gè)函數(shù)時(shí)這個(gè)屬性卻無(wú)影無(wú)蹤了,這到底是怎么回事,這個(gè)屬性又保存在哪里了呢?
其實(shí),在JS里,函數(shù)就是一個(gè)對(duì)象,這些屬性自然就跟對(duì)象的屬性一樣被保存起來(lái),函數(shù)名稱(chēng)指向這個(gè)對(duì)象的存儲(chǔ)空間。
函數(shù)調(diào)用過(guò)程沒(méi)查到資料,個(gè)人理解為:這個(gè)對(duì)象內(nèi)部擁有一個(gè)內(nèi)部屬性[[function]]保存有該函數(shù)體的字符串形式,當(dāng)使用()來(lái)調(diào)用的時(shí)候,就會(huì)實(shí)時(shí)對(duì)其進(jìn)行動(dòng)態(tài)解析和執(zhí)行,如同eval()一樣。
上圖是JS的具體內(nèi)存分配方式,JS中分為值類(lèi)型和引用類(lèi)型,值類(lèi)型的數(shù)據(jù)大小固定,我們將其分配在棧里,直接保存其數(shù)據(jù)。而引用類(lèi)型是對(duì)象,會(huì)動(dòng)態(tài)的增刪屬性,大小不固定,我們把它分配到內(nèi)存堆里,并用一個(gè)指針指向這片地址,也就是Person其實(shí)保存的是一個(gè)指向這片地址的指針。這里的Person對(duì)象是個(gè)函數(shù)實(shí)例,所以擁有特殊的內(nèi)部對(duì)象[[function]]用于調(diào)用。同時(shí)它也擁有內(nèi)部屬性arguments/this/name,因?yàn)椴幌嚓P(guān),這里我們沒(méi)有繪出,而展示了我們?yōu)槠涮砑拥膶傩詀ge。
函數(shù)與原型的關(guān)系同時(shí)在JS里,我們創(chuàng)建的每一個(gè)函數(shù)都有一個(gè)prototype(原型)屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)用于包含該對(duì)象所有實(shí)例的共享屬性和方法的對(duì)象。而這個(gè)對(duì)象同時(shí)包含一個(gè)指針指向這個(gè)這個(gè)函數(shù),這個(gè)指針就是constructor,這個(gè)函數(shù)也被成為構(gòu)造函數(shù)。這樣我們就完成了構(gòu)造函數(shù)和原型對(duì)象的雙向引用。
而上面的代碼實(shí)質(zhì)也就是當(dāng)我們創(chuàng)建了Person構(gòu)造函數(shù)之后,同步開(kāi)辟了一片空間創(chuàng)建了一個(gè)對(duì)象作為Person的原型對(duì)象,可以通過(guò)Person.prototype來(lái)訪問(wèn)這個(gè)對(duì)象,也可以通過(guò)Person.prototype.constructor來(lái)訪問(wèn)Person該構(gòu)造函數(shù)。通過(guò)構(gòu)造函數(shù)我們可以往實(shí)例對(duì)象里添加屬性,如上面的例子里的name屬性和sayName()方法。我們也可以通過(guò)prototype來(lái)添加原型屬性,如:
Person.prototype.name = "Nicholas"; Person.prototype.age = 24; Person.prototype.sayAge = function () { alert(this.age); };
這些原型對(duì)象為實(shí)例賦予了默認(rèn)值,現(xiàn)在我們可以看到它們的關(guān)系是:
要注意屬性和原型屬性不是同一個(gè)東西,也并不保存在同一個(gè)空間里:
Person.age; // 10 Person.prototype.age; // 24原型和實(shí)例的關(guān)系
現(xiàn)在有了構(gòu)造函數(shù)和原型對(duì)象,那我們接下來(lái)new一個(gè)實(shí)例出來(lái),這樣才能真正體現(xiàn)面向?qū)ο缶幊痰乃枷耄簿褪?b>繼承:
var person1 = new Person("Lee"); var person2 = new Person("Lucy");
我們新建了兩個(gè)實(shí)例person1和person2,這些實(shí)例的內(nèi)部都會(huì)包含一個(gè)指向其構(gòu)造函數(shù)的原型對(duì)象的指針(內(nèi)部屬性),這個(gè)指針叫[[Prototype]],在ES5的標(biāo)準(zhǔn)上沒(méi)有規(guī)定訪問(wèn)這個(gè)屬性,但是大部分瀏覽器實(shí)現(xiàn)了__proto__的屬性來(lái)訪問(wèn)它,成為了實(shí)際的通用屬性,于是在ES6的附錄里寫(xiě)進(jìn)了該屬性。__proto__前后的雙下劃線說(shuō)明其本質(zhì)上是一個(gè)內(nèi)部屬性,而不是對(duì)外訪問(wèn)的API,因此官方建議新的代碼應(yīng)當(dāng)避免使用該屬性,轉(zhuǎn)而使用Object.setPrototypeOf()(寫(xiě)操作)、Object.getPrototypeOf()(讀操作)、Object.create()(生成操作)代替。
這里的prototype我們稱(chēng)為顯示原型,__proto__我們稱(chēng)為隱式原型。
同時(shí)由于現(xiàn)代 JavaScript 引擎優(yōu)化屬性訪問(wèn)所帶來(lái)的特性的關(guān)系,更改對(duì)象的 [[Prototype]]在各個(gè)瀏覽器和 JavaScript 引擎上都是一個(gè)很慢的操作。其在更改繼承的性能上的影響是微妙而又廣泛的,這不僅僅限于 obj.__proto__ = ... 語(yǔ)句上的時(shí)間花費(fèi),而且可能會(huì)延伸到任何代碼,那些可以訪問(wèn)任何[[Prototype]]已被更改的對(duì)象的代碼。如果你關(guān)心性能,你應(yīng)該避免設(shè)置一個(gè)對(duì)象的 [[Prototype]]。相反,你應(yīng)該使用 Object.create()來(lái)創(chuàng)建帶有你想要的[[Prototype]]的新對(duì)象。
此時(shí)它們的關(guān)系是(為了清晰,忽略函數(shù)屬性的指向,用(function)代指):
在這里我們可以看到兩個(gè)實(shí)例指向了同一個(gè)原型對(duì)象,而在new的過(guò)程中調(diào)用了Person()方法,對(duì)每個(gè)實(shí)例分別初始化了name屬性和sayName方法,屬性值分別被保存,而方法作為引用對(duì)象也指向了不同的內(nèi)存空間。
我們可以用幾種方法來(lái)驗(yàn)證實(shí)例的原型指針到底指向的是不是構(gòu)造函數(shù)的原型對(duì)象:
person1.__proto__ === Person.prototype // true Person.prototype.isPrototypeOf(person1); // true Object.getPrototypeOf(person2) === Person.prototype; // true person1 instanceof Person; // true原型鏈
現(xiàn)在我們?cè)L問(wèn)實(shí)例person1的屬性和方法了:
person1.name; // Lee person1.age; // 24 person1.toString(); // [object Object]
想下這個(gè)問(wèn)題,我們的name值來(lái)自于person1的屬性,那么age值來(lái)自于哪?toString( )方法又在哪定義的呢?
這就是我們要說(shuō)的原型鏈,原型鏈?zhǔn)菍?shí)現(xiàn)繼承的主要方法,其思想是利用原型讓一個(gè)引用類(lèi)型繼承另一個(gè)引用類(lèi)型的屬性和方法。如果我們讓一個(gè)原型對(duì)象等于另一個(gè)類(lèi)型的實(shí)例,那么該原型對(duì)象就會(huì)包含一個(gè)指向另一個(gè)原型的指針,而如果另一個(gè)原型對(duì)象又是另一個(gè)原型的實(shí)例,那么上述關(guān)系依然成立,層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條,這就是原型鏈的概念。
上面代碼的name來(lái)自于自身屬性,age來(lái)自于原型屬性,toString( )方法來(lái)自于Person原型對(duì)象的原型Object。當(dāng)我們?cè)L問(wèn)一個(gè)實(shí)例屬性的時(shí)候,如果沒(méi)有找到,我們就會(huì)繼續(xù)搜索實(shí)例的原型,如果還沒(méi)有找到,就遞歸搜索原型鏈直到原型鏈末端。我們可以來(lái)驗(yàn)證一下原型鏈的關(guān)系:
Person.prototype.__proto__ === Object.prototype // true
同時(shí)讓我們更加深入的驗(yàn)證一些東西:
Person.__proto__ === Function.prototype // true Function.prototype.__proto__ === Object.prototype // true
我們會(huì)發(fā)現(xiàn)Person是Function對(duì)象的實(shí)例,F(xiàn)unction是Object對(duì)象的實(shí)例,Person原型是Object對(duì)象的實(shí)例。這證明了我們開(kāi)篇的觀點(diǎn):JavaScript是一門(mén)面向?qū)ο蟮脑O(shè)計(jì)語(yǔ)言,在JS里除了null和undefined,其余一切皆為對(duì)象。
下面祭出我們的原型鏈圖:
根據(jù)我們上面講述的關(guān)于prototype/constructor/__proto__的內(nèi)容,我相信你可以完全看懂這張圖的內(nèi)容。需要注意兩點(diǎn):
構(gòu)造函數(shù)和對(duì)象原型一一對(duì)應(yīng),他們與實(shí)例一起作為三要素構(gòu)成了三面這幅圖。最左側(cè)是實(shí)例,中間是構(gòu)造函數(shù),最右側(cè)是對(duì)象原型。
最最右側(cè)的null告訴我們:Object.prototype.__proto__ = null,也就是Object.prototype是JS中一切對(duì)象的根源。其余的對(duì)象繼承于它,并擁有自己的方法和屬性。
繼承 原型鏈繼承通過(guò)原型鏈我們已經(jīng)實(shí)現(xiàn)了對(duì)象的繼承,我們具體的實(shí)現(xiàn)下:
function Super(name) { this.name = name; this.colors = ["red", "blue"]; }; function Sub(age) { this.age = age; } Sub.prototype = new Super("Lee"); var instance = new Sub(20); instance.name; // Lee instance.age; // 20
我們通過(guò)讓Sub類(lèi)的原型指向Super類(lèi)的實(shí)例,實(shí)現(xiàn)了繼承,可以在instance上訪問(wèn)name和colors屬性。但是,其最大的問(wèn)題來(lái)自于共享數(shù)據(jù),如果實(shí)例1修改了colors屬性,那么實(shí)例2的colors屬性也會(huì)變化。另外,此時(shí)我們?cè)谧宇?lèi)上并不能傳遞父類(lèi)的參數(shù),限制性很大。
構(gòu)造函數(shù)繼承為了解決對(duì)象引用的問(wèn)題,我們調(diào)用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)繼承,保證每個(gè)實(shí)例擁有相同的父類(lèi)屬性,但值之間互不影響。實(shí)質(zhì)
function Super(name) { this.name = name; this.colors = ["red", "blue"]; this.sayName = function() { return this.name; } } function Sub() { Super.call(this, "Nicholas"); } var instance1 = new Sub(); var instance2 = new Sub(); instance1.colors.push("black"); instance1.colors; // ["red", "blue", "black"] instance2.colors; // ["red", "blue"]
此時(shí)我們通過(guò)改變父類(lèi)構(gòu)造函數(shù)的作用域就解決了引用對(duì)象的問(wèn)題,同時(shí)我們也可以向父類(lèi)傳遞參數(shù)了。但是,只用構(gòu)造函數(shù)就很難在定義方法時(shí)復(fù)用,現(xiàn)在我們創(chuàng)建所有實(shí)例時(shí)都要聲明一個(gè)sayName()的方法,而且此時(shí),子類(lèi)中看不到父類(lèi)的方法。
組合繼承為了復(fù)用方法,我們使用組合繼承的方式,即利用構(gòu)造函數(shù)繼承屬性,利用原型鏈繼承方法,融合它們的優(yōu)點(diǎn),避免缺陷,成為JS中最常用的繼承。
function Super(name) { this.name = name; this.colors = ["red", "blue"]; }; function Sub(name, age) { // 第二次調(diào)用 Super.call(this, name); this.age = age; } Super.prototype.sayName = function () { return this.name; }; // 第一次調(diào)用 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function () { return this.age; } var instance = new Sub("lee", 40); instance.sayName(); // lee instance.sayAge(); // 40
這時(shí)我們?nèi)种挥幸粋€(gè)函數(shù),不用再給每一個(gè)實(shí)例新建一個(gè),并且每個(gè)實(shí)例擁有相同的屬性,達(dá)到了我們想要的繼承。此時(shí)instanceof和isPrototypeOf()也能夠識(shí)別繼承創(chuàng)建的對(duì)象。
但是依然有一個(gè)不理想的地方是,我們會(huì)調(diào)用兩次父類(lèi)的構(gòu)造函數(shù),第一次在Sub的原型上設(shè)置了name和colors屬性,此時(shí)name的值是undefined;第二次調(diào)用在Sub的實(shí)例上新建了name和colors屬性,而這個(gè)實(shí)例屬性會(huì)屏蔽原型的同名屬性。所以這種繼承會(huì)出現(xiàn)兩組屬性,這并不是理想的方式,我們?cè)噲D來(lái)解決這個(gè)問(wèn)題。
我們先來(lái)看一個(gè)后面會(huì)用到的繼承,它根據(jù)已有的對(duì)象創(chuàng)建一個(gè)新對(duì)象。
function create(obj) { function F(){}; F.prototype = obj; return new F(); } var person = { name: "Nicholas", friends: ["Lee", "Luvy"] }; var anotherPerson = create(person); anotherPerson.name; // Nicholas anotherPerson.friends.push("Rob"); person.friends; // ["Lee", "Luvy", "Rob"]
也就是說(shuō)我們根據(jù)一個(gè)對(duì)象作為原型,直接生成了一個(gè)新的對(duì)象,其中的引用對(duì)象依然共用,但你同時(shí)也可以給其賦予新的屬性。
ES5規(guī)范化了這個(gè)原型繼承,新增了Object.create()方法,接收兩個(gè)參數(shù),第一個(gè)為原型對(duì)象,第二個(gè)為要混合進(jìn)新對(duì)象的屬性,格式與Object.defineProperties()相同。
Object.create(null, {name: {value: "Greg", enumerable: true}});寄生組合式繼承
function Super(name) { this.name = name; this.colors = ["red", "blue"]; }; function Sub(name, age) { Super.call(this, name); this.age = age; } Super.prototype.sayName = function () { return this.name; }; // 我們封裝其繼承過(guò)程 function inheritPrototype(Sub, Super) { // 以該對(duì)象為原型創(chuàng)建一個(gè)新對(duì)象 var prototype = Object.create(Super.prototype); prototype.constructor = Sub; Sub.prototype = prototype; } inheritPrototype(Sub, Super); Sub.prototype.sayAge = function () { return this.age; } var instance = new Sub("lee", 40); instance.sayName(); // lee instance.sayAge(); // 40
這種方式只調(diào)用了一次父類(lèi)構(gòu)造函數(shù),只在子類(lèi)上創(chuàng)建一次對(duì)象,同時(shí)保持原型鏈,還可以使用instanceof和isPrototypeOf()來(lái)判斷原型,是我們最理想的繼承方式。
Class類(lèi)ES6引進(jìn)了class關(guān)鍵字,用于創(chuàng)建類(lèi),這里的類(lèi)是作為ES5構(gòu)造函數(shù)和原型對(duì)象的語(yǔ)法糖存在的,其功能大部分都可以被ES5實(shí)現(xiàn),不過(guò)在語(yǔ)言層面上ES6也提供了部分支持。新的寫(xiě)法不過(guò)讓對(duì)象原型看起來(lái)更加清晰,更像面向?qū)ο蟮恼Z(yǔ)法而已。
我們先看一個(gè)具體的class寫(xiě)法:
//定義類(lèi) class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return "(" + this.x + ", " + this.y + ")"; } } var point = new Point(10, 10);
我們看到其中的constructor方法就是之前的構(gòu)造函數(shù),this就是之前的原型對(duì)象,toString()就是定義在原型上的方法,只能使用new關(guān)鍵字來(lái)新建實(shí)例。語(yǔ)法差別在于我們不需要function關(guān)鍵字和逗號(hào)分割符。其中,所有的方法都直接定義在原型上,注意所有的方法都不可枚舉。類(lèi)的內(nèi)部使用嚴(yán)格模式,并且不存在變量提升,其中的this指向類(lèi)的實(shí)例。
new是從構(gòu)造函數(shù)生成實(shí)例的命令。ES6 為new命令引入了一個(gè)new.target屬性,該屬性一般用在構(gòu)造函數(shù)之中,返回new命令作用于的那個(gè)構(gòu)造函數(shù)。如果構(gòu)造函數(shù)不是通過(guò)new命令調(diào)用的,new.target會(huì)返回undefined,因此這個(gè)屬性可以用來(lái)確定構(gòu)造函數(shù)是怎么調(diào)用的。
類(lèi)存在靜態(tài)方法,使用static關(guān)鍵字表示,其只能類(lèi)和繼承的子類(lèi)來(lái)進(jìn)行調(diào)用,不能被實(shí)例調(diào)用,也就是不能被實(shí)例繼承,所以我們稱(chēng)它為靜態(tài)方法。類(lèi)不存在內(nèi)部方法和內(nèi)部屬性。
class Foo { static classMethod() { return "hello"; } } Foo.classMethod() // "hello" var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function
類(lèi)通過(guò)extends關(guān)鍵字來(lái)實(shí)現(xiàn)繼承,在繼承的子類(lèi)的構(gòu)造函數(shù)里我們使用super關(guān)鍵字來(lái)表示對(duì)父類(lèi)構(gòu)造函數(shù)的引用;在靜態(tài)方法里,super指向父類(lèi);在其它函數(shù)體內(nèi),super表示對(duì)父類(lèi)原型屬性的引用。其中super必須在子類(lèi)的構(gòu)造函數(shù)體內(nèi)調(diào)用一次,因?yàn)槲覀冃枰{(diào)用時(shí)來(lái)綁定子類(lèi)的元素對(duì)象,否則會(huì)報(bào)錯(cuò)。
class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 調(diào)用父類(lèi)的constructor(x, y) this.color = color; } toString() { return this.color + " " + super.toString(); // 調(diào)用父類(lèi)的toString() } }參考資料
阮一峰 ES6 - class: http://es6.ruanyifeng.com/#do...
MDN文檔 - Object.create(): https://developer.mozilla.org...
深入理解原型對(duì)象和繼承: https://github.com/norfish/bl...
知乎 prototype和__proto__的區(qū)別: https://www.zhihu.com/questio...
Javascript高級(jí)程序設(shè)計(jì): 第四章(變量、作用域和內(nèi)存問(wèn)題)、第五章(引用類(lèi)型)、第六章(面向?qū)ο蟮某绦蛟O(shè)計(jì))
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/88756.html
摘要:是對(duì)象或者實(shí)例中內(nèi)置的,其指向的是產(chǎn)生該對(duì)象的對(duì)象的在瀏覽器中提供了讓我們可以訪問(wèn),通過(guò)的指向形成的一個(gè)鏈條,就稱(chēng)做原型鏈,原型鏈的整個(gè)鏈路是實(shí)例對(duì)象構(gòu)造函數(shù)的的。 原型和原型鏈 原型prototype,在創(chuàng)建新函數(shù)的時(shí)候,會(huì)自動(dòng)生成,而prototype中也會(huì)有一個(gè)constructor,回指創(chuàng)建該prototype的函數(shù)對(duì)象。 __proto__是對(duì)象或者實(shí)例中內(nèi)置的[[proto...
摘要:實(shí)現(xiàn)思路使用原型鏈實(shí)現(xiàn)對(duì)原型方法和方法的繼承,而通過(guò)借用構(gòu)造函數(shù)來(lái)實(shí)現(xiàn)對(duì)實(shí)例屬性的繼承。繼承屬性繼承方法以上代碼,構(gòu)造函數(shù)定義了兩個(gè)屬性和。 JS面向?qū)ο蟮某绦蛟O(shè)計(jì)之繼承的實(shí)現(xiàn)-組合繼承 前言:最近在細(xì)讀Javascript高級(jí)程序設(shè)計(jì),對(duì)于我而言,中文版,書(shū)中很多地方翻譯的差強(qiáng)人意,所以用自己所理解的,嘗試解讀下。如有紕漏或錯(cuò)誤,會(huì)非常感謝您的指出。文中絕大部分內(nèi)容引用自《Java...
摘要:由一個(gè)問(wèn)題引發(fā)的思考這個(gè)方法是從哪兒蹦出來(lái)的首先我們要清楚數(shù)組也是對(duì)象,而且是對(duì)象的實(shí)例也就是說(shuō),下面兩種形式是完全等價(jià)的只不過(guò)是一種字面量的寫(xiě)法,在深入淺出面向?qū)ο蠛驮透拍钇恼吕?,我們提到過(guò)類(lèi)會(huì)有一個(gè)屬性,而這個(gè)類(lèi)的實(shí)例可以通過(guò)屬性訪 1.由一個(gè)問(wèn)題引發(fā)的思考 let arr1 = [1, 2, 3] let arr2 = [4, 5, 6] arr1.c...
摘要:下面來(lái)看一個(gè)例子繼承屬性繼承方法在這個(gè)例子中構(gòu)造函數(shù)定義了兩個(gè)屬性和。組合繼承最大的問(wèn)題就是無(wú)論什么情況下都會(huì)調(diào)用兩次超類(lèi)型構(gòu)造函數(shù)一次是在創(chuàng)建子類(lèi)型原型的時(shí)候另一次是在子類(lèi)型構(gòu)造函數(shù)內(nèi)部。 組合繼承 組合繼承(combination inheritance),有時(shí)候也叫做偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)的技術(shù)組合到一塊,從而發(fā)揮二者之長(zhǎng)的一種繼承模式。其背后的思路是使用原型鏈...
閱讀 925·2021-11-22 11:59
閱讀 3314·2021-11-17 09:33
閱讀 2398·2021-09-29 09:34
閱讀 2041·2021-09-22 15:25
閱讀 2018·2019-08-30 15:55
閱讀 1391·2019-08-30 15:55
閱讀 604·2019-08-30 15:53
閱讀 3429·2019-08-29 13:55