《前端之路》- TypeScript (三) ES5 中實現繼承、類以及原理
阿新 • • 發佈:2020-03-22
[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