JS 中判斷一個(gè)函數(shù)是否是一個(gè)類

我們知道,在 ES2015 之后,JavaScript 終于迎來了標(biāo)準(zhǔn)的定義類的方式,然而在這之前,定義類只能使用函數(shù)定義式加綁 prototype 來實(shí)現(xiàn)。此外,雖然現(xiàn)在 JS 支持直接定義類了,但很多時(shí)候,開發(fā)者還是會(huì)選擇將其轉(zhuǎn)譯成 ES5 的代碼,比如使用 Babel,或者 TypeScript 時(shí)就經(jīng)常這么做。

這篇文章,我們來探討下應(yīng)該如何優(yōu)雅地實(shí)現(xiàn)判斷一個(gè)給定的對(duì)象,是否是一個(gè)類,或者說,可能被當(dāng)作類來識(shí)別(重心在 ES5 風(fēng)格寫的類上,因?yàn)?ES2015 的類很容易判斷)。

如果你使用 VSCode 編輯器來開發(fā),那么當(dāng)你在使用 ES5 的方式定義一個(gè)類時(shí),編輯器會(huì)提示你這個(gè)構(gòu)造函數(shù)可能轉(zhuǎn)換為類,并且提供一鍵轉(zhuǎn)換功能,非常強(qiáng)大。

但是 VSCode 又是怎么實(shí)現(xiàn)的呢?其實(shí)很簡單,VSCode 有很好的語法識(shí)別系統(tǒng),它能根據(jù)上下文分析你的代碼結(jié)構(gòu),從而判斷出你定義的一個(gè)“類構(gòu)造”函數(shù)是否符合類的特征。在這里,我將引用一個(gè)“數(shù)字指紋”的概念來對(duì)其進(jìn)行闡述。因?yàn)榧词刮覀兌x一個(gè)類的方式很多,但實(shí)際上在轉(zhuǎn)換成或者本身就使用 ES5 去定義類的時(shí)候,這個(gè)類會(huì)留下程序能夠識(shí)別的數(shù)字指紋特征。而根據(jù)這個(gè)特征,我們就能夠判斷出這個(gè)最終的函數(shù)是否應(yīng)該是一個(gè)可實(shí)例化的類。

VSCode 利用指紋特征并通過上下文來分析代碼結(jié)構(gòu),這能夠讓它在幾乎 100% 的情況下正確判斷,但是在我們自己的程序中,要做到融合上下文并不容易,并且其實(shí)也不是那么必要。所以接下來我將提供的是一個(gè)簡化的版本,但它依舊能夠提供幾乎 98% 的識(shí)別率。

廢話不多說,直接上代碼:

/**
 * Checks if an object could be an instantiable class.
 * @param {any} obj
 * @param {boolean} strict
 * @returns {boolean}
 */
