2018-10-29 關(guān)于js原型鏈的討論

起源:為什么使用原型鏈

使用原型鏈?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è)講的比較有道理,簡單的講解了為什么使用原型鏈


下載.png

但是我感覺還是沒有說清楚,所以我大膽假設(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的丑爆了好吧,圖源阮一峰)


bg2011062401.jpg

我不知道為什么后來有了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

FireShot Capture 2 - New Diagram_ - https___online.visual-paradigm.com_.png

簡單來說,我覺得這就是一個(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)那么等待著你的很可能是’咋肥四鴨‘


下載.jpeg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 什么是原型語言 只有對(duì)象,沒有類;對(duì)象繼承對(duì)象,而不是類繼承類。 “原型對(duì)象”是核心概念。原型對(duì)象是新對(duì)象的模板,...
    zhoulujun閱讀 2,441評(píng)論 0 12
  • JS中原型鏈,說簡單也簡單。 首先明確: 函數(shù)(Function)才有prototype屬性,對(duì)象(除Object...
    前小白閱讀 4,066評(píng)論 0 9
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 3,077評(píng)論 4 14
  • 車墩-下一站,亭林鎮(zhèn) 我和高女士相約在上海博物館看大英博物館百物展。 也不是對(duì)博物館感興趣,只不過兩個(gè)人無聊,又恰...
    謝七閱讀 289評(píng)論 1 2
  • 想你 念你 想你念我 我想 我念 我想念你
    SKTT1BBC閱讀 221評(píng)論 0 0

友情鏈接更多精彩內(nèi)容