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