起源:為什么使用原型鏈
使用原型鏈?zhǔn)菫榱藢?shí)現(xiàn)繼承,那js的繼承為什么選擇了原型鏈呢?我們來看看網(wǎng)絡(luò)的解釋
http://www.itdecent.cn/p/a97863b59ef7
https://www.zhihu.com/search?type=content&q=js%E5%8E%9F%E5%9E%8B%E9%93%BE
網(wǎng)絡(luò)上的解釋基本很一致啊,我覺得這個(gè)講的比較有道理,簡單的講解了為什么使用原型鏈

但是我感覺還是沒有說清楚,所以我大膽假設(shè),小心分析,思考出了以下內(nèi)容。
首先,我們?nèi)タ碋CMAScript的歷史,網(wǎng)景公司是它的發(fā)明者,那時(shí)候網(wǎng)頁多簡單啊,一個(gè)單純的頁面ok了,js可能就是隨便寫點(diǎn)數(shù)據(jù)修改云云就ok,而且那時(shí)候C語言應(yīng)該還很活躍吧,所以這么簡單的需求,面向過程啊,js對(duì)于數(shù)據(jù)的處理直接生硬的懟上去就好了呀。所以,依照預(yù)料,js或者說ECMAScript一開始就是面向過程的。(當(dāng)年網(wǎng)頁有多簡單如下,這tm的丑爆了好吧,圖源阮一峰)

