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