各種實現js繼承的方法總結

来源:https://www.cnblogs.com/Yellow-ice/archive/2019/03/04/10473176.html
-Advertisement-
Play Games

昨天主要介紹了原型,在js中,原型,原型鏈和繼承是三個很重要的概念,而這幾個概念也是面試中經常會被問到的問題,今天,就把昨天還沒總結的原型鏈和繼承繼續做一個整理,希望大家一起學習,一起進步呀O(∩_∩)O 一、原型鏈 學過java的同學應該都知道,繼承是java的重要特點之一,許多面向對象的語言都支 ...


  昨天主要介紹了原型,在js中,原型,原型鏈和繼承是三個很重要的概念,而這幾個概念也是面試中經常會被問到的問題,今天,就把昨天還沒總結的原型鏈和繼承繼續做一個整理,希望大家一起學習,一起進步呀O(∩_∩)O

 

一、原型鏈

  學過java的同學應該都知道,繼承是java的重要特點之一,許多面向對象的語言都支持兩種繼承方式:介面繼承和實現繼承,介面繼承只繼承方法簽名,而實現繼承則繼承實際的方法,在js中,由於函數沒有簽名,因此支持實現繼承,而實現繼承主要是依靠原型鏈來實現的,那麼,什麼是原型鏈呢?

  首先,我們先來回顧一下構造函數,原型和實例之間的關係

  當我們創建一個構造函數時,構造函數會獲得一個prototype屬性,該屬性是一個指針,指向一個原型對象,原型對象包含一個constructor屬性,該屬性也是一個指針,指向構造函數,而當我們創建構造函數的實例時,該實例其實會獲得一個[[Prototype]]屬性,指向原型對象

function SubType() {}

var instance = new SubType();

   比如上面的代碼,其中,SubType是構造函數,SubType.prototype是原型對象,instance是實例,這三者的關係可以用下麵的圖表示

   而這個時候呢,如果我們讓原型對象等於另一個構造函數的實例,此時的原型對象就會獲得一個[[Prototype]]屬性,該屬性會指向另一個原型對象,如果另一個原型對象又是另一個構造函數的實例,這個原型對象又會獲得一個[[Prototype]]屬性,該屬性又會指向另一個原型對象,如此層層遞進,就構成了實例與原型的鏈條,這就是原型鏈

  我們再看下上面的例子,如果這個時候,我們讓SubType.prototype是另一個構造函數的實例,此時會怎麼樣呢?

function SuperType() {}

function SubType() {}

SubType.prototype = new SuperType();    

var instance = new SubType();

   上面的代碼中,我們先是讓SubType繼承了SuperType,接著創建出SubType的實例instance,因此,instance可以訪問SubType和SuperType原型上的屬性和方法,也就是實現了繼承,繼承關係我們可以用下麵的圖說明

  最後,要提醒大家的是,所有引用類型預設都繼承了Object,這個繼承也是通過原型鏈實現的,因此,其實原型鏈的頂層就是Object的原型對象啦

 

 二、繼承

  上面我們弄清了原型鏈,接下來主要就介紹一些經常會用到的繼承方法,具體要用哪一種,還是需要依情況而定的

1、原型鏈繼承

  最常見的繼承方法就是使用原型鏈實現繼承啦,也就是我們上面所介紹的,接下來,還是看一個實際的例子把

function SuperType() {
    this.property = true;  
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
}
function SubType() {
    ths.subproperty = true;
}
SubType.prototype = new SuperType();      //  實現繼承
SubType.prototype.getSubValue = function() {
    return this.subprototype;
}

var instance = new SubType();
console.log(instance.getSuperValue());      //  true

  上面的例子中,我們沒有使用SubType預設提供的原型,而是給它換了一個新原型,這個新原型就是SuperType的實例,因此,新原型具有作為SuperType實例所擁有的全部實現和方法,並且指向SuperType的原型,因此,instance實例具有subproperty屬性,SubType.prototype具有property屬性,值為true,並且擁有getSubValue方法,而SuperType擁有getSuperValue方法

  當調用instance的getSuperValue()方法時,因此在instance實例上找不到該方法,就會順著原型鏈先找到SubType.prototype,還是找不到該方法,繼續順著原型鏈找到SuperType.prototype,終於找到getSuperValue,就調用了該函數,而該函數返回property,該值的查找也是同樣的道理,會在SubType.prototype中找到該屬性,值為true,所以顯示true

  存在的問題:通過原型鏈實現繼承時,原型實際上會變成另一個類型實例,而原先的實例屬性也會變成原型屬性,如果該屬性為引用類型時,所有的實例都會共用該屬性,一個實例修改了該屬性,其它實例也會發生變化,同時,在創建子類型時,我們也不能向超類型的構造函數中傳遞參數

 

2、借用構造函數

  為瞭解決原型中包含引用類型值所帶來的問題,開發人員開始使用借用構造函數的技術實現繼承,該方法主要是通過apply()和call()方法,在子類型構造函數的內部調用超類型構造函數,從而解決該問題

