# prototype和__proto__分析

標簽(空格分隔): JAVASCRIPT DEEP


本文總結js里不太好理解的幾個概念:prototype, _proto_, new, Object.create, instanceof, typeof。


對象和實例

先來說明實例和對象,簡單來說對象就是一個概念(抽象的),實例就是物體(實體)。
下面直接上代碼:

// 為了區分對象和原型,下面所有的對象統統用大寫
function A(name,age){this.name=name; this.age=age;}
a = new A("test", 10);
console.log(a.name, a.age); // => "test" 10

a就是一個對象,通過new這個關鍵字把實例變成了一個實例。new后面再說,這里先只考慮prototype
在js里,幾乎一切都是實例,而并非一切都是對象,可以簡單地認為有prototype這個屬性的都是對象。

這里只是粗略地說法,比如你強行給一個變量設置一個prototype屬性,其仍然不是一個對象,其仍然不能執行new操作。只能做99%的情況下,如果有prototype的屬性,就可以認為它是一個對象。

對象可以被實例化,對象可以被繼承。對象本身也是原型,其只不過是被更高層的對象實例化出來的而已。
說到底prototype也無非就是對象的一個屬性而已。這是一個特殊的屬性,我們可以簡單理解為該屬性里放著對象A專門給其實例和后代的實例準備的內容。反之,只有prototype里面的內容才會繼承給實例和子對象,A本身的方法并不會被繼承。對于一個對象,其顯式地通過A.prototype.someFunc來調用prototype屬性里的內容,而對于一個實例,則可以直接采用a.someFunc的方式調用其中的內容。
另外,為了方便開發者訪問實例的對象的prototype屬性,很多瀏覽器都實現了__proto__這個好用的關鍵字。在js中,任何內容都有__proto__這一屬性。之前說了js里幾乎一切都是實例,不過也有例外,比如數字、字符串等基本類型。雖然在使用instanceof的時候可能會返回false,但是這些基礎變量也都有__proto屬性,對應到了合適的類型上,這么做可能主要是為了使用方便吧。需要注意的是,__proto并非js標準,有些場景下可能會報錯。
下面我們來具體看一下prototype屬性的特點。代碼如下:

function A(){};
A.prototype.test = () => console.log("A.prototype.test");
let a = new A();
A.prototype.test(); // => A.prototype.test
//A.test(); // => Error...
A.test = () => console.log("A.test");
A.test() // => A.test
a.test(); // A.prototype.test
a.__proto__.test(); // A.prototype.test
a.test = () => console.log("a.test");
a.test(); // a.test
a.__proto__.test(); // A.prototype.test

