js 最佳实践

Author: yihuang

Contents

对于js框架,除了封装浏览器在dom 事件等方面的差异,还有很重要的一方面是要发掘并固化一些有用的设计模式,并鼓励高质量代码的编写。 本文总结一些我自己在这方面的一些经验,其中有些是从其他开源JS框架中拿来,也有些是受其他语言(比如python)的启发并转移到python上来的。 希望能起到一个抛砖引玉的作用。

对象/命名空间

对于js来说,对象就是一堆名字与值的映射,其实就是一个命名空间。

先设计一个场景,按照最直接的写法如下的一段代码:

var status = 1;
function change_status(){
    if(status==1)
        status = 2;
    else
        status = 1;
}

change_status();

实际上这样的代码并没有什么不好,简单直观,容易理解,只是当代码量,业务逻辑复杂度上去后, 在全局命名空间中积累越来越多的变量名字,容易发生名字冲突。

解决这个问题的一个办法就可以使用对象对不同的模块进行包装

var status = {
    _status : 1,
    change : function(){
        if(this._status==1)
            this._status = 2;
        else
            this._status = 1;
    }
}

status.change();

在一些极端情况下,也会有人甚至会担心别人会“不小心”直接修改了 status._status 的值,虽然在一个正经的团队中, 这种事情发生的可能性微乎其微。其实究竟动态语言是否真的需要强制性的私有变量是一个设计决策的问题, 也引发过很多讨论,这里只是展示一下可能性。

利用闭包实现私有成员

var status = (function(){
    var private_status = 1;
    return {
        change:function(){
            if(private_status==1)
                private_status = 2;
            else
                private_status = 1;
        }
    }
})()

status.change();

上面实现的 status 对象都是单件。但也有一些逻辑,需要能够创建多个实例,重复使用。 js本身即支持这样的做法:

var Status = function(init_status){
    this._status = init_status;
}
Status.prototype.change(){
    if(this._status==1)
        this._status=2
    else
        this._status=1
}

var status1 = new Status(1);
status1.change();
var status2 = new Status(2);
status1.change();

利用闭包实现私有变量:

var Status = function(init_status){
    var private_status = init_status;
    return {
        change:function(){
            if(private_status==1)
                private_status = 2;
            else
                private_status = 1;
        }
    }
}
var status1 = Status(1);
status1.change();
var status2 = Status(2);
status2.change();

函数式编程

js 的风格还是很函数式的,所以学会用函数式的方式来编写 js 对于编写质量高的代码还是很重要的。

当然函数式这个概念涵盖的东西很广,这里只是利用 js 高阶函数(通俗地说就是函数即对象) 的特性提出一些方便编写js代码的范式。

恰好 javascript1.6 规范中给 Array 增加的几个方法就是很好的范例。

// Array.forEach
var content = [];
[1,2,3,4,5,6,7].forEach(function(value, index, array){
    content.push('<li>'+value+'</li>');
})
document.getElementById('container').innerHTML = content.join('');

// Array.filter
[1,2,3,4,5,6,7].filter(function(value, index, array){
    return value>4;
}); // 返回 [5,6,7]

// Array.map
[1,2,3,4,5,6,7].map(function(value, index, array){
    return value * 2;
}); // 返回 [2,4,6,8,10,12,14]

var song_list = [
    {id:1, name:'aaa'},
    {id:2, name:'bbb'},
    {id:3, name:'ccc'}
];
var content = song_list.filter(function(value){
    // 只显示歌曲名长度小于5的
    return value.name.length<5;
}).map(function(value, index, array){
    return '<li>%(index). <a href="#" onclick="listen(%(song.id));return false;">%(song.name)</a></li>'.format({
        index:index,
        song:value
    });
});
document.getElementById('container').innerHTML = content.join('');

Array.some Array.every

TODO

装饰器模式

四人帮装饰器模式在动态语言中的变化。

保持被包装函数的接口不变,对被包装的函数透明的增加逻辑。透明意味着装饰器可以重用,可以给被包装函数同时应用多个装饰器等。

应用场景很丰富,比如:

trace

在进入和退出被包装函数的时候输出日志

function trace(func)
{
    return function(){
        console.log('enter function:'+func.name+' with arguments:'+Array.prototype.join.call(arguments, ','));
        var result = func.apply(this, arguments);
        console.log('function:'+func.name+' return '+result);
        return result;
    }
}
// test
function test(a){
    return new Date(a);
}
test = trace(test);
test(1414234243);
// 输出:
// enter function:test with arguments:1414234243
// function:test returns Sat Jan 17 1970 16:50:34 GMT+0800 (CST)

如果函数都放在命名空间里面,还可以一次性对所有函数启动这个调用跟踪器,在调试时开启, 发布时关闭:

var comm = {
    func1:function func1(){...},
    func2:function func2(){...},
    func3:function func3(){...},
    func4:function func4(){...}
}
if(DEBUG){
    for(var name in comm){
        if(comm[name].constructor = Function){
            comm[name] = trace(comm[name]);
        }
    }
}

这样所有函数都会记录自己的调用的参数和返回值,方便调试。 其实这已经是简单的面向方面编程了。

cache

对于一些需要大计算量的函数,比如需要构造一大块html,可以使用函数的参数作为key对函数的结果自动进行缓存。在第二次用相同的参数调用时,就可以直接从缓存中取数据了。

function cache(func){
    var _cache = {};
    return function(){
        // 如何对参数列表进行hash还值得商榷的
        var key = Array.prototype.join.call(arguments);
        if(key in _cache){
            console.log('cache命中');
            return _cache[key];
        }
        else{
            var result = func.apply(this, arguments);
            _cache[key] = result;
            return result;
        }
    }
}

使用起来也很简单,找个现成的函数比如::

var Cookie = {
    get:function(name){
        ...
    };
    set:function(name,value){
        ...
    };
}
Cookie.get = cache(Cookie.get);
Cookie.set = cache(Cookie.set);
// 然后用相同的参数重复调用同一个函数,第二次就不会重新解析 cookie 字符串了,直接可以从cache中取到结果。
Cookie.get('user_status');
Cookie.get('user_status');

比如现有的代码中就存在一定的对cookie的重复解析,应用上面这个东西可以提高一点性能。

这个方法对于同步拉取数据的函数效果会非常明显,但是对于异步拉取数据的函数就没有办法了,因为异步拉取数据的函数立刻返回。

解决this漂移

Function.prototype.bind = function(owner)
{
    var func = this; // 原函数
    return function(){
        func.apply(owner, arguments);
    }
}

var Obj = {
    a:1
}
Obj.test = function(){
    console.log(this.a);
}

在这个时候,如果调用 Obj.test() 当然没有什么问题,但是如果把 Obj.test 当作 事件处理函数或者ajax回调函数之类的话,this就不确定了,执行结果也很难预料, 或者通过 apply 也可以修改其 this 指向的对象。 这个时候可以通过 bind 固定 this:

Obj.test = Obj.test.bind(Obj); // 这样 Obj.test 就很像一个面向对象语言中的方法了。
Obj.test.apply(null); // apply 不能改变 this
some_element.onclick = Obj.test.bind(Obj); // 事件回调时也不会影响到函数中的 this

还是跟this有关

function AClass(a){
    this.a = a;
}
var obj = new AClass('xxx');
// 但有时会忘记写 new
var obj = AClass('xxx');
// 就出问题了,这个情况下obj还是没有a这个属性,因为这个时候this是window对象,结果定义了一个全局变量,但由于js的动态特性,这个错误并不会立刻显现

如何及早捕捉这个问题呢,可以用一个装饰器在函数的入口增加代码进行判断。

function class(func){
    return function(){
        if(!(this instanceof arguments.callee)){
            warn('error, please use new');
            return null;
        }
        func.apply(this, arguments);
    }
}

AClass = class(AClass); // 函数定义后把 AClass 装饰一下
var obj = AClass('xxx'); // 忘了写 new ,马上就会打出日志

增强arguments对象

在平时的js编程中发现,经常要处理 arguments,实际上 arguments 跟 Array 很像, 但偏偏不带 Array 那些方法,所以常常要写这样的代码:

Array.prototype.join(arguments, ',');

不如一次性把需要的 Array 的方法都加到 arguments 的原型里面去。

(function temp(){
    arguments.constructor.prototype.join = Array.prototype.join;
    // ... 其他方法
})();
// 这样就可以直接使用 arguments 的 join 方法了
function test(){
    return arguments.join(',');
}
test(1,2,3) // 返回 1,2,3

处理初始化代码

一般在 window.onload 中进行初始化,但 window.onload 往往来得太慢,一般页面初始化代码只需要在 dom 加载完毕即可开始执行, 所以这种代码又往往放到页面的结尾加一个 script 标签进行执行。但这种做法还是不够灵活,一来脚本混合到html页面里不利于维护, 二来如果公用的 js 里面想在初始化的时候加点逻辑也很不方便。

所以需要一个事件,引入一个类似 jquery 的 $(document).ready 事件就可以很好地解决问题。

管理全局变量

全局变量常常是不可避免,同时也是维护起来很麻烦的一个东西。主要问题在于可能出现的名字冲突,而且一旦出现,很难发现。 这里提一个解决方案:

global = {
    buildin_names:['buildin_names','define','show'],
    define:function(name,value){
        if(buildin_names.contains(name))
        {
            warn('与系统名字冲突:'+name);
            return;
        }
        if(!name in global)
            global[name] = value;
        else
            warn('冲突的全局变量:'+name);
    },
    show:function(){
        info('全局变量:');
        for(var name in global)
        {
            if(buildin_names.contains(name))
                continue;
            info(name+' : '+value);
        }
    }
}

这个需要和相应的规范一起执行,那就是:

  • 所有全局变量一定要通过 global.define('变量名') 进行定义。
  • 所以全局变量一律通过 global.变量名 进行访问。

这样可以比较好地解决问题。并还可以通过 global.show() 很方便地随时查看当前所有定义过的全局变量!

JS模板(*)

现有代码中大量存在拼接 html 片段的代码,这种代码是最难维护的,引入 js 模板可以极大增强代码可读性。

JS模板我建议可以有两套,一套轻量级的,一套重量级的。 轻量级的做法可以只处理简单的变量替换,通过正则表达式实现起来也很简单。重量级的做法可以加入条件判断、循环等语法。 轻量级的比如:

TODO

deferred 模式管理异步数据拉取(*)

这个参考 mochikit 框架的 async 模块,后者参考的是 python 的一个网络编程框架 twisted 中的异步处理模式。 详细文档可参考 http://mochikit.com/doc/html/MochiKit/Async.html

deferred 模式将一份当前不可用的数据、数据当前的状态、以及相应的回调函数链封装成为一个对象,使得异步 回调的处理变得更加直观。

// 这是一个简化的实现
function load_json(url)
{
    var deferred = new Deferred();
    loadJsonData(url, function(data){
        deferred.callback(data);
    }, function(err){
        deferred.errback(err);
    });
    return deferred;
}

// 如何使用
var some_deferred = load_json('...');
some_deferred.addCallback(function(data){
    // 处理数据
});
some_deferred.addErrback(function(err){
    // 处理错误
});

利用 deferred 模式可以方便地处理一些过去会很麻烦的异步加载数据的场景。

  • 同一份数据对应多处回调 有的时候一份公用的数据,比如获取用户状态信息,可能很多地方需要使用,当然,情况简单的时候,我们可以尽量地把 所有需要操作用户状态信息的代码集中放到一个js回调函数中来。但是为了要增强代码模块性和可重用性,很可能一个通用的处理逻辑 放在公用js中,而某些页面需要做一些特殊的处理而把逻辑放在页面自己的js中。这样就不好处理了。deferred 可以很好地处理这样的场景。

    在这种情况下,公用数据只需要提供一个全局的 deferred 对象即可,其他js都可以引用这个 deferred 对象增加回调处理函数。:

    global.define('deferred_user_info');
    function load_user_info(){
        global.deferred_user_info = load_json('http://....get_user_info.fcg');
    }
    
    ...
    
    global.deferred_user_info.addCallback( success_callback1 );
    global.deferred_user_info.addErrback( fail_callback1 );
    global.deferred_user_info.addCallback( success_callback2 );
    global.deferred_user_info.addErrback( fail_callback2 );
    ...
    

    回调函数按照注册的顺序执行。

  • 多个地方需要获取同一份数据 为了提高用户响应速度,前台可以在页面加载完毕就立刻对一些用户将要获取的数据进行预读,放到一个全局变量里,这样当用户真的操作到这里的时候, 数据可以直接读一个变量立即展示。

    但是由于加载数据有快慢,用户操作的到这里的时候,数据有可能还在预读中,还没有加载完毕。这个情况按照简单的做法就不太好解决。 使用 deferred 的话就是这样的::

    // 定义读取数据函数
    global.define('deferred_user_info');
    function load_user_info(){
        global.deferred_user_info = load_json('http://....get_user_info.fcg');
    
    }
    
    ...
    
    // 页面加载完毕的时候进行预读
    global.define('user_info');
    $(document).ready(function(){
        load_user_info();
        global.deferred_user_info.addCallBacks(function(data){
            global.user_info = data;
        }, function(err){
            global.user_info = null;
        });
    });
    
    ...
    
    // 用户实际执行一个依赖用户信息这个数据的一个操作的时候
    // 过去很难处理的地方
    function some_event_handler(){
        function show_user_info(user_info){
            //操作用户信息数据
            ...
        }
        function fail_callback(){
            // 处理错误
        }
        // 如果数据已加载完毕,直接操作
        if(global.deferred_user_info.status == 'loaded'){
            show_user_info(global.user_info);
        }
        else if(global.deferred_user_info.status == 'failed'){
            fail_callback();
        }
        // 否则,增加一个回调函数。
        else{
            global.deferred_user_info.addCallBacks(show_user_info);
        }
    }