function SuperType() {
    this.colors = ["red","blue","green"]
}
function SubType() {
    SuperType.call(this);      //  實現繼承
}
var instance1 = new SubType();
var instance2  = new SubType();
instance2.colors.push("black");
console.log(instance1.colors");      //  red,blue,green
console.log(instance2.colors");      //  red,blue,green,black

   在上面的例子中,如果我們使用原型鏈繼承,那麼instance1和instance2將會共用colors屬性,因為colors屬性存在於SubType.prototype中,而上面我們使用了借用構造函數繼承,通過使用call()方法,我們實際上是在新創建的SubType實例的環境下調用了SuperType的構造函數,因此,colors屬性是分別存在instance1和instance2實例中的,修改其中一個不會影響另一個

  使用這個方法,我們還可以在子類型構造函數中向超類型構造函數傳遞參數

function SuperType(name) {
    this.name = name;
}
function SubType() {
    SuperType.call(this,"Nicholas");
    this.age = 29;
}

var instance = new SubType();
console.log(instance.name);      //  Nicholas
console.log(instance.age);         //  29

   優點:解決了原型鏈繼承中引用類型的共用問題,同時可以在子類型構造函數中向超類型構造函數傳遞參數

  缺點:定義方法時,將會在每個實例上都會重新定義,不能實現函數的復用

 

3、組合繼承

  組合繼承主要是將原型鏈和借用構造函數的技術組合到一塊,從而發貨兩者之長的一種繼承模式,主要是使用原型鏈實現對原型屬性和方法的基礎,通過借用構造函數實現對實例屬性的基礎,這樣,可以通過在原型上定義方法實現函數的復用,又能夠保證每個實例都有自己的屬性

function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}
function SubType(name,age) {
    SuperType.call(this,name);
    this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    console.log(this.age);
}

var instance1 = new SubType("Nicholas", 29);
var instance2 =new SubType("Greg", 27);
instance1.colors.push("black");
console.log(instance1.colors);       //  red,blue,green,black
console.log(instance2.colors);       //  red,blue,green
instance1.sayName();                  //  Nicholas
instance2.sayName();                  //  29
instance1.sayAge();                     //  Greg
instance2.sayAge();                     //  27  

  組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,現在已經成為js中最常用的繼承方法

  缺點:無論什麼情況下,都會調用兩次超類型構造函數,一次是在創建子類型的時候,另一次是在子類型構造函數內部,子類型最終會包含超類型對象的全部實例屬性,但是需要在調用子類型構造函數時重寫這些屬性

 

4、原型式繼承

  原型式繼承主要的藉助原型可以基於已有的對象創建新的對象,基本思想就是創建一個臨時性的構造函數,然後將傳入的對象作為這個構造函數的原型,最後返回這個臨時類型的一個新實例

function Object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

  從上面的例子我們可以看出,如果我們想創建一個對象,讓它繼承另一個對象的話,就可以將要被繼承的對象當做o傳遞到Object函數裡面去,Object函數裡面返回的將會是一個新的實例,並且這個實例繼承了o對象

  其實,如果我們要使用原型式繼承的話,可以直接通過Object.create()方法來實現,這個方法接收兩個參數,第一個參數是用作新對象原型的對象,第二個參數是一個為新對象定義額外屬性的對象,一般來說,第二個參數可以省略

var person = {
    name: "Nicholas",
    friends: ["Shelby","Court","Van"]
}
var anotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
console.log(anotherPerson.name);      //  Greg

      上面的例子中,我們讓anotherPerson繼承了person,其中,friends作為引用類型,將會被所有繼承該對象的對象所共用,而通過傳入第二個參數,我們可以定義額外的屬性,修改person中的原有信息

  缺點:原型式繼承中包含引用類型的屬性始終都會共用相應的值

 

5、寄生式繼承

  寄生式繼承其實和我們前面說的創建對象方法中的寄生構造函數和工程模式很像,創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方法來增強對象,最後再返回該對象

function createAnother(original) {
    var clone = Object(original);      //  通過調用函數創建一個新對象
    clone.sayHi = function() {
        console.log("hi");
    }
    return clone;
}

  我們其實可以把寄生式繼承看做是傳進去一個對象,然後對該對象進行一定的加工,也就是增加一些方法來增強該對象,然後再返回一個包含新方法的對象的一個過程

var person = {
    name: "Nicholas",
    friends:["Shelby","Court","Van"]
}
var anotherPerson = createAnother(person);
anotherPerson.sayHi();      //  hi

  從上面的代碼中我們可以看出,原來person是沒有包含任何方法的,而通過將person傳進去createAnother方法中進行加工,返回的新對象就包含了一個新的方法

  缺點:不能實現函數的復用

 

6、寄生組合式繼承

  組合繼承是js中最經常用到的一種繼承方法,而我們前面也已經說了組合繼承的缺點,組合繼承需要調用兩次超類型構造函數,一次是在創建子類型原型的時候,另一次是在子類型構造函數內部,子類型最終會包含超類型對象的全部實例屬性,但是我們不得不在調用子類型構造函數時重寫這些屬性

function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}
function SubType(name,age) {
    SuperType.call(this,name);      //  第二次調用超類型構造函數
    this.age = age;
}
SubType.prototype = new SuperType();  //  第一次調用超類型構造函數
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    console.log(this.age);
}

  上面的代碼中有兩次調用了超類型構造函數,那兩次調用會帶來什麼結果呢?結果就是在SubType.prototype和SubType的實例上都會創建name和colors屬性,最後SubType的實例上的name和colors屬性會屏蔽掉SubType.prototype上的name和colors屬性

  寄生組合式繼承就是可以解決上面這個問題,寄生組合式繼承主要通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法,其實就是不必為了指定子類型的原型而調用超類型的構造函數,只需要超類型原型的一個副本就可以了

