1. 程式人生 > >《前端之路》- TypeScript (三) ES5 中實現繼承、類以及原理

《前端之路》- TypeScript (三) ES5 中實現繼承、類以及原理

[TOC] > 這篇文章中的內容會比較的多,而且在基礎中是資料相對比較複雜的基礎,主要是講到了 JS 這門語言中如何實現繼承、多型,以及什麼情況如何定義 私有屬性、方法,共有屬性、方法,被保護的屬性和方法。明確的定義了 JS 中的訪問邊界問題,以及最終實現的原理是什麼。接下來,讓我們仔細瞅瞅這部分吧~ ### 一、先講講 ES5 中建構函式(類)靜態方法和多型 > 首先在 ES5 中是沒有類的概念的,我們一般是通過建構函式中來實現類。下面就舉個例子。 > 另外我們再複習下我們在 JS 中的經常會提到的問題,原型以及原型鏈 #### 1-1 JS 中原型以及原型鏈 > JS 中通過 `__prpto__` 的橋樑實現原型鏈, 也叫做實現繼承。 > JS 中通過 `prototype` 的屬性複製自己的模版物件(也可以叫做被複制的物件) ##### 例子一 > 上例子之前,我們來看一張圖 ( 來自 juejin,侵刪) ![](https://img2020.cnblogs.com/blog/675289/202003/675289-20200321232508577-671349333.jpg) > 造物主無中生有,從 null 製造了一個 No.1 物件( 神 ),這個 No.1 物件覺得自己太孤獨,就 copy 了一份自己,我們叫她 Object ,同時 No1 物件 希望 Object 可以為自己幹活,然後 Object 就學會了 new 這個技能,new 一下,同時加入各種屬性,就可以 瞬間讓這個世界豐富多彩了起來,後來,豐富多彩的世界也物以群分了,然後就出現了 String、Number、Boolean、Array、Date... 等等型別(demo1),然後造物主又發現, ```javascript String.constructor; // ƒ String() { [native code] } Number.constructor; // ƒ Number() { [native code] } Array.constructor; // ƒ Array() { [native code] } Object.constructor; // ƒ Object() { [native code] } Function.constructor; // ƒ Function() { [native code] } ``` --- ```javascript // demo1 String.prototype; // String {"", constructor: ƒ, anchor: ƒ, big: ƒ, blink: ƒ, …} Number.prototype; // Number {0, constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, …} Array.prototype; // [constructor: ƒ, concat: ƒ, copyWithin: ƒ, fill: ƒ, find: ƒ, …] Function.prototype; // ƒ () { [native code] } Object.prototype; // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …} ``` > 其實到這裡的時候,其實很多同學,已經開始徹底蒙圈了,這是啥啊? 怎麼一會 constructor 一會 prototype 還有一個 **proto** 啊啊啊,簡直要瘋掉了,到底這些都是一些啥啊。。。不要著急,下面我們重新來認識一下這三個貨,出現的原因以及分別代表著什麼 > 測試 #### 1-2 JS 中原型以及原型鏈中,我們常見的 constructor、prototype、**proto** 這三者之間的關係 > 一、首先,我們總最容易理解的 `constructor`(構造器)來理解 ```javascript var F = function() { this.name = "F-建構函式"; }; var f1 = new F(); var f2 = new F(); console.log(F.constructor); // ƒ Function() { [native code] } 是瀏覽器自帶的原生方法 Function console.log(f1.constructor); // ƒ () {this.name = 'F-建構函式';} 是建構函式 F 本身 console.log(f2.constructor); // ƒ () {this.name = 'F-建構函式';} 是建構函式 F 本身 // 這個時候大家其實對於 constructor 屬性有一定的瞭解了,物件、函式都有 constructor 屬性 ``` > 這裡我們有一張圖 ![](https://img2020.cnblogs.com/blog/675289/202003/675289-20200322165305011-815457904.jpg) > 同時,JS 原生自帶的一些方法和 上文中 我們定義的 Person 類也非常類似,唯一的區別就是,Person 是使用者自己定義的, 生自帶的方法是官方指定的。 ![](https://img2020.cnblogs.com/blog/675289/202003/675289-20200322164732897-81070658.jpg) > JS 語言中自帶的原生方法 String、Number、Array、Boolean、Function、Object、Date 等,都是 Function 的例項化物件 > String、Number、Array、Boolean、Function、Object、Date 等 的 constructor 都指向 Function > (Function 的建構函式也指向 Function,不要疑惑,雖然毫無道理,但是就是這麼發生了) --- > 二、接下來我們再來看看 `prototype` 是怎麼樣產生的, JS 的語言設計,為什麼需要 `prototype` 物件。 - 還是上面的例子:只不過我們分別給 f1 和 f2 新增一個 say 方法,然後我們去對比這 2 個方法的差異 ```javascript var F = function() { this.name = "F-建構函式"; }; var f1 = new F(); var f2 = new F(); f1.say = function() { console.log("say hello"); }; f2.say = function() { console.log("say hello"); }; console.log(f1.say === f2.say); // false ``` - 我們發現並不相等,因為通過建構函式例項化生成的物件的指標都分別指向不同的棧(也可以理解為記憶體)(這裡不太明白的化,建議看下《你不知道的 JavaScript》) - 我們去對比這 2 個不同物件上的相同名稱的方法,肯定是不一樣的 - 那如果我例項化幾千幾萬個物件,都包含這個方法的化,那記憶體豈不是要爆了 - 所以基於節約記憶體的出發點,我們是否可以建立一個 例項話物件都可以訪問的公共物件,這個時候 `prototype` 就應運而生了 > 基於上面的例子我們再修改下: ```javascript var F = function() { this.name = "F-建構函式"; }; F.prototype.say = function() { console.log("say hello"); }; var f1 = new F(); var f2 = new F(); f1.say(); // say hello f2.say(); // say hello console.log(f1.say === f2.say); // true ``` > 所以 `prototype` 物件的出現,達到了 共享、公用的效果。節約了記憶體。同時 `prototype` 物件用於放某同一型別例項的共享屬性和方法,實質上是為了記憶體著想。 > 這裡我們再放一張圖 ![](https://img2020.cnblogs.com/blog/675289/202003/675289-20200322174627707-509986941.jpg) --- > 三、`constructor` 屬性具體在哪裡? - 以這裡為例子,我們打印出來 f1 的時候,並沒有在物件的一級目錄中找到 `constructor` 屬性,那會是在哪裡呢? - 按照上面的這張圖,我們會發現,每個例項化物件的 `constructor` 屬性都是指向 建構函式(Person) - 那如果我們例項化幾千幾萬個物件呢? 每個例項化物件的 `constructor` 想必也會佔用大量的記憶體,而且根本沒有必要 - 所以這個時候神奇的事情發生了,我們把 每個例項化物件的 `constructor` 作為一個共享資料,放在 `prototype` 物件中,節約記憶體。 - 這個時候就會又有下面的圖了 ![](https://img2020.cnblogs.com/blog/675289/202003/675289-20200322175110447-1121418350.jpg) - 這個時候我們肯定會思考一個問題就是: 我們直接通過 f1.constructor 訪問到的 構造屬性 是通過什麼方式來訪問到的呢? - 另外一個問題: 如果我們修改了 f1.constructor 這個值,我們是不是就根本沒有辦法訪問到 例項化物件的構造函數了? ```javascript var F = function() { this.name = "F-建構函式"; }; F.prototype.say = function() { console.log("say hello"); }; var f1 = new F(); var f2 = new F(); f1.constructor = function() { this.name = "匿名建構函式"; }; console.log(f1.constructor == f2.constructor); // fasle 這個時候 f1 物件就沒辦法找到自己的構造函數了, // 因為我們給 f1 例項化物件新增了一個 constructor 屬性,這個時候,JS 就會優先返回這個值,而不是真正的 建構函式物件,聰明的 JS 肯定不會讓這種事情發生的,對麼。下面就該我們的 __proto__ 出場啦 ``` > 四、`__proto__` 的出現 目的: 讓例項找到自己的例項 - 對,下面就是我們要來說到了 `__proto__` - 核心能力: 任何例項化物件的 `__proto__` 屬性都指向其 建構函式的 `prototype` (我們可以把 `prototype` 理解成一個 可以抽離成成千上萬例項化物件都具備的 公共屬性的集合 其中包括了:`constructor` 屬性、以及使用者定義在 `prototype` 上的屬性或者方法 ) - 廢話不多說了,我們先上圖 ![](https://raw.githubusercontent.com/erbing/blog/master/assets/imgs/10991584872025_.pic.jpg) - 然後我們就得出來一個結論 ```javascript f1.__proto__ === F.prototype; // true ``` > 五、總結 - 這裡我們針對 JS 中原型已經原型鏈又進一步的複習鞏固了一下,其實還有很多類容是可以深挖的,因為這裡是 ts 篇,我們就暫時先寫到這裡,後續我們可以在留言區進行進一步的討論 #### 1-2 JS 中通過建構函式來實現 類 > 實現一個類的話上面的案例基本上簡單的呈現了下: ```javascript function Person(name) { this.name = name; this.run = function() { console.log(this.name + "跑步"); }; } Person.prototype.age = 12; Person.prototype.work = function() { console.log(this.name + "寫程式碼"); }; Person.weight = "70kg"; Person.eat = function() { console.log("在吃飯"); }; var p = new Person("zhangsan"); p.run(); // zhangsan跑步 p.work(); // zhangsan寫程式碼 p.eat(); // Uncaught TypeError: p.eat is not a function ``` - 這裡我們就針對,上面出現的錯誤和正確的情況分析一下: - 1、為什麼執行 p.run() 成功了,這裡簡單過一下 new 的操作 ```javascript // new 操作背後的真相 function New(name) { this.name = name; } // 一、建立一個新的物件 var o = {}; // 二、需要認祖歸宗,需要知道自己是被哪個建構函式例項化生成的 o.__proto__ = New.prototype; // 三、需要拿到 祖上傳給你的傳家寶 New.apply(o, arguments); // arguments 為傳入的引數, 通過執行建構函式,巧妙的將建構函式中 this 的上下文轉換成了 新生成的 o 物件的上下文,讓其也擁有了構建函式內部的屬性和方法 // 四、最後返回 o return o; ``` - 2、為什麼執行 p.run() 成功了咧,因為 new 的過程中 例項化物件 p 中已經繼承了建構函式 Person 內的屬性和方法所以成功了 - 3、為什麼執行 p.work() 成功了? 因為 p 的 `__proto__` 指向的是 Person.prototype 剛好,我們在 Person.prototype 新增了一個 work 方法,所以 p 可以通過 `__proto__` 原型鏈找到 work 方法執行成功 - 4、為什麼 p.eat() 報錯了? 我們看看 eat 方法我們是如何定義的: ```javascript ... Person.eat = function() { console.log('在吃飯') } p2 = new Person('lisi') // 因為 eat 這個靜態方法是掛載在建構函式這個物件上的,而我們的 new 操作是繼承了 建構函式內部的方法和屬性, // 所以在繼承父類私有屬性的時候沒有找到,那還有 原型鏈上的呢?同樣,new 操作是將 `__proto__` 指向了 Person.prototype 而這個物件中也沒有這個方法,所以就報錯了 // 那如果 p2 想訪問,有辦法麼? 有的 p2.constructor.eat() // 在吃飯 // 同時 p2.constructor.eat() === Person.eat() === Person.prototype.constructor.eat() // 但是這種訪問的方式,沒辦法和物件的上下文結合起來,也沒有多大的作用,所以我們往往在我們日常的開發中用到的比較少。 ``` ### 總結 > 對,沒錯。這一章 都是基礎知識,那麼基於這個基礎上,我們下一章節會正式來進入 typescript 中 class 的學習中來 > 包括了 TypeScript 中的類,類的定義、方法屬性的定義和類的修飾符等,敬請期待~ > GitHub 地址:(歡迎 star 、歡迎推薦 : ) > [《前端之路》 - TypeScript(三)ES5 中實現繼承、類以及原理](https://github.com/erbing/blog/blob/master/%E5%89%8D%E7%AB%AF%E4%B9%8B%E8%B7%AF%20-%20TypeScript/TypeScript%20(%E4%B8%89)%20ES5%20%E4%B8%AD%E5%AE%9E%E7%8E%B0%E7%BB%A7%E6%89%BF%E3%80%81%E7%B1%BB%E4%BB%A5%E5%8F%8A%E5%8E%9F%E7%9