
看見這個(gè)標(biāo)題有沒有感到神奇?究竟是連小學(xué)生都不如,還是要搞什么哥德巴赫猜想;究竟是人性的扭曲,還是道德的淪喪,敬請收看……咳哄,跑題了。這不是篇技術(shù)博客嘛,怎么搞起數(shù)學(xué)來了!
這確實(shí)還是一篇技術(shù)博客,在我之前的博客中有提到,JS 除 undifined 和 null 以外的數(shù)據(jù)類型都是有對應(yīng)的原型對象的,這一篇,想講講 JS 的類中一些可能很難注意到的有趣的小東西。
先來做個(gè)測試。
let a1 = 10, a2 = Number(10), a3 = new Number(10);
console.log(a1, typeof a1);
console.log(a2, typeof a2);
console.log(a3, typeof a3);

這個(gè)細(xì)節(jié)可能很多人都沒注意到,用原型對象 new 出來的變量,用 typeof 無法正確判斷類型,但想到 JS 中 class 定義的類也會(huì)出現(xiàn)類似的問題,這個(gè)現(xiàn)象就顯得合理了。另外,判斷 class 定義的類時(shí),通常使用 instanceof 關(guān)鍵字,那么這里能否使用呢?
let a1 = 10, a2 = Number(10), a3 = new Number(10);
console.log(a1, a1 instanceof Number);
console.log(a2, a2 instanceof Number);
console.log(a3, a3 instanceof Number);

順便再加一下判斷……

是不是有一種感覺結(jié)果確應(yīng)如此卻又感覺哪里怪怪的,畢竟明明都是數(shù),只是定義方式不同罷了。
首先 JS 為什么要有原型對象,很簡單,JS 在原型對象上綁定了諸多原生方法,在使用基本數(shù)據(jù)類型時(shí)調(diào)用的方法(例如 number 類型的 toFixed 方法,string 類型的 trim 方法),其實(shí)就是向?qū)?yīng)的原生對象借來的。
利用這一點(diǎn),就能來玩點(diǎn)好 van♂ 的了,比如如何實(shí)現(xiàn) 1+1=3。那么先來扯兩個(gè)看似毫無關(guān)聯(lián)的東西。
- JS 類與實(shí)例中的繼承關(guān)系,也就是原型鏈(就不用 ES5 實(shí)現(xiàn)了想想就麻煩)。
簡單定義兩個(gè)類,類 C 繼承于類 P。
class P {
constructor() {
this.p = "P";
}
Pfunc() {
console.log(this.p);
}
}
class C extends P {
constructor() {
super();
this.c = "C";
}
Cfunc() {
console.log(this.c);
}
}

輸出后,首先可以看到類 C 和 P 的靜態(tài)屬性和方法 length、name、arguments,caller 等,如果有 ES5 開發(fā)經(jīng)驗(yàn)就能明白, ES6 的 class 不過是語法糖,JS 的類本質(zhì)上還是函數(shù)。另外,如果想要自定義類的靜態(tài)屬性和方法,只需在類外如 C.x = x、C.y = () => {}形式定義就可以,靜態(tài)顧名思義通過 new 創(chuàng)建的實(shí)例是無法訪問的,而實(shí)例可訪問的方法都存在 prototype 里,更多的 ES5 相關(guān)知識(shí)就不再贅述了。
輸出 C 和 P 主要想看的是__proto__,可以看到 C 的__proto__指向 P,這就是繼承關(guān)系的最直接的表現(xiàn),如果 C 的實(shí)例調(diào)用的方法在 C 的 prototype 中沒有找到,就會(huì)找到 C 的__proto__的 prototype,也就是 P 的 prototype。那如果還沒有呢,可以看到,雖然這里只定義了兩個(gè)類,但 P 的__proto__還是指向了一個(gè)函數(shù),也就是原生構(gòu)造函數(shù),這說明 P 是繼承于這個(gè)函數(shù)的,這也是類的本質(zhì)是函數(shù)的原因,而許多數(shù)據(jù)類型對應(yīng)的原生對象也同樣繼承于這個(gè)函數(shù),同時(shí),這個(gè)函數(shù)的__proto__又指向了一個(gè)對象,而這個(gè)對象不存在__proto__屬性,也就是說這個(gè)對象是最頂層的對象,JS 中一切對象、函數(shù)無論是否是由開發(fā)者定義的,最終都會(huì)指向這個(gè)對象,其中有的會(huì)指向原生構(gòu)造函數(shù)。因此,只要是在__proto__這條鏈上所有對象的 prototype 里的方法,最底層創(chuàng)建的實(shí)例都可以訪問。
注:建議多輸出幾個(gè)不同類型的例子觀察__proto__指向,這里就不一一展示了,如:
console.log([], {});
const a = () => {};
console.log({ a });
console.log({ a: Math.random });
console.log({ Number, Array });
特別說明,[]的__proto__指向的 Array(0),并不是 Array 類,而是 Array 的 prototype,這是因?yàn)閇]是 Array 的實(shí)例,而實(shí)例指向類的 prototype。

