JavaScript设计模式 笔记

119 阅读10分钟

JavaScript设计模式

概要

  • 编写可维护的代码

    • 可读的
    • 风格一致的
    • 可预测的
    • 看起来像是同一个人写的
    • 有文档的
  • 减少全局变量

for循环

  • document.images // 页面中所有的IMG元素
  • document.links
  • document.forms
  • document.forms[0].elements

备注

  • switch 模式对齐 case switch
  • 避免隐式转换
  • 使用parseInt 进行数字转换
  • 代码规范
  • 命名规范
  • 写注释
  • 写api文档
  • 编写易读代码
  • 同事评审
  • 发布时代码压缩
  • 运行JSLINT

函数

回调函数

func(callback)

函数为参数,执行时作为回调函数执行

重定义函数

函数内重新定义函数,修改函数指向

即时函数

(function(){})()

  • 使用函数表达式定义一个函数。(不能使用函数声明。)

  • 在最后加入一对括号,这会使函数立即被执行。

  • 把整个函数包裹到一对括号中(只在没有将函数赋值给变量时需要)。

    好处和用法

    即时函数应用很广泛。它可以帮助我们做一些不想留下全局变量的工作。所有定义的变量都只是即时函数的本地变量,你完全不用担心临时变量会污染全局对象。

对象即时初始化

还有另外一种可以避免污染全局作用域的方法,和前面描述的即时函数相似,叫做“对象即时初始化”模式。这种模式使用一个带有init()方法的对象来实现,这个方法在对象被创建后立即执行。初始化的工作由init()函数来完成。

({
    // 这里可以定义一些设置项,比如常量
    maxwidth: 600,
    maxheight: 400,
    // 你也可以定义一些方法
    gimmeMax: function () {
        return this.maxwidth + "x" + this.maxheight;
    },
    // 初始化
    init: function () {
        console.log(this.gimmeMax());
        // 更多的初始化任务……
    }
}).init();
({...}).init();
({...}.init());

这种模式的好处和即时函数模式是一样的:在做一些一次性的初始化工作的时候保护全局作用域不被污染。从语法上看,这种模式似乎比即时函数要复杂一些,但是如果你的初始化工作比较复杂(这种情况很常见),它会给整个初始化工作一个比较清晰的结构。比如,一些私有的辅助性函数可以被很轻易地看出来,因为它们是这个临时对象的属性,但是如果是在即时函数模式中,它们很可能只是一些散落的函数。

这种模式的一个弊端是,JavaScript压缩工具可能不能像压缩一段包裹在函数中的代码一样有效地压缩这种模式的代码。这些私有的属性和方法不被会重命名为一些更短的名字,因为从压缩工具的角度来看,保证压缩的可靠性更重要。在写作本书的时候,Google出品的Closure Compiler的“advanced”模式是唯一会重命名立即初始化的对象的属性的压缩工具。一个压缩后的样例是这样:

条件初始化

条件初始化(也叫条件加载)是一种优化模式。当你知道某种条件在整个程序生命周期中都不会变化的时候,那么对这个条件的探测只做一次就很有意义。浏览器探测(或者特征检测)是一个典型的例子。

// 优化后的代码
// 接口
var utils = {
    addListener: null,
    removeListener: null
};
// 实现
if (typeof window.addEventListener === 'function') {
    utils.addListener = function (el, type, fn) {
        el.addEventListener(type, fn, false);
    };
    utils.removeListener = function (el, type, fn) {
        el.removeEventListener(type, fn, false);
    };
} else if (typeof document.attachEvent === 'function') { // IE
    utils.addListener = function (el, type, fn) {
        el.attachEvent('on' + type, fn);
    };
    utils.removeListener = function (el, type, fn) {
        el.detachEvent('on' + type, fn);
    };
} else { // older browsers
    utils.addListener = function (el, type, fn) {
        el['on' + type] = fn;
    };
    utils.removeListener = function (el, type, fn) {
        el['on' + type] = null;
    };
}

记忆模式

函数也是对象,所以它们可以有属性。事实上,函数也确实本来就有一些属性。比如,对一个函数来说,不管是用什么语法创建的,它会自动拥有一个length属性来标识这个函数期待接受的参数个数:

function func(a, b, c) {}
console.log(func.length); // 3
var myFunc = function (param) {
    if (!myFunc.cache[param]) {
        var result = {};
        // ……复杂的计算……
        myFunc.cache[param] = result;
    }
    return myFunc.cache[param];
};
// 缓存
myFunc.cache = {};

上面的代码假设函数只接受一个参数param,并且这个参数是原始类型(比如字符串)。如果你有更多更复杂的参数,则通常需要对它们进行序列化。比如,你需要将arguments对象序列化为JSON字符串,然后使用JSON字符串作为cache对象的key:

var myFunc = function () {
    var cachekey = JSON.stringify(Array.prototype.slice.call(arguments)),
        result;
    if (!myFunc.cache[cachekey]) {
        result = {};
        // ……复杂的计算……
        myFunc.cache[cachekey] = result;
    }
    return myFunc.cache[cachekey];
};
// 缓存
myFunc.cache = {};

需要注意的是,在序列化的过程中,对象的“标识”将会丢失。如果你有两个不同的对象,却碰巧有相同的属性,那么他们会共享同样的缓存内容。

前面代码中的函数名还可以使用arguments.callee来替代,这样就不用将函数名硬编码。不过尽管现阶段这个办法可行,但是仍然需要注意,arguments.callee在ECMAScript5的严格模式中是不被允许的:

var myFunc = function (param) {
    var f = arguments.callee,
        result;
    if (!f.cache[param]) {
        result = {};
        // ……复杂的计算……
        f.cache[param] = result;
    }
    return f.cache[param];
};
// 缓存
myFunc.cache = {};

配置对象

配置对象模式是一种为自己的代码提供更简洁的API的方法,如果你正在写一个即将被其它程序调用的类库之类的代码的时候就特别有用。

var conf = {
    username: "batman",
    first: "Bruce",
    last: "Wayne"
};
addPerson(conf);

配置对象模式的好处是:

  • 不需要记住参数的顺序
  • 可以很安全地跳过可选参数
  • 拥有更好的可读性和可维护性
  • 更容易添加和移除参数

配置对象模式的坏处是:

  • 需要记住参数的名字
  • 参数名字不能被压缩

柯里化(curry)

函数应用

在一些纯粹的函数式编程语言中,对函数的描述不是被调用(called或者invoked),而是被应用(applied)。在JavaScript中也有同样的东西——我们可以使用Function.prototype.apply()来应用一个函数,因为在JavaScript中,函数实际上是对象,并且他们拥有方法。

// 定义函数
var sayHi = function (who) {
    return "Hello" + (who ? ", " + who : "") + "!";
};
// 调用函数
sayHi(); // "Hello"
sayHi('world'); // "Hello, world!"
// 应用函数
sayHi.apply(null, ["hello"]); // "Hello, hello!"
apply(this,[param]) //this指向 ,param 数组对象参数
call(a,b,c,d,e) //a this指向 其余为参数
部分应用
var add = function (x, y) {
    return x + y;
};
// 完整应用
add.apply(null, [5, 4]); // 9
// 部分应用
var newadd = add.partialApply(null, [5]);
// 为新函数传入一个参数
newadd.apply(null, [4]); // 9
//partialApply()部分应用
柯里化

什么是js函数的currying/柯里化

说道js的柯里化,相信很多朋友都会头大,或者不是很清楚。用一句话总结柯里化,js柯里化是逐步传值,逐步缩小函数的适用范围,逐步求解的过程。

柯里化这个名字来自数学家Haskell Curry。(Haskell编程语言也是因他而得名。)柯里化是一个变换函数的过程。柯里化的另外一个名字也叫schönfinkelisation,来自另一位数学家——Moses Schönfinkelisation——这种变换的最初发明者。

所以我们怎样对一个函数进行柯里化呢?其它的函数式编程语言也许已经原生提供了支持并且所有的函数已经默认柯里化了。在JavaScript中我们可以修改一下add()函数使它柯里化,然后支持部分应用。