可以看到,對于對象A,它是無法直接通過A.test訪問到·prototype里的內容的。對于實例a,如果有test則直接使用自己的test,如果沒有找到則會去其對象的prototype(即__proto__)里找。(當然如果還沒找到會繼續找其父對象的prototype
到現在為止,基本可以理清prototype的含義了,基本就是一個特殊屬性,主要是服務于對象和實例之間的聯系以及對象之間的繼承關系。


繼承關系

上面分析了prototype屬性的作用,但是只分析了其在對象和實例之間的紐帶作用。而它更主要的作用是用于實現繼承,js里的繼承關系就是基于prototype實現的。盡管現在已經引入了class,extends等這些關鍵字,不過這僅僅是語法糖而已,實際還是通過prototype等關鍵字實現的。
開始之前,我們先確認一下怎么才算繼承。
既然要繼承,子對象肯定要有父對象所有的屬性,而且子對象實例化出來的變量應該也是父對象的實例。
具體的實現方法可以參考廖雪峰的JS入門教程,這里給出另一種寫法,本質是一樣的。

function A(){}
function B(props){
    A.call(this,props);
// ...
}
B.prototype = Object.create(A.prototype);
// 修復
B.prototype.constructor = B;

為啥非要通過F=>new F來轉換一遍呢?
直接復制過去了PrimaryStudentStudent就沒區別了啊,你想給PrimaryStudent加一個新方法,你會發現Student也有了該方法。
為啥非要修復最后一個constructor?
事實上,這一步即便不執行,在大部分場景下也不會出問題。為什么一定要保證prototype里的constructor指向對象本身呢?這類似C++里的多態,具體分析可以參考下面的鏈接Stack Overflow關于prototype.constructor的討論。大致就是在基類中操作的某些通用函數需要知道處理誰。

這樣做完以后,所有從B實例化出去的變量都能夠直接使用A的方法,且都是A的實例化。

b = new B()
b.__proto__ === B.prototype // true
b.__protot__.__proto__ === A.prototype // true
b instanceof B // true
b instanceof A // true

如下:

console.log(A.prototype);// => {constructor: f}
//constructor是個函數?
A.prototype.constructor === A // true  wtf?

我們發現對象的prototype屬性有一個constructor屬性等于對象本身。

一等公民——Function

很多文章里都會說,函數是js里的一等公民,之前也就簡單理解為函數比較重要罷了,不過實際拿prototype__proto__試了一下發現,Function確實比較特殊。
具體看下面代碼:

Object.__proto__ === Function.prototype; // true
Function.__proto__ === Function.prototype; // true
Function.__proto__ === Object.__proto__; // true
Function.__proto__.__proto__ === Object.prototype; //true

Function instanceof Object; // true
Object instanceof Function; // true

可以簡單總結為

  • Function和Object都是Function的實例。
  • Function繼承自Object。
  • Function是Object類型。
  • Object是Function類型。

這么設計肯定有原因的,至于具體原因這里就不深入探討了,不過確實只有Function有這些屬性。其他內置的類型,例如Number,Date之類的都沒有這些等式。
從這里可以看到,函數在js里確實比較特殊,這也是為什么經常會看到函數被用作橋梁而其他的類型就不會。

對象擴展

有了prototype的加持,我們可以隨意地對一個現有對象進行擴展,比如下面這段代碼就是給Date加了一個format方法,功能與moment.js里的format類似。

Date.prototype["Format"] = function(fmt) {
        fmt = fmt || "YYYY-MM-dd hh:mm:ss";
        let o = {
            "M+": this.getMonth() + 1, //月份
            "d+": this.getDate(), //日
            "h+": this.getHours(), //小時
            "m+": this.getMinutes(), //分
            "s+": this.getSeconds(), //秒
            "q+": Math.floor((this.getMonth() + 3) / 3), //季度
            S: this.getMilliseconds() //毫秒
        };
        if (/(y+)/.test(fmt))
            fmt = fmt.replace(
                RegExp.$1,
                (this.getFullYear() + "").substr(4 - RegExp.$1.length)
            );
        for (let k in o)
            if (new RegExp("(" + k + ")").test(fmt))
                fmt = fmt.replace(
                    RegExp.$1,
                    RegExp.$1.length == 1
                        ? o[k]
                        : ("00" + o[k]).substr(("" + o[k]).length)
                );
        return fmt;
    }
}

有了這個擴展,我們就可以寫出下面的代碼了:

let d = new Date();
console.log(d.format("yyyy-MM-dd:hh"));
// => 2017-09-09:10
console.log(Date.format("yyyy-MM-dd:hh"));
// TypeError

類似地,我們同樣可以對對象本身進行擴展:

JSON["safeParse"] = function(
        text,
        reviver
    ) {
        try {
            return JSON.parse(text, reviver);
        } catch (e) {
            return null;
        }
    }
}
// parse
JSON.parse("{x:")
// => null

上面兩個例子可以看到,我們需要分清應用的場景,需要針對需求進行擴展。
這么擴展的代價是什么?
這種擴展方式很簡單粗暴,不過這也會給我們帶來一些副作用(雖然大部分情況下都不會涉及)。
先看一下下面的代碼:

// somewhere unkown
Object.prototype.hello = () => console.log("hello");

// doing sth
let a = {};
a.name = "test";
a.age = "21";
a.roler = "adc";

for(let k in a) {
    console.log(a[k].toString());
}
// => test
// => 21
// => adc
// => () => console.log("hello");

這里調用toString()主要是為了清晰地看到問題所在————我們擴展的對象會被in操作符所遍歷,這個在數組上也會有類似的問題(不過數組對象可以使用of來避免這個問題,這也是為什么大部分教程里都會建議使用of來遍歷數組的原因)。前面知道所有的對象都來自Object,所以對Object的擴展會影響到所有的in關鍵字!!!
那怎么解決呢?
為了避免這個問題,后面有了hasOwnProperty這個函數。我們只需要簡單地加一行代碼就行了。

for(let k in a) {
    if(!a.hasOwnProperty(k))continue;
    console.log(a[k].toString());
}

