全面分析javascript繼承的原理

來源:互聯網
上載者:User
本篇文章給大家帶來的內容是關於全面分析javascript繼承的原理 ,有一定的參考價值,有需要的朋友可以參考一下,希望對你有所協助。

繼承

我們知道JS是OO編程,自然少不了OO編程所擁有的特性,學習完原型之後,我們趁熱打鐵,來聊聊OO編程三大特性之一——繼承。

繼承這個詞應該比較好理解,我們耳熟能詳的,繼承財產,繼承家業等,他們的前提是有個繼承人,然後你是繼承者,這樣才有繼承而言。沒錯,JS中的繼承正如你所理解的一樣,也是成對出現的。
繼承就是將對象的屬性複製一份給需要繼承的對象

OO語言支援兩種繼承方式:介面繼承和實現繼承,其中介面繼承只繼承方法簽名,而實現繼承則繼承實際的方法。由於ECMAScript中的函數沒有簽名,因此無法實現介面繼承,只支援實現繼承,而繼承的主要方式,是通過原型鏈實現的,要理解原型鏈,首先要知道什麼是原型,不懂的小夥伴,可以看這篇javascript原型是什嗎?javascript原型的詳細解說

其實繼承說白了就是
①它上面必須有個父級
②且它擷取了這個父級的所有執行個體和方法

這裡普及一個小概念,上文提到的沒有簽名,第一次看這個字眼也不是很懂,搜尋了一下,覺得這個說法還是比較認可的。

沒有簽名
我們知道JS是弱類型語言,它的參數可以由0個或多個值的數組來表示,我們可以為JS函數具名引數,這個做法只是為了方便,但不是必須,也就是說,我們命不具名引數和傳不傳參數沒有必然聯絡,既可以具名引數,但不傳(此時預設為undefined),也可以不具名引數,但傳參數,這種寫法在JS中是合法的,反之,強型別語言,對這個要求就非常嚴格,定義了幾個參數就一定要傳幾個參數下去。具名引數這塊必須要求事先建立函數簽名,而將來的調用也必須與該簽名一致。(也就是說定義幾個參數就要傳幾個下去)**,而js沒有這些條條框框,解析器不會驗證具名引數,所以說js沒有簽名。

舉個例子

function JSNoSignature () {    console.log("first params" + arguments[0] + "," + "second params" + arguments[1]);}JSNoSignature ("hello", "world");

這個例子很明顯了。具名引數為空白,但我們依舊可以傳參,調用該方法。所謂的參數類型,參數個數,參數位置,出入參數,js統統不關心,它所有的值都被放到arguments中了,需要傳回值的話直接return,不用聲明,這就叫做js沒有簽名。

原型鏈

什麼是原型鏈呢?字面上也很好理解,就是將所有的原型串在一起就叫做原型鏈。當然這個解釋只是為了方便理解罷了,原型鏈是作為實現繼承的主要方法,其基本思想是利用原型一個參考型別繼承另一個參考型別的屬性和方法。我們知道每個建構函式都有一個原型對象,原型對象都包含一個指向建構函式的指標,而執行個體都包含一個指向原型的內部指標。此時,如果我們讓原型對象等於另外一個類型的執行個體,那麼此時的原型對象將包含一個指向另一個原型的指標,相應地,另一個原型中也包含著一個指向另一個建構函式的指標,這種周而復始的串連關係,就構成了執行個體與原型的鏈條,這就是原型鏈。

一句話說白了,就是執行個體→原型→執行個體→原型→執行個體... 串連下去就是原型鏈。

我覺得繼承就是原型鏈的一種表現形式

我們知道了原型鏈後,要知道他如何去使用,ECMA提供一套原型鏈的基本模式基本模式如下

原型鏈的基本模式

// 建立一個父類function FatherType(){    this.fatherName = '命名最頭痛';}FatherType.prototype.getFatherValue = function() {    return this.fatherName;}function ChildType(){    this.childName = 'George';}// 繼承了FatherType,即將一個執行個體賦值給函數原型,我們就說這個原型繼承了另一個函數執行個體// 將子類的原型指向這個父類的執行個體ChildType.prototype = new FatherType();ChildType.prototype.getChildValue = function() {    return this.childName;}let instance = new ChildType();console.log(instance.getFatherValue()); // 命名最頭痛