// 柯里化过的add()方法,可以接受部分参数
function add(x, y) {
    var oldx = x, oldy = y;
    if (typeof oldy === "undefined") { // 部分应用
        return function (newy) {
            return oldx + newy;
        };
    }
    // 完整应用
    return x + y;
}
// 测试
typeof add(5); // "function"
add(3)(4); // 7
// 创建并保存函数
var add2000 = add(2000);
add2000(10); // 2010

在这些例子中,add()函数自己处理了部分应用。有没有可能用一种更为通用的方式来做同样的事情呢?换句话说,我们能不能对任意一个函数进行处理,得到一个新函数,使它可以处理部分参数?下面的代码片段展示了一个通用函数的例子,我们叫它schonfinkelize(),它正是用来做这个的。我们使用schonfinkelize()这个名字,一部分原因是它比较难发音,另一部分原因是它听起来比较像动词(使用“curry”则不是那么明确),而我们刚好需要一个动词来表明这是一个函数转换的过程。

这是一个通用的柯里化函数:

function schonfinkelize(fn) {
    var slice = Array.prototype.slice,
    stored_args = slice.call(arguments, 1);
    return function () {
        var new_args = slice.call(arguments),
        args = stored_args.concat(new_args);
        return fn.apply(null, args);
    };
​

小结

在JavaScript中,对开发者在函数这个话题的理解和运用的要求是比较苛刻的。在本章中,主要讨论了有关函数的一些背景知识和术语。介绍了JavaScript函数中两个重要的特性,也就是:

  1. 函数是一等对象,他们可以被作为值传递,也可以拥有属性和方法。
  2. 函数拥有本地作用域,而大括号不产生块级作用域。另外需要注意的是,变量的声明会被提前到本地作用域顶部。

创建一个函数的语法有:

  1. 具名函数表达式
  2. 匿名函数表达式(和上一种一样,但是没有名字),也就是为大家熟知的“匿名函数”
  3. 函数声明,与其它语言的函数语法相似

在介绍完背景和函数的语法后,介绍了一些有用的模式,按分类列出:

  1. API模式,它们帮助我们为函数给出更干净的接口,包括:

    • 回调模式

      传入一个函数作为参数
      
    • 配置对象

       帮助保持函数的参数数量可控
      
    • 返回函数

       函数的返回值是另一个函数
      
    • 柯里化

       新函数在已有函数的基础上再加上一部分参数构成
      
  2. 初始化模式,这些模式帮助我们用一种干净的、结构化的方法来做一些初始化工作(在web页面和应用中非常常见),通过一些临时变量来保证不污染全局命名空间。这些模式包括:

    • 即时函数

       当它们被定义后立即执行
      
    • 对象即时初始化

       初始化工作被放入一个匿名对象,这个对象提供一个可以立即被执行的方法
      
    • 条件初始化

       使分支代码只在初始化的时候执行一次,而不是在整个程序生命周期中反复执行
      
  3. 性能模式,这些模式帮助提高代码的执行速度,包括:

    • 记忆模式

       利用函数的属性,使已经计算过的值不用再次计算
      
    • 自定义函数

       复制代码 重写自身的函数体,使第二次及后续的调用做更少的工作
      

对象创建模式

命名空间模式

使用命名空间可以减少全局变量的数量,与此同时,还能有效地避免命名冲突和前缀的滥用。

// 重构后:一个全局变量
// 全局对象
var MYAPP = {};
// 构造函数
MYAPP.Parent = function () {}; 
MYAPP.Child = function () {};
// 一个变量
MYAPP.some_var = 1;
// 一个对象容器
MYAPP.modules = {};
// 嵌套的对象
MYAPP.modules.module1 = {}; 
MYAPP.modules.module1.data = {a: 1, b: 2}; 
MYAPP.modules.module2 = {};
// 不安全的做法
var MYAPP = {};
// 更好的做法
if (typeof MYAPP === "undefined") {
    var MYAPP = {}; 
}
// 简写
var MYAPP = MYAPP || {};
--------------------------------------------------------------------------
// 使用命名空间函数
MYAPP.namespace('MYAPP.modules.module2');
// 等价于:
// var MYAPP = {
//     modules: {
//         module2: {}
//     }
// };