我不知道為什么后來有了C++,JAVA(么得研究過)這么diao的OOP語言,但是對(duì)于ECMAScript而言,他一直堅(jiān)挺著,在web界保持著自己的地位,所以時(shí)代更迭OOP逐漸登上流行舞臺(tái),但是JS還保持著自己在web界的地位,卻發(fā)現(xiàn)很多內(nèi)容如果用單純的面向過程不太OK了,那就意味著JS得進(jìn)步呀,他也得去做OOP。
所以這時(shí)候我就想到了類比C語言,為什么?因?yàn)镃語言也是面向過程,JS使用的是function來表示一個(gè)類,C語言則是使用了struct(這里的類不具有OO含義)。JS有一個(gè)很有趣的點(diǎn)就叫做模擬,因?yàn)閃EB只有JS,不能繼承不能多態(tài)只能模擬(但是C語言這種,對(duì)吧,不能繼承不能多態(tài),換C++啊=w=)。那么JS選擇了原型模式,原型鏈去模擬這種繼承。
tips:原型鏈的由來
類模式(這是我自己的叫法,包括生成器,工廠,抽象工廠模式)與原型模式
大致可以這樣理解,
抽象工廠模式: 一個(gè)類由一個(gè)構(gòu)造函數(shù)初始化
生成器模式: 一個(gè)類由一個(gè)專門的私有函數(shù)決定其初始狀態(tài)
工廠模式: 一個(gè)類初始化了一些基本的內(nèi)容(可以稱之為基類),具體內(nèi)容可以由子類進(jìn)行豐富完整
原型模式: 當(dāng)實(shí)現(xiàn)一個(gè)類式的實(shí)例消耗很大的時(shí)候,使用的模式
分析:繼承與原型鏈
剛才簡單介紹了JS選擇原型鏈繼承的由來,但是感覺還不夠清楚,以下繼續(xù)分析。
我專門百度了C語言的模擬繼承:https://blog.csdn.net/snow_5288/article/details/70197366
我們可以看到,繼承的核心無非就是兩個(gè)類,其中一個(gè)要復(fù)用另外一個(gè)之中的方法,如果要用C語言去模擬,那就是在其中的一個(gè)struct之中將另一個(gè)struct聲明出來,然后在這個(gè)struct中就包含了另外一個(gè)struct的屬性和方法。
當(dāng)然這是不太夠的吧,如果我去調(diào)用:
typedef void (*FUN)()
struct _A //父類
{
FUN _fun; //由于C語言中結(jié)構(gòu)體不能包含函數(shù),故只能用函數(shù)指針在外面實(shí)現(xiàn)
int _a;
};
struct _B //子類
{
_A _a_; //在子類中定義一個(gè)基類的對(duì)象即可實(shí)現(xiàn)對(duì)父類的繼承
int _b;
};
void Test()
{
//測試C++中的繼承與多態(tài)
A a; //定義一個(gè)父類對(duì)象a
B b; //定義一個(gè)子類對(duì)象b
A* p1 = &a; //定義一個(gè)父類指針指向父類的對(duì)象
p1->fun(); //調(diào)用父類的同名函數(shù)
p1 = &b; //讓父類指針指向子類的對(duì)象
p1->fun(); //調(diào)用子類的同名函數(shù)
//C語言模擬繼承與多態(tài)的測試
_A _a; //定義一個(gè)父類對(duì)象_a
_B _b; //定義一個(gè)子類對(duì)象_b
_a._fun = _fA; //父類的對(duì)象調(diào)用父類的同名函數(shù)
_b._a_._fun = _fB; //子類的對(duì)象調(diào)用子類的同名函數(shù)
/* 這堆可以不用看。。。。
_A* p2 = &_a; //定義一個(gè)父類指針指向父類的對(duì)象
p2->_fun(); //調(diào)用父類的同名函數(shù)
p2 = (_A*)&_b; //讓父類指針指向子類的對(duì)象,由于類型不匹配所以要進(jìn)行強(qiáng)轉(zhuǎn)
p2->_fun(); //調(diào)用子類的同名函數(shù)
*/
}
不太夠在哪里呢?我調(diào)用方法得b.a.func(),好蠢啊不是么?
所以原型登場了,JS用原型去解決了這個(gè)問題。它對(duì)于每個(gè)實(shí)例,都初始化了一個(gè)proto屬性,這個(gè)屬性指向的是實(shí)例的基類的公共方法,當(dāng)用戶用實(shí)例去調(diào)用方法的時(shí)候,如果是繼承的內(nèi)容(如何判斷后面會(huì)講,其實(shí)我覺得大家也都清楚,要不然面試都過不了),就會(huì)到這里去遍歷查找,查找到了就可以使用基類的方法,所以有:
let a = new String()
a.hasOwnProperty() // ->可以運(yùn)行
let b = null
b.hasOwnProperty() // ->無法運(yùn)行
對(duì)于a來說,a本身是個(gè)string類型的實(shí)例,在string上并不具有hasOwnProperty這樣的方法,但是String本身是繼承自O(shè)bject的,這些方法寫在Object類的原型之中,所以a.hasOwnProperty() === a.proto.proto.hasOwnProperty
而JS基本類型還有倆神經(jīng)病,null和undefined,這倆是完全獨(dú)立的(啊,雖然有typeof null = object的bug,但是這是bug,別在意這些神經(jīng)病細(xì)節(jié))。他們不具有proto,所以他倆沒有辦法直接調(diào)用相應(yīng)的方法(似乎這是很顯然的事情,不過這里為了作對(duì)比,強(qiáng)行講一下)
PS.typeof null = object 大致是因?yàn)?,object的proto實(shí)際上是指向了null。
proto與prototype
好的,終于到了大家都懂的部分了,有點(diǎn)不太想寫這種東西,網(wǎng)上一大堆,自己找好了,我覺得寫的很nice的幾個(gè):
https://zhuanlan.zhihu.com/p/34766836
https://zhuanlan.zhihu.com/p/23026595
https://zhuanlan.zhihu.com/p/22787302
https://zhuanlan.zhihu.com/p/40708626
我畫的圖:
https://online.visual-paradigm.com/tw/w/zbrkocui/diagrams.jsp#diagram:proj=0&id=1

