天天看点

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文档甚至是看源码之外毫无他法。

继续阅读