- 類使用運(yùn)算符的方法。
比如在 C++中,可以針對定義的類重載運(yùn)算符,但其實(shí) JS 中有更方便的方法。在《JavaScript の大數(shù)運(yùn)算のこと》中提到了 Big 框架,假設(shè)現(xiàn)在以及導(dǎo)入框架,來做兩個(gè)運(yùn)算。

誒 new Big 不是一個(gè)實(shí)例嘛,為啥可以做運(yùn)算,以及為啥我自己定義的類不能正確做運(yùn)算?這是因?yàn)?Big 這個(gè)類里,實(shí)現(xiàn)了 valueOf 和 toString 兩個(gè)方法,做運(yùn)算時(shí),JS 會(huì)自動(dòng)調(diào)用這兩個(gè)方法的一個(gè)取返回值,優(yōu)先級是 valueOf 高于 toString。
class P {
constructor() {
this.p = "P";
}
valueOf() {
return 1;
}
toString() {
return 2;
}
}

class P {
constructor() {
this.p = "P";
}
toString() {
return 2;
}
}

class P {
constructor() {
this.p = "P";
}
}

而在沒有實(shí)現(xiàn) valueOf 或 toSting 的情況下,會(huì)沿著__proto__鏈找到第一個(gè)有 valueOf 或 toString 的父類調(diào)用(這一點(diǎn)在面試?yán)飷鄢龈鞣N奇奇怪怪的考題,懂原理就很簡單了),也就是原生構(gòu)造函數(shù)的 toString 方法。
進(jìn)入正題,如文章開頭所說,對于原生對象,使用 new 操作符和直接定義對應(yīng)的數(shù)據(jù)類型,有著非常微妙的區(qū)別,具體原因可以看 ECMAScript 規(guī)范和 JavaScript 引擎相關(guān)知識(shí),由于實(shí)在過于晦澀,我就不介紹了(其實(shí)我也沒看懂)。
那么對于 let n = new Number(10)定義的變量而言,它既然是個(gè)實(shí)例,在做運(yùn)算時(shí)候,就必然會(huì)調(diào)用自己或某個(gè)父類的 valueOf 方法,那么如果我們把 valueOf 重寫掉,那么從理論上講,我們就可以讓四則運(yùn)算得到任意的值。那么來找找 Number 的 valueOf 或 toString 方法。

可以看到在 Number 的 prototype 上,valueOf 和 toString 都有,那么先試試能不能拿到 value。
Number.prototype.valueOf = function (value) {
console.log(value);
return 1;
};

用 function 函數(shù)不用箭頭函數(shù)的原因后續(xù)再說??梢娢覀冸m然自定義了返回,但 value 并不會(huì)傳到函數(shù)里,那如果希望根據(jù) value 值操作返回值該怎么辦呢,可否先將 valueOf 函數(shù)存下來,再調(diào)用獲取 value 呢。
const valueOf = Number.prototype.valueOf;
Number.prototype.valueOf = function () {
let value = valueOf();
console.log(value);
return 1;
};

誒!報(bào)錯(cuò)了,說是 this 找不到,這是因?yàn)?valueOf 取值時(shí),需要從 Number 對象實(shí)例上拿,而拿到實(shí)例需要通過 this,這就是這里為什么用 function 函數(shù),因?yàn)?function 函數(shù)的 this 指向調(diào)用函數(shù)的對象,比如 new Number(10) + 1 觸發(fā)了 valueOf,this 就指向 new Number(10),如果用箭頭函數(shù)的話,由于箭頭函數(shù)沒有 this,this 就會(huì)從父作用域里找,這里就會(huì)指向 Window 而無法指向 new Number(10),另外箭頭函數(shù)同樣因?yàn)闆]有 this,也無法通過 bind、call、apply 改變 this 指向。那么既然 function 里的 this 指向正確,就可以修改變量 valueOf 的 this 指向了。
修改 this 指向可以通過 bind、call、apply,其中 bind 會(huì)返回一個(gè)修改完 this 指向的函數(shù),不會(huì)立即執(zhí)行,而 call 和 apply 會(huì)在修改完 this 指向以后立即執(zhí)行,而 call 和 apply 的區(qū)別就是傳參形式不同,比如修改 this 指向的函數(shù)需要傳入 a、b、c 三個(gè)參數(shù),傳參方式分別為 call(this, a, b, c)、apply(this, [a, b, c])。
const valueOf = Number.prototype.valueOf;
Number.prototype.valueOf = function () {
let value = valueOf.call(this);
return value + 1;
};

至此,本文主題 1+1=3,實(shí)現(xiàn)。めでたしめでたし!