function couldBeClass(obj, strict) {
    if (typeof obj != "function") return false;

    var str = obj.toString();
    
    // async function or arrow function
    if (obj.prototype === undefined) return false;
    // generator function or malformed definition
    if (obj.prototype.constructor !== obj) return false;
     // ES6 class
    if (str.slice(0, 5) == "class") return true;
    // has own prototype properties
    if (Object.getOwnPropertyNames(obj.prototype).length >= 2) return true;
    // anonymous function
    if (/^function\s+\(|^function\s+anonymous\(/.test(str)) return false;
    // ES5 class without `this` in the body and the name's first character 
    // upper-cased.
    if (strict && /^function\s+[A-Z]/.test(str)) return true;
     // has `this` in the body
    if (/\b\(this\b|\bthis[\.\[]\b/.test(str)) {
        // not strict or ES5 class generated by babel
        if (!strict || /classCallCheck\(this/.test(str)) return true;

        return /^function\sdefault_\d+\s*\(/.test(str);
    }

    return false;
}

exports.couldBeClass = couldBeClass;
exports.default = couldBeClass;

現(xiàn)在,再對(duì)每一行 if 進(jìn)行解釋一下。

第一句不用多數(shù),如果不是函數(shù),肯定返回 false,即使是 ES2015 的類,其類型也是一個(gè) function。

然后。我們對(duì) prototype 進(jìn)行分析,你肯定很好奇什么樣的函數(shù)會(huì)沒有 prototype,沒錯(cuò),就是箭頭(=>)函數(shù)和異步(async)函數(shù)。

接下來,我們判斷 constructor,一個(gè)正常的類,只要它的定義是合法的,那么它的 prototype.constructor 就會(huì)指向類本身,否則它就不能是一個(gè)類,例如生成器函數(shù),其 prototype.constructor 始終指向 GeneratorFunction 基函數(shù),而不是你定義時(shí)的那個(gè)函數(shù)。因此它是不能被當(dāng)作類來使用的,如果你嘗試去 new 一個(gè)迭代器函數(shù),解釋器還會(huì)拋出錯(cuò)誤。

而我什么要強(qiáng)調(diào)類的定義必須是合法的呢?因?yàn)閷?shí)際上,可能是網(wǎng)上教程錯(cuò)誤的原因,導(dǎo)致很多人可能會(huì)像下面這樣去定義一個(gè)“類”:

function MyClass() {}
MyClass.prototype = {
    // ...
}

// 或者在繼承時(shí)
function AnotherClass() {}
AnotherClass.prototype = new MyClass();

請(qǐng)務(wù)必注意這兩種寫法都是錯(cuò)誤的,并且一定不要這么寫。幸好,現(xiàn)在我們都可以用 ES2015 的寫法來定義類了,即使轉(zhuǎn)譯為 ES5,編譯器也會(huì)正確地編譯為正確的姿勢,而不用我們?nèi)タ紤]應(yīng)該怎么實(shí)現(xiàn)。不過我覺得還是值得提一下,下面這種定義方式才是正確的:

function MyClass() {}
MyClass.prototype.show = function show() {
    // ...
}
console.log(MyClass.prototype.constructor === MyClass); // true 永遠(yuǎn)可用并指向類自身

// 繼承
function AnotherClass() {}
Object.setPrototypeOf(AnotherClass, MyClass); // 繼承靜態(tài)方法和靜態(tài)屬性
function Super() { this.construcor =  AnotherClass } 
Super.prototype = MyClass.prototype;
AnotherClass.prototype = new Super(); // 這樣做的好處是不需要傳任何參數(shù)
console.log(AnotherClass.prototype.constructor === AnotherClass); // true

接下來我們繼續(xù)看 str.slice(0, 5) == "class",使用 ES6 定義的類,它的 toString() 方法返回的文本就是我們定義類時(shí)的樣子,不會(huì)轉(zhuǎn)換為函數(shù),因此總是以 class 開頭。

然后我們判斷函數(shù)的 prototype (中屬性和方法)的長度/個(gè)數(shù),之所以條件 >= 2,是因?yàn)橐粋€(gè)類,無論是 ES5 還是 ES2015,永遠(yuǎn)有一個(gè) constructor 屬性在 prototype 中(這是標(biāo)準(zhǔn)函數(shù)類型的特有屬性,除了箭頭函數(shù)和異步函數(shù),如上面所說),也就是其長度永遠(yuǎn)至少有一個(gè),我們不能判斷一個(gè) prototype 中沒有自定義屬性和方法的函數(shù)為一個(gè)類,因?yàn)槠胀ê瘮?shù)是不需要它們的。但當(dāng)它有時(shí),它基本上就是一個(gè)類了。

注意我這里使用了 Object.getOwnPropertyNames 而不是 Object.keys,Object.getOwnPropertyNames 能夠返回通過 Object.defineProperty 定義的屬性(包括 gettersetter),而 Object.keys 則不能。

當(dāng)以上檢測都不通過后,我們只能從函數(shù)體中判斷來判斷了。首先我們要忽略匿名函數(shù)(/^function\s+\(|^function\s+anonymous\(/.test(str)),即函數(shù)定義式中沒有指定名字,或者名字為 anonymous 的函數(shù),這兩種函數(shù)是下面這樣的:

var func = function () {}; // 注意在現(xiàn)代引擎中,`func.name` 為 `func`
var func2 = new Function("", ""); // 注意 `func2.name` 為 `anonymous`

由于 Function.name 在這兩種匿名函數(shù)中都是有值的,因此我們需要通過函數(shù)體來判斷它是否是匿名函數(shù),而不能依靠 Function.name

接下來,我們?cè)僭趪?yán)格模式下判斷函數(shù)名是否首字母為大寫(strict && /^function\s+[A-Z]/.test(str))。之所以這個(gè)判斷在嚴(yán)格模式中,是因?yàn)楝F(xiàn)代建議的 JS 寫法,類名是應(yīng)該要使用首字母大寫的駝峰命名法的,而函數(shù)與方法,則使用首字母小寫的駝峰命名法。因此,嚴(yán)格模式指的是在嚴(yán)格書寫格式的前提下,將所有首字母大寫得函數(shù)識(shí)別為類。