in關鍵字存在的地方務必都加上這一判斷。如果沒有加這個關鍵字,即便現在程序沒有出錯,但是后續開發中一旦有人對關聯的對象做了擴展,這塊代碼就有可能出錯或者輸出跟預期不符,可以設想把上面的hello函數換成Object.prototype.hello = "nevermore",這個時候程序不會報錯,只是輸出錯了。這個時候就看可能出現一些詭異的bug,項目大的時候很難調試。

從上面我們可以看到,利用prototype進行擴展會給系統帶來不小的隱患,尤其是對基礎對象(Object, Funtiong, Array)進行擴展的時候,因為我們永遠也沒法保證其他人的代碼都是安全的。

怎么解決這一隱患呢?
為了解決這一問題,又有了defineProperty這一函數。借用這個函數我們可以更加安全地擴展對象,這里簡單地介紹一下,更加詳細的內容請參考MDN文檔
該函數原型如下:Object.defineProperty(obj, prop, descriptor)

  • obj: 我們需要擴展的對象,例如:Date.prototype
  • prop: 我們需要擴展的函數(或變量)名稱,例如:"format"
  • descriptor: 設置這一新屬性的特征。例如:{enumerable: false, value: () => console.log("hello")} 表示不允許該屬性被枚舉到,即不會被in之類的迭代器遍歷到,且數值設置為一個函數。這個參數有下面6個配置項。
    • configurable: 是否允許修改或者刪除屬性特征。
    • enumerable: 是否允許迭代器遍歷
    • value: 屬性的值。
    • writable: 是否支持修改屬性值。這里的修改和configurable的修改管理的內容不一樣,這里是確定屬性值是否允許修改,而上面是確認配置項是否允許修改。如果配置項允許修改我們可以再次調用defineProperty來把writable修改成true然后再修改該字段內容也是可以的。
    • get 見set
    • set get/set是一對gettersetter,它們是一套和value/writable互斥的配置,不能同時設置,否則會報錯。其中getgetter完全一致,就是相當于吧該屬性變成了一個gettersetter則會在該屬性被修改的時候被調用。

關于上面講到的get/set可以參考下面的代碼:

let _am_ = 0;
Object.defineProperty(Object.prototype, "game", {
  get: function() { return Date.now() },
  set: function(nV) { _am_ = nV + 1; },
});
let a = {};
setInterval(()=>(a.game = a.game) && console.log(`game: ${a.game}`) || console.log(`_am_: ${_am_}`), 1000);
// => game: 1504956090106
// => _am_: 1504956090090
// => game: 1504956091110
// => _am_: 1504956091111

這里就展示get=>set的工作過程,每一次出現a.game=?的操作的時候就會觸發set,可以嘗試把上面的賦值操作去掉,就會發現set不會被觸發(這個時候a.game還是會變化的)。

判斷對象

我們經常會遇到需要判斷參數的類型的場景,比如我們有一個下面的函數:

// 判斷請求的token的格式
handler = rgx => req => rgx.test(req.header("token"));
handler(()=>{});
// Error

為了安全起見,這個時候我們勢必希望能夠判斷rgx的類型。在使用nodejs做服務的時候,類型判斷就是最討厭的問題之一。雖然js動態語言的性質導致了這一問題不可能完全解決,不過js還是提供了一些手段來做基本的類型判斷。
typeof: typeof關鍵字會返回實例的對象,代碼如下:

typeof function(){} // => "function"
typeof 0 // => "number"
typeof null // => "object"
typeof undefined // => "undefined"
typoef "" // => "string"
typeof new RegExp(/^\d{11}$/) // => "object"

基本的類型typeof就可以確定了,不過上面的正則返回的是更底層的"object"。
instanceof: 該關鍵字就是用于判斷類型的。

/\d+/g instanceof RegExp // => true
/\d+/g instanceof Object // => true

關于instanceof的用法跟其他語言里基本一樣,這里不再贅述。
除了上面兩種寫法,還有下面這種寫法:

let typeOf = Object.prototype.toString;
typeOf.call(/^\d{11}$/) // => [object RegExp]
typeOf.apply(/^\d{11}$/) // => [object RegExp]

采用這種方法就可以找到一個對象更細致的類型了,具體哪些就不列了,有興趣自己去嘗試吧。關于call和apply的用法可以參考下面鏈接理解call和apply

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容