調用instance.getFatherValue()時會經曆三個搜尋步驟
①搜尋執行個體
②搜尋ChildType.prototype
③搜尋FatherType.prototype,此時在這步找到該方法,在找不到屬性或方法的情況下,搜尋過程總是要一環一環地向前行到原型鏈末端才會停下來。

此時的原型鏈是instance → ChildType.prototype → FatherType.prototype
執行instance.getFatherValue()後,getFatherValue裡面的this是ChildType,此時ChildType會根據原型鏈去找fatherName屬性,最終在FatherType中找到。
此時instance.constructor是指向FatherType的

預設的原型

所有的參考型別預設都繼承了Object,而這個繼承也是通過原型鏈實現的,因此,所有函數的預設原型都是Object的執行個體,因此預設原型都會包含一個內部指標,指向Object。prototype,這也就是所有自訂類型都會繼承toString(),valueOf()等預設方法的根本原因。
Array類型也是繼承了Object類型的。
因此,我們可以總結一下,在原型鏈的最頂端就是Object類型,所有的函數預設都繼承了Object中的屬性。

原型和執行個體關係的確認

isPrototypeOf方法

在javascript原型是什嗎?javascript原型的詳細解說中我們有提到過isPrototypeOf方法可以用於判斷這個執行個體的指標是否指向這個原型,這一章我們學習了原型鏈,這裡做個補充,按照原型鏈的先後順序,isPrototypeOf方法可以用於判斷這個執行個體是否屬於這個原型的。

依舊用上面那個例子// 注意,這裡用的是原型,Object.prototype,FatherType.prototype,ChildType.prototypeconsole.log(Object.prototype.isPrototypeOf(instance)); // trueconsole.log(FatherType.prototype.isPrototypeOf(instance)); // trueconsole.log(ChildType.prototype.isPrototypeOf(instance)); // true

下面再介紹另一種方法,通過instanceof操作符,也可以確定原型和執行個體之間的關係

instanceof操作符

instanceof操作符是用來測試原型鏈中的建構函式是否有這個執行個體

function FatherType(){    this.fatherName = '命名最頭痛';}FatherType.prototype.getFatherValue = function() {    return this.fatherName;}function ChildType(){    this.childName = 'George';}// 繼承了FatherTypeChildType.prototype = new FatherType();// 建立執行個體let instance = new ChildType();// 為ChildType原型上添加新方法,要放在繼承FatherType之後,這是因為new FatherType()會將ChildType原型上添加的新方法全部覆蓋掉ChildType.prototype.getChildValue = function() {    return this.childName;}// 此時getFatherValue被重寫了ChildType.prototype.getFatherValue = function() {    return true}console.log(instance.getFatherValue()); // true

②通過原型鏈實現繼承時,不能使用對象字面量建立原型方法,因為這樣會重寫原型鏈。這部分的例子和解釋在javascript原型是什嗎?javascript原型的詳細解說中已經表述過了。一樣的道理,只不過把原型換成了原型鏈罷了。

原型鏈的bug

原型鏈雖然強大,可以用它來實現繼承,但是也是存在bug的,它最大的bug來自包含參考型別值的原型。也就是說原型鏈上面定義的原型屬性會被所有的執行個體共用。
它還有另外一個bug,即在建立子類型的執行個體時,不能向父類型(超類型)的建構函式中傳遞參數。或者說沒有辦法在不影響所有對象執行個體的情況下,給超類型的建構函式傳遞參數。
基於以上這兩個原因,實踐過程中很少會單獨使用原型鏈

借用建構函式

其設計思想就是在子類型建構函式的內部調用父類(超類)建構函式。
由於函數只不過是在特定環境中執行代碼的對象,因此通過apply()和call()方法也可以在(將來)新建立的對象上執行建構函式。