最后,我們通過判斷函數(shù)體中是否存在 this 偽變量來判斷它是否應(yīng)該是一個(gè)函數(shù)還是類(/\b\(this\b|\bthis[\.\[]\b/.test(str)),因?yàn)楹瘮?shù)中是沒有 this 變量的。但它依舊可能是一個(gè)類方法而不是類,因此我們還需要特別判斷,在非 strict 模式時(shí),始終將包含 this 的函數(shù)識(shí)別為類,因?yàn)楦鶕?jù)統(tǒng)計(jì)學(xué),大多數(shù)人寫方法時(shí)不會(huì)指定一個(gè)名稱,而是直接使用匿名函數(shù),例如:

MyClass.prototype.show = function() {}; // 即使是編譯器,也會(huì)生成這樣,而不是 function show() {}

而我們前面已經(jīng)將匿名函數(shù)判斷為 false 了,因此在非嚴(yán)格模式時(shí),我們這么判斷是沒有問題的,而如果啟用嚴(yán)格模式,那么如前面所說的,則會(huì)先判斷是否首字母大寫。如果不通過,我們?cè)倥袛嗪瘮?shù)體中是否存在一個(gè) classCallCheck 函數(shù)的調(diào)用,它是 babel 在轉(zhuǎn)譯時(shí)自動(dòng)插入的(實(shí)際上是 __classCallCheck),用來檢測用戶是否將類當(dāng)作函數(shù)調(diào)用,而這個(gè)行為在標(biāo)準(zhǔn) ES2015 類中則是被禁止的。

最后的最后,我們?cè)诤瘮?shù)體中包含 this 變量的函數(shù),判斷函數(shù)是否是一個(gè)默認(rèn)導(dǎo)出的匿名函數(shù),在 TypeScript 中,轉(zhuǎn)譯后的默認(rèn)導(dǎo)出,不管是匿名類還是匿名函數(shù),都會(huì)生成一個(gè) default_1 的函數(shù)/類名。由于當(dāng)前函數(shù)體中存在 this 變量,而它又是一個(gè)默認(rèn)導(dǎo)出,那么它只能是一個(gè)類。

經(jīng)過這些判斷,我們基本上就能夠比較準(zhǔn)確(98%)地判斷一個(gè)函數(shù)是否為一個(gè)類了,并且它也同時(shí)兼容 Babel 和 TypeScript 轉(zhuǎn)譯后的代碼,因此這個(gè)判斷函數(shù)你可以完全在 Babel 和 TypeScript 項(xiàng)目中使用它,而不用擔(dān)心轉(zhuǎn)譯會(huì)帶來什么缺陷。

更多介紹,請(qǐng)看 GitHub could-be-class

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

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

  • 工廠模式類似于現(xiàn)實(shí)生活中的工廠可以產(chǎn)生大量相似的商品,去做同樣的事情,實(shí)現(xiàn)同樣的效果;這時(shí)候需要使用工廠模式。簡單...
    舟漁行舟閱讀 8,118評(píng)論 2 17
  • 函數(shù)和對(duì)象 1、函數(shù) 1.1 函數(shù)概述 函數(shù)對(duì)于任何一門語言來說都是核心的概念。通過函數(shù)可以封裝任意多條語句,而且...
    道無虛閱讀 4,944評(píng)論 0 5
  • 第2章 基本語法 2.1 概述 基本句法和變量 語句 JavaScript程序的執(zhí)行單位為行(line),也就是一...
    悟名先生閱讀 4,546評(píng)論 0 13
  • 2在我離開學(xué)校選擇職業(yè)生涯里第一份工作的時(shí)候,同學(xué)、朋友都十分驚訝的問我“為什么來iwill”。當(dāng)時(shí)帶著一股義無反...
    西貝小妞閱讀 139評(píng)論 0 0
  • 崔永元者,冀州武邑人也,太祖十四年誕于津門北辰。喜辭令、善雄辯,昔為央視名嘴。其父原系京師都尉,故永元幼四歲徙京。...
    子騫_3a4f閱讀 564評(píng)論 0 0

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