整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          一篇文章告訴你JavaScript 如何實現繼承

          景簡介

          JavaScript 在編程語言界是個特殊種類,它和其他編程語言很不一樣,JavaScript 可以在運行的時候動態地改變某個變量的類型。

          比如你永遠也沒法想到像isTimeout這樣一個變量可以存在多少種類型,除了布爾值true和false,它還可能是undefined、1和0、一個時間戳,甚至一個對象。

          如果代碼跑異常,打開瀏覽器,開始斷點調試,發現InfoList這個變量第一次被賦值的時候是個數組:

          [{name: 'test1', value: '11'}, {name: 'test2', value: '22'}]

          過了一會竟然變成了一個對象:

          {test1:'11', test2: '22'}

          除了變量可以在運行時被賦值為任何類型以外,JavaScript 中也能實現繼承,但它不像 Java、C++、C# 這些編程語言一樣基于類來實現繼承,而是基于原型進行繼承。

          這是因為 JavaScript 中有個特殊的存在:對象。每個對象還都擁有一個原型對象,并可以從中繼承方法和屬性。


          提到對象和原型,有如下問題:

          1. JavaScript 的函數怎么也是個對象?
          2. proto和prototype到底是啥關系?
          3. JavaScript 中對象是怎么實現繼承的?
          4. JavaScript 是怎么訪問對象的方法和屬性的?


          原型對象和對象的關系

          在 JavaScript 中,對象由一組或多組的屬性和值組成:

          {
            key1: value1,
            key2: value2,
            key3: value3,
          }

          在 JavaScript 中,對象的用途很是廣泛,因為它的值既可以是原始類型(number、string、boolean、null、undefined、bigint和symbol),還可以是對象和函數。


          不管是對象,還是函數和數組,它們都是Object的實例,也就是說在 JavaScript 中,除了原始類型以外,其余都是對象。

          這也就解答了問題1:JavaScript 的函數怎么也是個對象?

          在 JavaScript 中,函數也是一種特殊的對象,它同樣擁有屬性和值。所有的函數會有一個特別的屬性prototype,該屬性的值是一個對象,這個對象便是我們常說的“原型對象”。

          我們可以在控制臺打印一下這個屬性:

          function Person(name) {
            this.name = name;
          }
          console.log(Person.prototype);

          打印結果顯示為:

          可以看到,該原型對象有兩個屬性:constructor和proto

          到這里,我們仿佛看到疑惑 “2:proto和prototype到底是啥關系?”的答案要出現了。在 JavaScript 中,proto屬性指向對象的原型對象,對于函數來說,它的原型對象便是prototype。函數的原型對象prototype有以下特點:

          • 默認情況下,所有函數的原型對象(prototype)都擁有constructor屬性,該屬性指向與之關聯的構造函數,在這里構造函數便是Person函數;
          • Person函數的原型對象(prototype)同樣擁有自己的原型對象,用proto屬性表示。前面說過,函數是Object的實例,因此Person.prototype的原型對象為Object.prototype。

          我們可以用這樣一張圖來描述prototype、proto和constructor三個屬性的關系:

          從這個圖中,我們可以找到這樣的關系:

          • 在 JavaScript 中,proto屬性指向對象的原型對象;
          • 對于函數來說,每個函數都有一個prototype屬性,該屬性為該函數的原型對象;


          使用 prototype 和 proto 實現繼承

          對象之所以使用廣泛,是因為對象的屬性值可以為任意類型。因此,屬性的值同樣可以為另外一個對象,這意味著 JavaScript 可以這么做:通過將對象 A 的proto屬性賦值為對象 B,即:

          A.__proto__ = B

          此時使用A.proto便可以訪問 B 的屬性和方法。

          這樣,JavaScript 可以在兩個對象之間創建一個關聯,使得一個對象可以訪問另一個對象的屬性和方法,從而實現了繼承;


          使用prototype和proto實現繼承

          以Person為例,當我們使用new Person()創建對象時,JavaScript 就會創建構造函數Person的實例,比如這里我們創建了一個叫“zhangsan”的Person:

          var zhangsan = new Person("zhangsan");

          上述這段代碼在運行時,JavaScript 引擎通過將Person的原型對象prototype賦值給實例對象zhangsan的proto屬性,實現了zhangsan對Person的繼承,即執行了以下代碼:

          //JavaScript 引擎執行了以下代碼
          var zhangsan = {};
          zhangsan.__proto__ = Person.prototype;
          Person.call(zhangsan, "zhangsan");

          我們來打印一下zhangsan實例:

          console.log(zhangsan)

          結果如下圖所示:




          可以看到,zhangsan作為Person的實例對象,它的proto指向了Person的原型對象,即Person.prototype。

          這時,我們再補充下上圖中的關系:




          從這幅圖中,我們可以清晰地看到構造函數和constructor屬性、原型對象(prototype)和proto、實例對象之間的關系,這是很多容易混淆。根據這張圖,我們可以得到以下的關系:

          1. 每個函數的原型對象(Person.prototype)都擁有constructor屬性,指向該原型對象的構造函數(Person);
          2. 使用構造函數(new Person())可以創建對象,創建的對象稱為實例對象(lily);
          3. 實例對象通過將proto屬性指向構造函數的原型對象(Person.prototype),實現了該原型對象的繼承。

          那么現在,關于proto和prototype的關系,我們可以得到這樣的答案:

          • 每個對象都有proto屬性來標識自己所繼承的原型對象,但只有函數才有prototype屬性;
          • 對于函數來說,每個函數都有一個prototype屬性,該屬性為該函數的原型對象;
          • 通過將實例對象的proto屬性賦值為其構造函數的原型對象prototype,JavaScript 可以使用構造函數創建對象的方式,來實現繼承。


          所以一個對象可通過proto訪問原型對象上的屬性和方法,而該原型同樣也可通過proto訪問它的原型對象,這樣我們就在實例和原型之間構造了一條原型鏈。紅色線條所示:



          通過原型鏈訪問對象的方法和屬性

          當 JavaScript 試圖訪問一個對象的屬性時,會基于原型鏈進行查找。查找的過程是這樣的:

          1. 首先會優先在該對象上搜尋。如果找不到,還會依次層層向上搜索該對象的原型對象、該對象的原型對象的原型對象等(套娃告警);
          2. JavaScript 中的所有對象都來自Object,Object.prototype.proto === null。null沒有原型,并作為這個原型鏈中的最后一個環節;
          3. JavaScript 會遍歷訪問對象的整個原型鏈,如果最終依然找不到,此時會認為該對象的屬性值為undefined。

          我們可以通過一個具體的例子,來表示基于原型鏈的對象屬性的訪問過程,在該例子中我們構建了一條對象的原型鏈,并進行屬性值的訪問:

          var o = {a: 1, b: 2}; // 讓我們假設我們有一個對象 o, 其有自己的屬性 a 和 b:
          o.__proto__ = {b: 3, c: 4}; // o 的原型 o.__proto__有屬性 b 和 c:

          當我們在獲取屬性值的時候,就會觸發原型鏈的查找:

          console.log(o.a); // o.a => 1
          console.log(o.b); // o.b => 2
          console.log(o.c); // o.c => o.__proto__.c => 4
          console.log(o.d); // o.c => o.__proto__.d => o.__proto__.__proto__ == null => undefined

          綜上,整個原型鏈如下:

          {a:1, b:2} ---> {b:3, c:4} ---> null, // 這就是原型鏈的末尾,即 null


          可以看到,當我們對對象進行屬性值的獲取時,會觸發該對象的原型鏈查找過程。

          既然 JavaScript 中會通過遍歷原型鏈來訪問對象的屬性,那么我們可以通過原型鏈的方式進行繼承。

          也就是說,可以通過原型鏈去訪問原型對象上的屬性和方法,我們不需要在創建對象的時候給該對象重新賦值/添加方法。比如,我們調用lily.toString()時,JavaScript 引擎會進行以下操作:

          1. 先檢查lily對象是否具有可用的toString()方法;
          2. 如果沒有,則``檢查lily的原型對象(Person.prototype)是否具有可用的toString()方法;
          3. 如果也沒有,則檢查Person()構造函數的prototype屬性所指向的對象的原型對象(即Object.prototype)是否具有可用的toString()方法,于是該方法被調用。

          由于通過原型鏈進行屬性的查找,需要層層遍歷各個原型對象,此時可能會帶來性能問題:

          1. 當試圖訪問不存在的屬性時,會遍歷整個原型鏈;
          2. 在原型鏈上查找屬性比較耗時,對性能有副作用,這在性能要求苛刻的情況下很重要。

          因此,我們在設計對象的時候,需要注意代碼中原型鏈的長度。當原型鏈過長時,可以選擇進行分解,來避免可能帶來的性能問題。


          其他方式實現繼承

          除了通過原型鏈的方式實現 JavaScript 繼承,JavaScript 中實現繼承的方式還包括經典繼承(盜用構造函數)、組合繼承、原型式繼承、寄生式繼承,等等。

          • 原型鏈繼承方式中引用類型的屬性被所有實例共享,無法做到實例私有;
          • 經典繼承方式可以實現實例屬性私有,但要求類型只能通過構造函數來定義;
          • 組合繼承融合原型鏈繼承和構造函數的優點,它的實現如下:
          function Parent(name) {
            // 私有屬性,不共享
            this.name = name;
          }
          // 需要復用、共享的方法定義在父類原型上
          Parent.prototype.speak = function() {
            console.log("hello");
          };
          function Child(name) {
            Parent.call(this, name);
          }
          // 繼承方法
          Child.prototype = new Parent();

          組合繼承模式通過將共享屬性定義在父類原型上、將私有屬性通過構造函數賦值的方式,實現了按需共享對象和方法,是 JavaScript 中最常用的繼承模式。

          雖然在繼承的實現方式上有很多種,但實際上都離不開原型對象和原型鏈的內容,因此掌握proto和prototype、對象的繼承等這些知識,是我們實現各種繼承方式的前提條件。


          總結

          關于 JavaScript 的原型和繼承,常常會在我們面試題中出現。隨著 ES6/ES7 等新語法糖的出現,可能更傾向于使用class/extends等語法來編寫代碼,原型繼承等概念逐漸變淡。

          其次JavaScript 的設計在本質上依然沒有變化,依然是基于原型來實現繼承的。如果不了解這些內容,可能在我們遇到一些超出自己認知范圍的內容時,很容易束手無策。

          端開發者丨JavaScript

          實際需求中開始

          要求:

          • 此類繼承自 Date,擁有Date的所有屬性和對象

          • 此類可以自由拓展方法

          形象點描述,就是要求可以這樣:

          1. // 假設最終的類是 MyDate,有一個getTest拓展方法

          2. let date = newMyDate();

          3. // 調用Date的方法,輸出GMT絕對毫秒數

          4. console.log(date.getTime());

          5. // 調用拓展的方法,隨便輸出什么,譬如helloworld!

          6. console.log(date.getTest());

          于是,隨手用JS中經典的組合寄生法寫了一個繼承,然后,剛準備完美收工,一運行,卻出現了以下的情景:

          但是的心情是這樣的: 囧

          以前也沒有遇到過類似的問題,然后自己嘗試著用其它方法,多次嘗試,均無果(不算暴力混合法的情況),其實回過頭來看,是因為思路新奇,憑空想不到,并不是原理上有多難。。。

          于是,借助強大的搜素引擎,搜集資料,最后,再自己總結了一番,才有了本文。

          正文開始前,各位看官可以先暫停往下讀,嘗試下,在不借助任何網絡資料的情況下,是否能實現上面的需求?(就以 10分鐘為限吧)

          分析問題的關鍵

          借助stackoverflow上的回答。

          經典的繼承法有何問題

          先看看本文最開始時提到的經典繼承法實現,如下:

          1. /**

          2. * 經典的js組合寄生繼承

          3. */

          4. functionMyDate() {

          5. Date.apply(this, arguments);

          6. this.abc = 1;

          7. }

          8. functioninherits(subClass, superClass) {

          9. functionInner() {}

          10. Inner.prototype = superClass.prototype;

          11. subClass.prototype = newInner();

          12. subClass.prototype.constructor = subClass;

          13. }

          14. inherits(MyDate,Date);

          15. MyDate.prototype.getTest = function() {

          16. returnthis.getTime();

          17. };

          18. let date = newMyDate();

          19. console.log(date.getTest());

          20. 就是這段代碼?,這也是JavaScript高程(紅寶書)中推薦的一種,一直用,從未失手,結果現在馬失前蹄。。。

            我們再回顧下它的報錯:

            再打印它的原型看看:

            怎么看都沒問題,因為按照原型鏈回溯規則, Date的所有原型方法都可以通過 MyDate對象的原型鏈往上回溯到。再仔細看看,發現它的關鍵并不是找不到方法,而是 thisisnotaDateobject.

            嗯哼,也就是說,關鍵是:由于調用的對象不是Date的實例,所以不允許調用,就算是自己通過原型繼承的也不行。

            為什么無法被繼承?

            首先,看看 MDN上的解釋,上面有提到,JavaScript的日期對象只能通過 JavaScriptDate作為構造函數來實例化。

            然后再看看stackoverflow上的回答:

            有提到, v8引擎底層代碼中有限制,如果調用對象的 [[Class]]不是 Date,則拋出錯誤。

            總的來說,結合這兩點,可以得出一個結論:要調用Date上方法的實例對象必須通過Date構造出來,否則不允許調用Date的方法。

            該如何實現繼承?

            雖然原因找到了,但是問題仍然要解決啊,真的就沒辦法了么?當然不是,事實上還是有不少實現的方法的。

            暴力混合法

            首先,說說說下暴力的混合法,它是下面這樣子的:

            說到底就是:內部生成一個 Date對象,然后此類暴露的方法中,把原有 Date中所有的方法都代理一遍,而且嚴格來說,這根本算不上繼承(都沒有原型鏈回溯)。

            ES5黑魔法

            然后,再看看ES5中如何實現?

            1. // 需要考慮polyfill情況

            2. Object.setPrototypeOf = Object.setPrototypeOf ||

            3. function(obj, proto) {

            4. obj.__proto__ = proto;

            5. returnobj;

            6. };

            7. /**

            8. * 用了點技巧的繼承,實際上返回的是Date對象

            9. */

            10. functionMyDate() {

            11. // bind屬于Function.prototype,接收的參數是:object, param1, params2...

            12. vardateInst =new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();

            13. // 更改原型指向,否則無法調用MyDate原型上的方法

            14. // ES6方案中,這里就是[[prototype]]這個隱式原型對象,在沒有標準以前就是__proto__

            15. Object.setPrototypeOf(dateInst,MyDate.prototype);

            16. dateInst.abc = 1;

            17. returndateInst;

            18. }

            19. // 原型重新指回Date,否則根本無法算是繼承

            20. Object.setPrototypeOf(MyDate.prototype,Date.prototype);

            21. MyDate.prototype.getTest = functiongetTest() {

            22. returnthis.getTime();

            23. };

            24. let date = newMyDate();

            25. // 正常輸出,譬如1515638988725

            26. console.log(date.getTest());

            27. 一眼看上去不知所措?沒關系,先看下圖來理解:(原型鏈關系一目了然)

              可以看到,用的是非常巧妙的一種做法:

              正常繼承的情況如下:

              • newMyDate()返回實例對象 date是由 MyDate構造的

              • 原型鏈回溯是: date(MyDate對象)-date.__proto__-MyDate.prototype-MyDate.prototype.__proto__-Date.prototype

              這種做法的繼承的情況如下:

              • newMyDate()返回實例對象 date是由 Date構造的

              • 原型鏈回溯是: date(Date對象)-date.__proto__-MyDate.prototype-MyDate.prototype.__proto__-Date.prototype

              可以看出,關鍵點在于:

              • 構造函數里返回了一個真正的 Date對象(由 Date構造,所以有這些內部類中的關鍵 [[Class]]標志),所以它有調用 Date原型上方法的權利

              • 構造函數里的Date對象的 [[ptototype]](對外,瀏覽器中可通過 __proto__訪問)指向 MyDate.prototype,然后 MyDate.prototype再指向 Date.prototype。

              所以最終的實例對象仍然能進行正常的原型鏈回溯,回溯到原本Date的所有原型方法。

              這樣通過一個巧妙的欺騙技巧,就實現了完美的Date繼承。不過補充一點, MDN上有提到盡量不要修改對象的 [[Prototype]],因為這樣可能會干涉到瀏覽器本身的優化。如果你關心性能,你就不應該在一個對象中修改它的 [[Prototype]]

              ES6大法

              當然,除了上述的ES5實現,ES6中也可以直接繼承(自帶支持繼承 Date),而且更為簡單:

              1. classMyDateextendsDate{

              2. constructor() {

              3. super();

              4. this.abc = 1;

              5. }

              6. getTest() {

              7. returnthis.getTime();

              8. }

              9. }

              10. let date = newMyDate();

              11. // 正常輸出,譬如1515638988725

              12. console.log(date.getTest());

              對比下ES5中的實現,這個真的是簡單的不行,直接使用ES6的Class語法就行了。而且,也可以正常輸出。

              注意:這里的正常輸出環境是直接用ES6運行,不經過babel打包,打包后實質上是轉化成ES5的,所以效果完全不一樣。

              ES6寫法,然后Babel打包

              雖然說上述ES6大法是可以直接繼承Date的,但是,考慮到實質上大部分的生產環境是: ES6+Babel

              直接這樣用ES6 + Babel是會出問題的。

              不信的話,可以自行嘗試下,Babel打包成ES5后代碼大致是這樣的:

              然后當信心滿滿的開始用時,會發現:

              對,又出現了這個問題,也許這時候是這樣的⊙?⊙

              因為轉譯后的ES5源碼中,仍然是通過 MyDate來構造,而 MyDate的構造中又無法修改屬于 Date內部的 [[Class]]之類的私有標志,因此構造出的對象仍然不允許調用 Date方法(調用時,被引擎底層代碼識別為 [[Class]]標志不符合,不允許調用,拋出錯誤)。

              由此可見,ES6繼承的內部實現和Babel打包編譯出來的實現是有區別的。(雖說Babel的polyfill一般會按照定義的規范去實現的,但也不要過度迷信)。

              幾種繼承的細微區別

              雖然上述提到的三種方法都可以達到繼承 Date的目的-混合法嚴格說不能算繼承,只不過是另類實現。

              于是,將所有能打印的主要信息都打印出來,分析幾種繼承的區別,大致場景是這樣的:

              可以參考:( 請進入調試模式)https://dailc.github.io/fe-interview/demo/extends_date.html

              從上往下, 1,2,3,4四種繼承實現分別是:(排出了混合法)

              • ES6的Class大法

              • 經典組合寄生繼承法

              • 本文中的取巧做法,Date構造實例,然后更改 __proto__的那種

              • ES6的Class大法,Babel打包后的實現(無法正常調用的)

              1. ~~~~以下是MyDate們的prototype~~~~~~~~~

              2. Date{constructor: ?, getTest: ?}

              3. Date{constructor: ?, getTest: ?}

              4. Date{getTest: ?, constructor: ?}

              5. Date{constructor: ?, getTest: ?}

              6. ~~~~以下是new出的對象~~~~~~~~~

              7. SatJan13201821:58:55GMT+0800(CST)

              8. MyDate2{abc:1}

              9. SatJan13201821:58:55GMT+0800(CST)

              10. MyDate{abc:1}

              11. ~~~~以下是new出的對象的Object.prototype.toString.call~~~~~~~~~

              12. [objectDate]

              13. [objectObject]

              14. [objectDate]

              15. [objectObject]

              16. ~~~~以下是MyDate們的__proto__~~~~~~~~~

              17. ?Date() { [native code] }

              18. ? () { [native code] }

              19. ? () { [native code] }

              20. ?Date() { [native code] }

              21. ~~~~以下是new出的對象的__proto__~~~~~~~~~

              22. Date{constructor: ?, getTest: ?}

              23. Date{constructor: ?, getTest: ?}

              24. Date{getTest: ?, constructor: ?}

              25. Date{constructor: ?, getTest: ?}

              26. ~~~~以下是對象的__proto__與MyDate們的prototype比較~~~~~~~~~

              27. true

              28. true

              29. true

              30. true

              31. 看出,主要差別有幾點:

                1. MyDate們的proto指向不一樣

                2. Object.prototype.toString.call的輸出不一樣

                3. 對象本質不一樣,可以正常調用的 1,3都是 Date構造出的,而其它的則是 MyDate構造出的

                我們上文中得出的一個結論是:由于調用的對象不是由Date構造出的實例,所以不允許調用,就算是自己的原型鏈上有Date.prototype也不行

                但是這里有兩個變量:分別是底層構造實例的方法不一樣,以及對象的 Object.prototype.toString.call的輸出不一樣(另一個 MyDate.__proto__可以排除,因為原型鏈回溯肯定與它無關)。

                萬一它的判斷是根據 Object.prototype.toString.call來的呢?那這樣結論不就有誤差了?

                于是,根據ES6中的, Symbol.toStringTag,使用黑魔法,動態的修改下它,排除下干擾:

                1. // 分別可以給date2,date3設置

                2. Object.defineProperty(date2,Symbol.toStringTag, {

                3. get:function() {

                4. returnDate;

                5. }

                6. });

                然后在打印下看看,變成這樣了:

                1. [objectDate]

                2. [objectDate]

                3. [objectDate]

                4. [objectObject]

                可以看到,第二個的 MyDate2構造出的實例,雖然打印出來是 [objectDate],但是調用Date方法仍然是有錯誤。

                此時我們可以更加準確一點的確認:由于調用的對象不是由Date構造出的實例,所以不允許調用。

                而且我們可以看到,就算通過黑魔法修改 Object.prototype.toString.call,內部的 [[Class]]標識位也是無法修改的。(這塊知識點大概是Object.prototype.toString.call可以輸出內部的[[Class]],但無法改變它,由于不是重點,這里不贅述)。

                ES6繼承與ES5繼承的區別

                從上午中的分析可以看到一點:ES6的Class寫法繼承是沒問題的。但是換成ES5寫法就不行了。

                所以ES6的繼承大法和ES5肯定是有區別的,那么究竟是哪里不同呢?(主要是結合的本文繼承Date來說)

                區別:(以 SubClass, SuperClass, instance為例)

                ES5中繼承的實質是:(那種經典組合寄生繼承法)

                • 先由子類( SubClass)構造出實例對象this

                • 然后在子類的構造函數中,將父類( SuperClass)的屬性添加到 this上, SuperClass.apply(this,arguments)

                • 子類原型( SubClass.prototype)指向父類原型( SuperClass.prototype)

                • 所以 instance是子類( SubClass)構造出的(所以沒有父類的 [[Class]]關鍵標志)

                • 所以, instance有 SubClass和 SuperClass的所有實例屬性,以及可以通過原型鏈回溯,獲取 SubClass和 SuperClass原型上的方法

                ES6中繼承的實質是:

                • 先由父類( SuperClass)構造出實例對象this,這也是為什么必須先調用父類的 super()方法(子類沒有自己的this對象,需先由父類構造)

                • 然后在子類的構造函數中,修改this(進行加工),譬如讓它指向子類原型( SubClass.prototype),這一步很關鍵,否則無法找到子類原型(注,子類構造中加工這一步的實際做法是推測出的,從最終效果來推測)

                • 然后同樣,子類原型( SubClass.prototype)指向父類原型( SuperClass.prototype)

                • 所以 instance是父類( SuperClass)構造出的(所以有著父類的 [[Class]]關鍵標志)

                • 所以, instance有 SubClass和 SuperClass的所有實例屬性,以及可以通過原型鏈回溯,獲取 SubClass和 SuperClass原型上的方法

                以上?就列舉了些重要信息,其它的如靜態方法的繼承沒有贅述。(靜態方法繼承實質上只需要更改下 SubClass.__proto__到 SuperClass即可)

                可以看著這張圖快速理解:

                有沒有發現呢:ES6中的步驟和本文中取巧繼承Date的方法一模一樣,不同的是ES6是語言底層的做法,有它的底層優化之處,而本文中的直接修改_proto_容易影響性能。

                ES6中在super中構建this的好處?

                因為ES6中允許我們繼承內置的類,如Date,Array,Error等。如果this先被創建出來,在傳給Array等系統內置類的構造函數,這些內置類的構造函數是不認這個this的。所以需要現在super中構建出來,這樣才能有著super中關鍵的 [[Class]]標志,才能被允許調用。(否則就算繼承了,也無法調用這些內置類的方法)

                構造函數與實例對象

                看到這里,不知道是否對上午中頻繁提到的構造函數,實例對象有所混淆與困惑呢?這里稍微描述下。

                要弄懂這一點,需要先知道 new一個對象到底發生了什么?先形象點說:

                new MyClass()中,都做了些什么工作
                1. functionMyClass() {

                2. this.abc = 1;

                3. }

                4. MyClass.prototype.print = function() {

                5. console.log('this.abc:'+this.abc);

                6. };

                7. let instance = newMyClass();

                譬如,上述就是一個標準的實例對象生成,都發生了什么呢?

                步驟簡述如下:(參考MDN,還有部分關于底層的描述略去-如[[Class]]標識位等)

                1. 構造函數內部,創建一個新的對象,它繼承自 MyClass.prototype, letinstance=Object.create(MyClass.prototype);

                2. 使用指定的參數調用構造函數 MyClass,并將 this綁定到新創建的對象, MyClass.call(instance);,執行后擁有所有實例屬性

                3. 如果構造函數返回了一個“對象”,那么這個對象會取代整個 new出來的結果。如果構造函數沒有返回對象,那么new出來的結果為步驟1創建的對象。 (一般情況下構造函數不返回任何值,不過用戶如果想覆蓋這個返回值,可以自己選擇返回一個普通對象來覆蓋。當然,返回數組也會覆蓋,因為數組也是對象。)

                結合上述的描述,大概可以還原成以下代碼(簡單還原,不考慮各種其它邏輯):

                1. let instance = Object.create(MyClass.prototype);

                2. let innerConstructReturn = MyClass.call(instance);

                3. let innerConstructReturnIsObj =typeofinnerConstructReturn ==='object'||typeofinnerConstructReturn ==='function';

                4. returninnerConstructReturnIsObj ? innerConstructReturn : instance;

                注意?:普通的函數構建,可以簡單的認為就是上述步驟。實際上對于一些內置類(如Date等),并沒有這么簡單,還有一些自己的隱藏邏輯,譬如 [[Class]]標識位等一些重要私有屬性。譬如可以在MDN上看到,以常規函數調用Date(即不加 new 操作符)將會返回一個字符串,而不是一個日期對象,如果這樣模擬的話會無效。

                覺得看起來比較繁瑣?可以看下圖梳理:

                那現在再回頭看看。

                什么是構造函數?

                如上述中的 MyClass就是一個構造函數,在內部它構造出了 instance對象。

                什么是實例對象?

                instance就是一個實例對象,它是通過 new出來的?

                實例與構造的關系

                有時候淺顯點,可以認為構造函數是xxx就是xxx的實例。即:

                1. let instance = newMyClass();

                此時我們就可以認為 instance是 MyClass的實例,因為它的構造函數就是它。

                實例就一定是由對應的構造函數構造出的么?

                不一定,我們那ES5黑魔法來做示例。

                1. functionMyDate() {

                2. // bind屬于Function.prototype,接收的參數是:object, param1, params2...

                3. vardateInst =new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();

                4. // 更改原型指向,否則無法調用MyDate原型上的方法

                5. // ES6方案中,這里就是[[prototype]]這個隱式原型對象,在沒有標準以前就是__proto__

                6. Object.setPrototypeOf(dateInst,MyDate.prototype);

                7. dateInst.abc = 1;

                8. returndateInst;

                9. }

                10. 我們可以看到 instance的最終指向的原型是 MyDate.prototype,而 MyDate.prototype的構造函數是 MyDate,因此可以認為 instance是 MyDate的實例。

                  但是,實際上, instance卻是由 Date構造的,我們可以繼續用 ES6中的 new.target來驗證。

                  注意?:關于 new.target, MDN中的定義是:new.target返回一個指向構造方法或函數的引用。

                  嗯哼,也就是說,返回的是構造函數。

                  我們可以在相應的構造中測試打印:

                  1. classMyDateextendsDate{

                  2. constructor() {

                  3. super();

                  4. this.abc = 1;

                  5. console.log('~~~new.target.name:MyDate~~~~');

                  6. console.log(new.target.name);

                  7. }

                  8. }

                  9. // new操作時的打印結果是:

                  10. // ~~~new.target.name:MyDate~~~~

                  11. // MyDate

                  然后,可以在上面的示例中看到,就算是ES6的Class繼承, MyDate構造中打印 new.target也顯示 MyDate,但實際上它是由 Date來構造(有著 Date關鍵的 [[Class]]標志,因為如果不是Date構造(如沒有標志)是無法調用Date的方法的)。

                  這也算是一次小小的勘誤吧。

                  所以,實際上用 new.target是無法判斷實例對象到底是由哪一個構造構造的(這里指的是判斷底層真正的 [[Class]]標志來源的構造)。

                  再回到結論:實例對象不一定就是由它的原型上的構造函數構造的,有可能構造函數內部有著寄生等邏輯,偷偷的用另一個函數來構造了下,當然,簡單情況下,我們直接說實例對象由對應構造函數構造也沒錯(不過,在涉及到這種Date之類的分析時,我們還是得明白)。

                  [[Class]]與Internal slot

                  這一部分為補充內容。

                  前文中一直提到一個概念:Date內部的 [[Class]]標識。

                  其實,嚴格來說,不能這樣泛而稱之(前文中只是用這個概念是為了降低復雜度,便于理解),它可以分為以下兩部分:

                  在ES5中,每種內置對象都定義了 [[Class]] 內部屬性的值,[[Class]] 內部屬性的值用于內部區分對象的種類

                  • Object.prototype.toString訪問的就是這個[[Class]]

                  • 規范中除了通過 Object.prototype.toString,沒有提供任何手段使程序訪問此值。

                  • 而且Object.prototype.toString輸出無法被修改

                  而在ES5中,之前的 [[Class]] 不再使用,取而代之的是一系列的 internalslot

                  • Internal slot 對應于與對象相關聯并由各種ECMAScript規范算法使用的內部狀態,它們沒有對象屬性,也不能被繼承

                  • 根據具體的 Internal slot 規范,這種狀態可以由任何ECMAScript語言類型或特定ECMAScript規范類型值的值組成

                  • 通過 Object.prototype.toString,仍然可以輸出Internal slot值

                  • 簡單點理解(簡化理解),Object.prototype.toString的流程是:如果是基本數據類型(除去Object以外的幾大類型),則返回原本的slot,如果是Object類型(包括內置對象以及自己寫的對象),則調用 Symbol.toStringTag。 Symbol.toStringTag方法的默認實現就是返回對象的Internal slot,這個方法可以被重寫

                  這兩點是有所差異的,需要區分(不過簡單點可以統一理解為內置對象內部都有一個特殊標識,用來區分對應類型-不符合類型就不給調用)。

                  JS內置對象是這些:

                  1. Arguments,Array,Boolean,Date,Error,Function,JSON,Math,Number,Object,RegExp,String

                  ES6新增的一些,這里未提到:(如Promise對象可以輸出 [objectPromise]),而前文中提到的:

                  1. Object.defineProperty(date,Symbol.toStringTag, {

                  2. get:function() {

                  3. returnDate;

                  4. }

                  5. });

                  它的作用是重寫Symbol.toStringTag,截取date(雖然是內置對象,但是仍然屬于Object)的 Object.prototype.toString的輸出,讓這個對象輸出自己修改后的 [objectDate]。

                  但是,僅僅是做到輸出的時候變成了Date,實際上內部的 internalslot值并沒有被改變,因此仍然不被認為是Date。

                  如何快速判斷是否繼承?

                  其實,在判斷繼承時,沒有那么多的技巧,就只有關鍵的一點: [[prototype]]( __ptoto__)的指向關系。

                  譬如:

                  1. console.log(instanceinstanceofSubClass);

                  2. console.log(instanceinstanceofSuperClass);

                  實質上就是:

                  • SubClass.prototype是否出現在 instance的原型鏈上

                  • SuperClass.prototype是否出現在 instance的原型鏈上

                  然后,對照本文中列舉的一些圖,一目了然就可以看清關系。有時候,完全沒有必要弄的太復雜。

                  覺得本文對你有幫助?請分享給更多人

                  前端開發者丨JavaScript

          S雖然不像是JAVA那種強類型的語言,但也有著與JAVA類型的繼承屬性,那么JS中的繼承是如何實現的呢?

          一、構造函數繼承

          在構造函數中,同樣屬于兩個新創建的函數,也是不相等的
           function Fn(name){
           this.name = name;
           this.show = function(){
           alert(this.name);
           }
           }
           var obj1 = new Fn("AAA");
           var obj2 = new Fn("BBB");
           console.log(obj1.show==obj2.show); //false
           此時可以看出構造函數的多次創建會產生多個相同函數,造成冗余太多。
           利用原型prototype解決。首先觀察prototype是什么東西
           function Fn(){}
           console.log(Fn.prototype);
           //constructor表示當前的函數屬于誰
           //__proto__ == [[prototype]],書面用語,表示原型指針
           var fn1 = new Fn();
           var fn2 = new Fn();
           Fn.prototype.show = function(){
           alert(1);
           }
           console.log(fn1.show==fn2.show); //ture
          

          此時,任何一個對象的原型上都有了show方法,由此得出,構造函數Fn.prototype身上的添加的方法,相當于添加到了所有的Fn身上。

          二、call和applay繼承

          function Father(skill){
           this.skill = skill;
           this.show = function(){
           alert("我會"+this.skill);
           }
           }
           var father = new Father("絕世木匠");
           function Son(abc){
           //這里的this指向函數Son的實例化對象
           //將Father里面的this改變成指向Son的實例化對象,當相遇將father里面所有的屬性和方法都復制到了son身上
           //Father.call(this,abc);//繼承結束,call適合固定參數的繼承
           //Father.apply(this,arguments);//繼承結束,apply適合不定參數的繼承
           }
           father.show()
           var son = new Son("一般木匠");
           son.show();
          

          三、原型鏈繼承(demo)

          這個的么實現一個一個簡單的拖拽,a->b的一個繼承。把a的功能繼承給b。

          HTML:

          <div id="drag1"></div>
          <div id="drag2"></div>
          

          CSS:

          *{margin: 0;padding: 0;}
           #drag1{width: 100px;height: 100px;background: red;position: absolute;}
           #drag2{width: 100px;height: 100px;background: black;position: absolute;left: 500px;}
          

          JS:

          function Drag(){}
           Drag.prototype={
           constructor:Drag,
           init:function(id){
           this.ele=document.getElementById(id);
           this.cliW=document.documentElement.clientWidth||document.body.clientWidth;
           this.cliH=document.documentElement.clientHeight||document.body.clientHeight;
           var that=this;
           this.ele.onmousedown=function(e){
           var e=event||window.event;
           that.disX=e.offsetX;
           that.disY=e.offsetY;
           document.onmousemove=function(e){
           var e=event||window.event;
           that.move(e);
           }
           that.ele.onmouseup=function(){
           document.onmousemove=null;
           }
           } 
           },
           move:function(e){
           this.x=e.clientX-this.disX;
           this.y=e.clientY-this.disY;
           this.x=this.x<0?this.x=0:this.x;
           this.y=this.y<0?this.y=0:this.y;
           this.x=this.x>this.cliW-this.ele.offsetWidth?this.x=this.cliW-this.ele.offsetWidth:this.x;
           this.y=this.y>this.cliH-this.ele.offsetHeight?this.y=this.cliH-this.ele.offsetHeight:this.y;
           this.ele.style.left=this.x+'px';
           this.ele.style.top=this.y+'px';
           }
           }
           new Drag().init('drag1')
           function ChidrenDrag(){}
           ChidrenDrag.prototype=new Drag()
           new ChidrenDrag().init('drag2')
          

          四、混合繼承

          function Father(skill,id){
           this.skill = skill;
           this.id = id;
           }
           Father.prototype.show = function(){
           alert("我是father,這是我的技能"+this.skill);
           }
           function Son(){
           Father.apply(this,arguments);
           }
           //如果不做son的原型即成father的原型,此時會報錯:son.show is not a function
           Son.prototype = Father.prototype;
           //因為,如果不讓son的原型等于father的原型,son使用apply是繼承不到原型上的方法
           //但這是一種錯誤的原型繼承示例,如果使用這種方式,會導致修改son原型上的show方法時,會把father身上的show也修改
           //內存的堆和棧機制
           Son.prototype.show = function(){
           alert("我是son,這是我的技能"+this.skill);
           }
           var father = new Father("專家級鐵匠","father");
           var son = new Son("熟練級鐵匠","son");
           father.show();
           son.show();
           上面的示例應該修改成以下形式:
           以上紅色的代碼應改成:
           for(var i in Father.prototype){
           Son.prototype[i] = Father.prototype[i];
           }
           //遍歷father的原型身上的所有方法,依次拷貝給son的原型,這種方式稱為深拷貝
           這種繼承方式叫做混合繼承,用到了for-in繼承,cell和apple繼承。
          

          五、Es6的class繼承(demo)

          這個demo的功能和原型鏈繼承的demo功能一樣,a->b的繼承

          HTML:

          <div id="drag1"></div>
          <div id="drag2"></div>
          

          CSS:

          *{margin: 0;padding: 0;}
          #drag1{width: 100px;height: 100px;background: red;position: absolute;}
          #drag2{width: 100px;height: 100px;background: black;position: absolute;left: 500px;}
          

          JS:

          class Drag{
           constructor(id){
           this.ele=document.getElementById(id);
           this.init();
           };
           init(){
           var that=this;
           this.ele.onmousedown=function(e){
           var e=event||window.event;
           that.disX=e.offsetX;
           that.disY=e.offsetY;
           document.onmousemove=function(e){
           var e=event||window.event;
           that.move(e);
           }
           that.ele.onmouseup=function(){
           document.onmousemove=null;
           that.ele.onmouseup=null;
           }
           }
           };
           move(e){
           this.ele.style.left=e.clientX-this.disX+"px";
           this.ele.style.top=e.clientY-this.disY+"px";
           }
           }
           new Drag("drag1");
           class ExtendsDrag extends Drag{
           constructor(id){
           super(id);
           }
           }
           new ExtendsDrag("drag2")
          

          我總結的這幾種繼承方法.兩個demo繼承的方法大家最好在編譯器上跑一下,看看。這樣才能更深刻的去理解。尤其是原型鏈的繼承,js作為一個面向對象的編程語言,還是很常用的。

          對前端的技術,架構技術感興趣的同學關注我的頭條號,并在后臺私信發送關鍵字:“前端”即可獲取免費的架構師學習資料

          知識體系已整理好,歡迎免費領取。還有面試視頻分享可以免費獲取。關注我,可以獲得沒有的架構經驗哦!!


          主站蜘蛛池模板: 中文人妻av高清一区二区| 福利视频一区二区牛牛| av无码免费一区二区三区| 午夜影视日本亚洲欧洲精品一区| 精品无码一区二区三区水蜜桃| 国产一区二区三区免费看| 一区二区三区免费看| 丰满少妇内射一区| 在线观看一区二区三区av| 一区二区中文字幕| 人妻少妇久久中文字幕一区二区| 无码人妻精品一区二区三区东京热 | 国产剧情一区二区| 国产人妖在线观看一区二区| 国模丽丽啪啪一区二区| 精品国产AV无码一区二区三区| 国产一区二区电影| 一区五十路在线中出| 香蕉视频一区二区三区| 精品天海翼一区二区| 四虎在线观看一区二区| 亚洲宅男精品一区在线观看| 岛国无码av不卡一区二区| 精品不卡一区中文字幕 | 一区二区三区四区精品| 国产精品成人一区无码| 亚洲av乱码中文一区二区三区| 久热国产精品视频一区二区三区| 波多野结衣在线观看一区二区三区| 国产成人一区二区精品非洲| 男人的天堂av亚洲一区2区| 亚洲av综合av一区二区三区| 蜜桃传媒一区二区亚洲AV| 欧美日韩国产免费一区二区三区| 亚洲一区二区三区免费在线观看| 在线视频一区二区| 无码一区二区三区AV免费| 无码人妻啪啪一区二区| 国产午夜三级一区二区三| 人妻无码一区二区三区免费| 无码人妻av一区二区三区蜜臀 |