天天看點

PIXI.js源碼解析(6)-js文法特性

在js實作面向對象架構的一些技巧

保護構造函數不被亂調用

在pixi的源碼中有如下的一個内部函數,通過檢查this指針的指向來進行檢查,其原理是不加new調用構造函數時,this就指向一個全局單例對象(在浏覽器環境中則是window)。而加new調用時,this指向構造函數對應的對象。

(PS:更深入的解釋一下,因為js直接聲明函數預設就是挂載到全局對象上,不加new,就相當于在全局對象中調用該函數,this指向全局對象在正常不過了。但是new這個關鍵字,卻首先在記憶體中建立一個對象,然後傳遞了一個隐式的this指針,指向新建立的對象,是以此時函數内部的this就不再指向全局對象。關鍵是要搞清this不是什麼神秘的東西,無論在什麼語言中,this和一個普通的變量沒什麼兩樣,也和普通變量一樣占據記憶體空間,隻是他的行為在大多數語言裡被隐藏起來了,很多行為都是隐式發生的。是以想要弄明白的,建議用C語言模拟一下面向對象(實作繼承,封裝和多态),手動來傳遞this指針)

function _classCallCheck(instance, Constructor) {
                //檢查this 是否是對應構造函數的執行個體
                if (!(instance instanceof Constructor)) {
                    throw new TypeError("Cannot call a class as a function");
                }
            }
           

用法如下

function Bounds() {
          //在構造函數裡第一句就調用_classCallCheck來檢查this指針
           //防止被當成普通函數調用           
        _classCallCheck(this, Bounds);
//.....

}
           

這裡再多說兩句,關于庫和架構的設計,有些人可能覺得這種檢查沒什麼,就算不檢查偶爾失誤沒加new直接調用了構造函數,可能運作時也不會報錯呢?确實有這種可能,這主要還是看構造函數裡做了什麼,一般來說,肯定會有給this指針指派對應的變量,這就把全局作用域的變量名搞的亂七八糟了,要是不小心覆寫掉一個什麼全局變量,可能就會演化成一個根本找不出的bug,此時報錯的地方根本不在最初應該報錯的地方。

是以在設計庫時就有一個以下原則,那就是及早報錯,凡是不根據你的本來目的來使用庫的行為,最好全部都直接報錯,雖然這些錯誤可能并不會直接導緻一個運作時錯誤,但很多時候會間接導緻一個難以查明的bug,甚至是一個邏輯Bug(邏輯bug是最難找的bug之一,因為根本沒有報錯資訊。)

稍微舉一個例子,就拿空指針來說吧,假設你設計的一個方法,不接受空指針,但是你在參數為null的時候卻沒有報錯,而是将他直接傳遞給了下一個方法,下一個方法在需要調用該參數的方法的時候,此時才抛出一個空指針錯誤,那麼這個錯誤距離他最開始出現的位置,已經偏離了相當遠了,這就非常容易幹擾你找bug時候的判斷,輕則浪費了更多時間,重則導緻嚴重的代碼邏輯問題。

關于private

衆所衆知js沒有顯式的私有通路控制,這個python也沒有,但問題是js居然還沒有動态語言中比較重要的通路攔截機制,這也就導緻了他不能像python一樣用裝飾模式來勉強實作私有函數和私有屬性。

是以目前比較通用的方法還是使用_字首,加在私有變量和私有函數的名字前面,某些python庫也是這麼做的,然後某些ide居然也支援,他會乖乖的不提示有_字首的變量,你如果調用私有函數的話他還會提示你。因為大家基本還是用ide寫代碼的,是以這種依賴于約定俗成和ide支援的解決方案居然也還可以?反正我寫python的時候,确實沒感覺出有多少問題,畢竟顯式支援private的語言例如java,你如果想強行調用私有函數和通路私有變量也可以強行用反射來實作,是以我覺得私有通路關鍵字最大的作用還是在開發的時候給開發者一個提示。

但那是python(我用的python ide是pycharm)。說回js,因為它本身的文法就難以parse,目前連一個能正常提示的ide都找不出(我用過的每一個js ide 都會把無論屬不屬于他自身的變量和函數一股腦的提示出來),如果連基本的提示也做不到,更不用談隐藏私有變量了,是以對于js來說,隻能是靠開發者自己遵守潛規則了。

PIXI.js源碼解析(6)-js文法特性

可以看到,pixi.js的一些内部函數前面都加上了_字首

pixi.js裡return語句有這種寫法

return this._filters && this._filters.slice();
           

類似這種寫法是先檢查非空然後再調用方法,是

if(xxx!=null){return xxx.dosth()}else{return null}
           

的簡寫,要點就是null檢查一定要放前面

this._filters && this._filters.slice(); 
           

還有一種類似的變體就是

return xx||xx.dosth();
           
他也是
           
if(xx!=null){
    return xx.dosth;
}
else{
    return null;
}
           

的簡寫,還是要注意判斷為null的要放前面,如果調用放前面可能就會報空指針錯,因為調用了null的方法。

在一些語言中有專門針對這種情況的文法糖,例如kotlin的?: ?.之類的操作符,可以把寫法更加簡化。

上面隻能是針對return語句,但是如下面這種常見的情況就沒辦法了

if (this._mask)
        {
            this._mask.renderable = true;
        }
           

如果對象有多個嵌套的子對象,那麼js最安全的方法還是寫一堆嵌套的if來判斷非null,代碼醜是醜了一點,但是也沒有别的辦法。kotlin也有針對這種辦法的簡寫,隻是目前kotlin生成的js還是太淩亂了,我試過幾次之後就受不了了。

--------------------------------------------------------

實際上我覺得寫JS最蛋疼的問題倒不是他的文法很靈活(或者說零散),目前我的主要問題還是他的代碼提示真的亂七八糟,不管是不是這個對象的方法統統給提示出來,無論是用webstorm還是什麼,但是其他的動态語言,雖然準确性不高,但大多數情況下提示還是準确的,例如ruby的rubymine,python的pycharm。是以覺得還是js的文法問題,就算相比其他的動态語言,他文法也是最難做代碼分析最亂的。

pixi中也有所表現,systemRender中有如下代碼

if (typeof options === 'number')
        {
            options = Object.assign({
                width: options,
                height: arg2 || settings.RENDER_OPTIONS.height,
            }, arg3);
        }
           

可以看到他是根據參數的類型,做了一次重載(詳細說就是預設option是一個object類型儲存了所有附加參數,但如果options傳入的是一個number類型,那麼第二個參數變為寬度,第三個參數變為高度,第四個參數變成原來options所代表的意義)。

這種東西除了看源碼看api文檔之外就不可能知道了。但是換做一個靜态語言,用實際的函數重載或者參數預設值馬上就解決了,提示出來就知道哪個地方該填什麼參數。但是js代碼提示不出來,除了記住重載的各個函數之外别無他法。pixi還算好,目前沒有看到哪個函數有超過三個的重載調用,jquery就很誇張了,一個函數内部根據參數類型有七八個重載的多了去了。

我感覺針對js這種代碼提示淩亂的特點,還是少用内部的隐式重載,直接換用不同的名字,看起來會更加清楚一點。

是以也就做不到看方法名和參數直接使用,往往要去查文檔,要記憶一下方法名和各種參數,讓人很頭疼。

特别是js不支援顯式的重載,很多庫實作間接重載函數的方法都是在函數内部判斷argument的長度,以及各個變量的類型,這就更讓人蛋疼了,代碼提示無法定位到準确的參數,除了看api文檔甚至是看源碼之外毫無他法。

繼續閱讀