function FatherType() {  this.name = 'George';} function ChildType() {  //通過call方法改變this的指向,此時FatherType中的this指的是ChildType,相當於在建構函式中定義自己的屬性。  FatherType.call(this);}let instance1 = new ChildType(); instance1.name = '命名最頭痛';console.log(instance1.name); // '命名最頭痛'let instance2 = new ChildType();console.log(instance2.name); // George

通過上述方法很好解決了原型屬性共用問題,此外,既然是一個函數,它也能傳相應的參數,因此也能實現在子類型建構函式中向超類型建構函式傳遞參數。

function FatherType(name){  this.name = name}function ChildType(){  FatherType.call(this, "George");  this.age = 18}let instance = new ChildType();console.log(instance.name);  // Georgeconsole.log(instance.age);   // 18

借用建構函式的問題
借用建構函式,方法都在建構函式中定義,那麼函數的複用就無從談起,而且在父類(超類型)的原型定義的方法,對子類型而言也是不可見的,結果所有類型都只能使用建構函式模式。

組合繼承

組合繼承也叫偽經典繼承,其設計思想是將原型鏈和借用建構函式的技術組合到一塊,發揮二者之長的一種繼承模式,其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用建構函式來實現對執行個體屬性的繼承,這樣既通過在原型上定義方法實現了函數複用,又能夠保證每個執行個體都有它自己的屬性。

function FatherType(name){  this.name = name  this.colors = ['red', 'blue', 'green']}FatherType.prototype.sayName = function() {  console.log(this.name)}// 借用建構函式實現對執行個體的繼承function ChildType(name, age){  // 使用call方法繼承FatherType中的屬性  FatherType.call(this, name);  this.age = age}// 利用原型鏈實現對原型屬性和方法的繼承ChildType.prototype = new FatherType(); //將FatherType的執行個體賦值給ChildType原型ChildType.prototype.constructor = ChildType; // 讓ChildType的原型指向ChildType函數ChildType.prototype.sayAge = function(){  console.log(this.age)}let instance1 = new ChildType('命名最頭痛', 18);instance1.colors.push('black');console.log(instance1.colors);         // 'red, blue, green, black'instance1.sayName();instance1.sayAge();var instance2 = new ChildType('命名最頭痛', 18);console.log(instance2.colors);         // 'red, blue, green'instance2.sayName();                   // '命名最頭痛'instance2.sayAge();                    // 18

組合繼承方式避免了原型鏈和借用建構函式的缺陷,是JS中常用的繼承方式。

原型鏈繼承

原型鏈繼承沒有使用嚴格意義上的建構函式,其思想是基於已有的對象建立新對象

// 此object函數返回一個執行個體, 實際上object()對傳入其中的對象執行了一次淺複製.function object(o) {  function F() {}  // 建立一個臨時建構函式  F.prototype = o; // 將傳入的對象作為建構函式的原型  return new F();  // 返回這個臨時建構函式的新執行個體}let demo = {  name: 'George',  like: ['apple', 'dog']}let demo1 = object(demo);demo1.name = '命名';     // 基本類型demo1.like.push('cat'); // 參考型別共用一個記憶體位址let demo2 = object(demo);demo2.name = '頭痛';    // 基本類型demo2.like.push('chicken') // 參考型別共用一個記憶體位址console.log(demo.name) // Georgeconsole.log(demo.like) // ["apple", "dog", "cat", "chicken"]

原型鏈繼承的前提是必須要有一個對象可以作為另一個對象的基礎。通過object()函數產生新對象後,再根據需求對新對象進行修改即可。 由於新對象(demo1, demo2)是將傳入對象(demo)作為原型的,因此當涉及到參考型別時,他們會共用一個記憶體位址,參考型別會被所有執行個體所共用,實際上相當於建立了demo對象的兩個副本。

Object.create()方法

ECMA5中新增Object.create()方法正常化了原型式繼承。該方法接收兩個參數
①基礎對象,這個參數的實際作用是定義了模板對象中有的屬性,就像上面例子中的demo,只有一個參數情況下,Object.create()與上例子中的object相同
②這個是選擇性參數,一個為基礎對象定義額外屬性的對象, 該對象的書寫格式與Object.defineProperties()方法的第二個參數格式相同,每個屬性都是通過自己的描述符定義的,以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬性。

// 只有一個參數var demoObj = {  name: 'George',  like: ['apple', 'dog', 'cat']}let demo1Obj = Object.create(demoObj);demo1Obj.name = '命名';demo1Obj.like.push('banana');let demo2Obj = Object.create(demoObj);demo2Obj.name = '頭痛';demo2Obj.like.push('walk');console.log(demoObj.like) //["apple", "dog", "cat", "banana", "walk"]// 兩個參數var demoObj = {  name: 'George',  like: ['apple', 'dog', 'cat']}let demo1Obj = Object.create(demoObj, {  name: {    value:'命名'  },  like:{    value: ['monkey']  },  new_val: {    value: 'new_val'  }});console.log(demoObj.name) // Georgeconsole.log(demo1Obj.name) // 命名console.log(demo1Obj.like) // ["monkey"]console.log(demo1Obj.new_val) // new_valconsole.log(Object.getOwnPropertyDescriptor(demo1Obj,'new_val')) // {value: "new_val", writable: false, enumerable: false, configurable: false}

如果只想讓一個對象與另一個對象保持類型的情況下,原型式繼承是完全可以勝任的,不過要注意的是,參考型別值的屬性始終都會共用相應的值。

寄生式繼承

寄生式繼承是與原型式繼承緊密相關的一種思路,其設計思想與寄生建構函式和原廠模式類似,即建立一個僅用於封裝繼承過程的函數,該函數內部以某種方式來增強對象,最後返回一個對象。

// 這個函數所返回的對象,既有original的所有屬性和方法,也有自己的sayHello方法function createAnother(original) {  let clone = Object.create(original);  clone.sayHello = function(){                console.log('HELLO WORLD')  }  return clone;}let person = {  name: 'George',  foods: ['apple', 'banana']}let anotherPerson = createAnother(person);anotherPerson.sayHello();  // HELLO WORLD

使用寄生式繼承來為對象添加函數,會由於不能做到函數複用而降低效率,這一點與建構函式模式類似。

寄生組合式繼承

所謂寄生組合式繼承,即通過借用建構函式來繼承屬性,通過原型鏈的混合形式來繼承方法。其背後思想:不必為了指定子類型的原型而調用超類型的建構函式,我們所需要的無非就是超類型原型的一個副本而已。說白了就是使用寄生式繼承來繼承超類型的原型,然後再將結果指定給子類型的原型。

function inheritPrototype(childType, fatherType){  let fatherObj = Object.create(fatherType.prototype);  // 建立對象  fatherObj.constructor = childType;   // 彌補重寫原型而失去的預設constructor屬性  childType.prototype = fatherObj;     // 指定對象}

上例是寄生組合式繼承最簡單的形式,這個函數接受兩個參數:子類型建構函式和超類型建構函式,在函數內部,①建立了父類型原型的一個副本,②為建立的副本添加constructor屬性,從而彌補因重寫原型而失去的預設的constructor屬性。③將新建立的對象(即副本)賦值給子類型的原型。

function FatherType(name){  this.name = name;  this.foods = ['apple', 'banana'];}FatherType.prototype.sayName = function(){  console.log(this.name)}function ChildType(name, age){  FatherType.call(this, name);  this.age = age;}inheritPrototype(ChildType, FatherType);ChildType.prototype.sayAge = function(){  console.log(this.age)}

總結

JS繼承的主要方式是通過原型鏈實現的

執行個體-原型-執行個體-原型...無限連結下去就是原型鏈

所有參考型別的預設原型都是Object

instanceof操作符和isPrototypeOf方法都可以用於判斷執行個體與原型的關係,其區別是,前者用的是原型,後者用的是建構函式

給原型添加方法的代碼一定要放在繼承之後,這是因為,在繼承的時候被繼承者會覆蓋掉繼承者原型上的所有方法

Object.create()方法用於建立一個新對象,其屬性會放置在該對象的原型上

繼承有6種方式,分別是原型鏈,借用建構函式,組合繼承,原型式繼承,寄生式繼承和寄生組合式繼承

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.