function inheritPrototype(subType,SuperType) {
    var prototype = Object(SuperType);      //  創建對象
    prototype.constructor = subType;          //  增強對象
    subType.prototype = prototype;            //  指定對象
}

  在上面的例子中,第一步創建了超類型原型的一個副本,第二步為創建的副本添加constructor屬性,從而彌補因重寫原型而失去的預設的constructor屬性,最後一步將副本也就是新對象賦值給子類型的原型,因此,我們可以用這個函數去替換前面說到為子類型原型賦值的語句

function SuperType(name) {
    this.name = name;
    this.colors = ["red","blue","green"]
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}
function SubType(name,age) {
    SuperType.call(this,name);
    this.age = age;
}
inheritPrototype(SubType,SuperType);
SubType.prototype.sayAge = function() {
    console.log(this.age);
}

  寄生組合式繼承只調用了一次SuperType構造函數,避免了在SubType.prototype上面創建的不必要的,多餘的屬性,現在也是很多人使用這種方法實現繼承啦

 

7、es6中的繼承

  我們在前面創建對象中也提到了es6中可以使用Class來創建對象,而同樣的道理,在es6中,也新增加了extends實現Class的繼承,Class 可以通過extends關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多

class Point {
}

class ColorPoint extends Point {
}

  上面這個例子中可以實現ColorPoint類繼承Point類,這種簡潔的語法確實比我們上面介紹的那些方法要簡潔的好多呀

  但是呢,使用extends實現繼承的時候,還是有幾點需要註意的問題,子類在繼承父類的時候,子類必須在constructor方法中調用super方法,否則新建實例時會報錯,這是因為子類自己的this對象,必須先通過父類的構造函數完成塑造,得到與父類同樣的實例屬性和方法,然後再對其進行加工,加上子類自己的實例屬性和方法,如果不調用super方法,子類就得不到this對象

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正確
  }
}

  上面代碼中,子類的constructor方法沒有調用super之前,就使用this關鍵字,結果報錯,而放在super方法之後就是正確的,正確的繼承之後,我們就可以創建實例了

let cp = new ColorPoint(25, 8, 'green');

cp instanceof ColorPoint // true
cp instanceof Point // true

  對於es6的繼承,如果大家想繼續瞭解,可以進行更進一步的學習

 

  今天就介紹到這裡啦,對於js的繼承方法,不知道大家是不是更瞭解了呢

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 解決的方法就是將用戶名改成system ...
  • 理解SQL語句中各部分的含義,學會使用MySQL語句管理資料庫。 ...
  • 分類二級分類框架名稱簡介Star 數最近更新 UI 刷新 SmartRefreshLayout 智能下拉刷新框架 14k 18天 UI 刷新 Android-PullToRefresh 比較早的一款下拉刷新框架 8.7k 5年 UI 刷新 android-Ultra-Pull-To-Refresh ...
  • arc4random 不需要初始種子(用 srand 或 srandom),使它更加容易使用。arc4random 範圍可達 0x100000000 (4294967296),而 rand 和 random 的上限在 RAND_MAX = 0x7fffffff (2147483647)。rand 經 ...
  • ""中文編程"知乎專欄原文" 第一個issue: "Error compiling template if using unicode naming as v for alias · Issue 6971 · vuejs/vue" 多謝尤大 "搞定" , 貌似是把標識符的正則表達式匹配檢測去掉了. ...
  • 其實js的this指向很簡單。我們記住下麵3種情況。 當我們執行fn()的時候,這個普通函數中的this指向到底是什麼?答案就是指向的是瀏覽器中的window.(這裡說明,這裡實在瀏覽器下,如果是node環境不是)。 fn類(這裡使用了構造函數new方式生成,這個時候函數fn可以看做是一個類),生成 ...
  • 最近公司的新項目。前端樣式採用的螞蟻金服的antDesign。 比較喜歡antDesign、BootStrap一類簡約大方的前端樣式庫。 但是在頁面佈局上、包括一些選擇框。預設的scroll樣式簡直醜爆。 遂度娘資料,通過css3進行修改。 ::-webkit-scrollbar 滾動條整體部分 : ...
  • 首先設置meta屬性,如下代碼: 使用如下代碼就能實現移動端的適配: 100vw相當於瀏覽器的window.innerWidth,是瀏覽器的內部寬度,註意,滾動條寬度也計算在內!那麼1vw就是表示1%的屏幕寬度。 其中的13.33333333vw是怎麼來的呢?就是你的設計稿是750px,那麼設計稿的 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...