簡單來說,我覺得這就是一個(gè)new的問題,為什么這么說呢?首先我們要知道,proto是一個(gè)自帶的屬性,不需要我們?nèi)ゾ帉懀ó?dāng)然你非要搞事情改里面內(nèi)容也沒人攔著),而prototype呢是程序員自己去寫的一個(gè)內(nèi)容。雖然這樣說不太準(zhǔn)確,因?yàn)轭愃芆bject,String等基本類型的prototype也沒讓程序員自己寫?。鞘且?yàn)镴S開發(fā)的程序員替你寫了啊喂),看如下代碼:
// 需要注意的是,幾大基本類型的prototype,是需要考量的。
// 對(duì)于幾大基本類型,string,number,boolean應(yīng)該是類似的,
// function, array等混合類型需要特別討論,null和undefined沒有原型鏈
let a = '' // a 為string
let b = 0
let c = function () { }
let d = false
// Object 混合類型,也是一種基本混合類型,但是他完全可以規(guī)劃到new類型當(dāng)中
let O = new Object()
// 不過前四種也是可以轉(zhuǎn)換一下的
let aa = new String()
let bb = new Number()
let cc = new Function()
let dd = new Boolean()
// 所以歸根結(jié)底,是討論一個(gè)new的原型鏈傳遞
console.log(a) //string
console.log(a.__proto__) //object.prototype
console.log(aa) // string
console.log(aa.__proto__) //object.prototype
console.log(c) // function
console.log(c.__proto__) // f() {}
console.log(O)
console.log(cc)
console.log(O.__proto__)
console.log(cc.__proto__)
我們可以看到,每一個(gè)實(shí)例實(shí)際上都是用new去初始化的,即使像我一開始寫的那四種方式,實(shí)際上也可以理解為var a = new ...的語法糖
而prototype代表的也是一個(gè)基類的屬性,實(shí)際上他的實(shí)現(xiàn)也是一個(gè)指針,它指向的即是子類的proto(這樣說不太準(zhǔn)確,更準(zhǔn)確的是這兩個(gè)可以說是嚴(yán)格相等的,兩者都是指針,指向的都是一片包含著基類公共函數(shù)的公共區(qū)域),例如我寫Object.prototype里面就包含了前面舉例用的hasOwnProperty函數(shù),也包含了一系列會(huì)被子類繼承的原型函數(shù),而如果你去寫一條Object.prototype.fuckYou = 'oh my gosh' , 那么你再次打印,就可以看到Object的prototype下面多了一個(gè)fuckYou,而String,number等繼承自O(shè)bject的子類的 proto也多了一個(gè) fuckYou屬性。
當(dāng)然這一點(diǎn)對(duì)于Function類型和Array類型在console出來的時(shí)候似乎是不適用的,但是實(shí)際上,他倆也是符合這條規(guī)則的,我們打印Function.proto的時(shí)候發(fā)現(xiàn)是一個(gè)? () { [native code] }(同理,打印Array的proto時(shí)也是一樣),而我們?nèi)绻褂肍unction.hasOwnProperty發(fā)現(xiàn)是可以調(diào)的到的,不出意外這個(gè)函數(shù)應(yīng)該是搜索原型鏈從Object上搜索出來的(我不相信,JS的編寫者把這個(gè)函數(shù)分別在Function和Array上又實(shí)現(xiàn)了一遍),為什么會(huì)打印出來與基本類型不同的東西,大概這就是混合類型區(qū)別基本類型的地方吧,這一點(diǎn)我還沒有來得及細(xì)細(xì)研究,歡迎大家來回答一下。
所以本質(zhì)上,我們可以看到,這就是一個(gè)new的問題,對(duì)于對(duì)象,new了一個(gè)實(shí)例之后,或者被一個(gè)函數(shù)繼承了之后,這個(gè)實(shí)例或者子類的proto就指向了基類或者父類的prototype。
關(guān)于這堆亂七八糟我也經(jīng)常混淆,這篇知乎講的尤為詳細(xì),必要時(shí)可參考:
https://zhuanlan.zhihu.com/p/23026595
最后還應(yīng)該討論一下這個(gè)“鏈的問題”,他之所以是鏈,是因?yàn)樗且粋€(gè)逐層向上的搜索過程,類似于dom冒泡的那種感覺,如果子類調(diào)用了一個(gè)方法,那么對(duì)于這個(gè)方法將從子類的本身構(gòu)造開始,到子類的proto也就是父類的prototype,再到父類的父類的原型,一直到object再到null(應(yīng)該這兩個(gè)是終極了吧),成為一種鏈?zhǔn)降臓顟B(tài),稱之為原型鏈。
原型鏈與繼承方法
紅寶書里講了一堆啊,什么構(gòu)造函數(shù)方式,工廠模式,寄生構(gòu)造函數(shù),組合繼承,寄生組合繼承等等等(我這里不太想把這些展開,因?yàn)楦杏X一講就要飛到設(shè)計(jì)模式,莫哥說了別這樣)。這些的繼承方式其實(shí)是異曲同工,從根本上來講,他們都是從
function foo() {
this.a = 1
}
foo.prototype = {
protoFoo: ()=> {
return 6666
}
}
let ex = new foo()
ex.protoFoo() // ->6666
這樣的類型脫胎而來的,只不過,前人用各種設(shè)計(jì)模式的方法,基本上模擬出來了類的extend,public,private,protected等等js中本來不存在的關(guān)鍵字的功能
具體的可以看這篇文章,還是蠻詳細(xì):
https://zhuanlan.zhihu.com/p/24964910
from JS to TS
ok,我們現(xiàn)在在寫TS,小伙伴們怕是好久沒有接觸到原型鏈這個(gè)東西了吧,感覺很爽因?yàn)樵僖膊挥孟裎乙粯釉趯戇@篇文章的示例的時(shí)候一樣糾結(jié)啦~
但是實(shí)際上,很多東西,都只是語法糖,你之所以不用糾結(jié),是因?yàn)?,微軟的工程師們替你糾結(jié)了。
如下是我寫的一段示例TS
class A {
protected test1!: number
protected test2: string = ''
private x: number = 1
constructor() {
this.test1 = Infinity
}
public foo1() {
if (this.x > 0) {
this.x - this.x * (1 / 1000)
this.foo1()
} else {
const result = (1 + this.x ^ (1 / 2) - 1) * 2 / this.x
return result
}
}
}
class B extends A {
constructor() {
super()
this.test1 = 1
}
}
let a = new A()
let b = new B()
b.foo1()
而下面是編譯之后的JS
"use strict";
var __extends = (this && this.__extends) || (function () {
var extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
return function (d, b) {
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
var A = /** @class */ (function () {
function A() {
this.test2 = '';
this.x = 1;
this.test1 = Infinity;
}
A.prototype.foo1 = function () {
if (this.x > 0) {
this.x - this.x * (1 / 1000);
this.foo1();
}
else {
var result = (1 + this.x ^ (1 / 2) - 1) * 2 / this.x;
return result;
}
};
return A;
}());
var B = /** @class */ (function (_super) {
__extends(B, _super);
function B() {
var _this = _super.call(this) || this;
_this.test1 = 1;
return _this;
}
return B;
}(A));
var a = new A();
var b = new B();
b.foo1(); // ->可以運(yùn)行
稍微閱讀一下代碼,可以看出, ts的編寫者使用了寄生式組合繼承的方式實(shí)現(xiàn)了一個(gè)extends函數(shù),將其作為了一個(gè)js本來原型繼承的一個(gè)語法糖。然后在你寫到extend的時(shí)候,大概是進(jìn)行了正則匹配句法分析詞法分析的編譯原理工作,檢測到了你的繼承,然后就會(huì)調(diào)用這個(gè)全局__extend函數(shù),并且將從java而來的super方式引入,將其作為一個(gè)回調(diào)函數(shù)的默認(rèn)參數(shù)傳入,讓其獲取到父類的上下文,并在子類的構(gòu)造函數(shù)中執(zhí)行一遍,這樣符合了我上面所說的繼承的本質(zhì) ---- 將父類的東西讓子類可以用。而可以看到這樣實(shí)現(xiàn)之后,子類是可以調(diào)到父類的函數(shù)的,不用使用b.a.foo的方式,也闡釋了我們上面對(duì)于原型方式模擬類式繼承的理論。
可以看到,在編譯之后的TS廣泛的使用了proto和prototype,并在父類 A進(jìn)行了一系列的類型判斷和屬性set,而這僅僅是實(shí)現(xiàn)了一個(gè)繼承而已,如果要實(shí)現(xiàn)TS中向強(qiáng)類型語言學(xué)習(xí)的泛型,多態(tài),寫出來的代碼可謂成倍量的提升,而且TS是經(jīng)過檢驗(yàn)的,可以說考慮到了很多邊際情況。如果我們自己寫這樣的js,或者真的可以寫出來,但是如果遇到了我上文所說的一些神經(jīng)病邊際情況(諸如typeof null = object)那么等待著你的很可能是’咋肥四鴨‘
