js技巧篇--钩子写法

@wengjq 2017-10-17 05:52:59发表于 wengjq/Blog 技巧

无论是开发复杂的业务还是写有很多兼容情况的代码都免不了复杂的 if else if 代码,但是有时 if else if 写的多有可能就变成一堆乱麻,可读性越来越差,有没有更好的方式来组织代码呢?下面已一个 js 判断类型的例子来演示 hook 到底如何使用。

// 当然这里只是示例,为什么不转成小写,因为其他场景不是那么刚刚好。。。
function type (obj) {
   var t = Object.prototype.toString.call(obj).slice(8, -1);
   if (t === "Array") {
       return "array"
   } else if (t === "Object") {
       return "object"
   } else if (t === "String") {
       return "string"
   } else if (t === "Date") {
       return "date"
   } else if (t === "RegExp") {
       return "regexp"
   } else if (t === "Function") {
       return "function"
   } else if (t === "Boolean") {
       return "boolean"
   } else if (t === "Number") {
       return "number"
   } else if (t === "Null") {
       return "null"
   } else if (t === "Undefined") {
       return "undefined"
   }
} 

上面的流程如下图:

可以看到,在其中判断类型的代码中,运用了很多 else if,如果现在类型发生变化,又多了其他一些类型,那么 else if 势必越来越复杂,往后维护代码也将越来越麻烦,成本很大,那么这个时候如果使用钩子机制,该如何做呢?

// 新的类型只需要在钩子里添加
var typeHook = {
    'Array': 'array',   
    'Object': 'object',
    'String': 'string',
    'Date': 'date'
    ...
}

function type (obj) {
   var t = Object.prototype.toString.call(obj).slice(8, -1);
   typeHook[t];
} 

可以看到,使用钩子去处理多种情况,可以让代码的逻辑更加清晰,省去大量的条件判断,上面的钩子机制的实现方式,采用的就是表驱动方式,就是我们事先预定好一张表(俗称打表),用这张表去适配多种情况。我们来看看 jquery 的 type 判断怎么写的。

(function(window, undefined) {
    var
        // 用于预存储一张类型表用于 hook
        class2type = {};
        
    // 针对一些特殊的对象(例如 null,Array,RegExp)也进行精准的类型判断
    // 运用了钩子机制,判断类型前,将常见类型打表,先存于一个 Hash 表 class2type 里边
    jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
        class2type["[object " + name + "]"] = name.toLowerCase();
    });
 
    jQuery.extend({
        // 确定JavaScript 对象的类型
        type: function(obj) {
 
            if (obj == null) {
                return String(obj);
            }
            // 利用事先存好的 hash 表 class2type 作精准判断
            // 这里因为 hook 的存在,省去了大量的 else if 判断
            return typeof obj === "object" || typeof obj === "function" ?
                class2type[core_toString.call(obj)] || "object" :
                typeof obj;
        }
    })
    
    //class2type表是这样的
    {
        "[object Array]": "array",
        "[object Boolean]": "boolean",
        "[object Date]": "date",
        "[object Error]": "error",
        "[object Function]": "function",
        "[object Number]": "number",
        "[object Object]": "object",
        "[object RegExp]": "regexp",
        "[object String]": "string"
    }
    
})(window);

上述每个分支其实是从上到下依次判断,最后仅走入其中一个,对于这种简单的判定没有问题。如果每个 else if 分支都包含了复杂的条件判断,且其对执行的先后顺序有所要求。这时我们可以用钩子函数来解决。

var hook = {
    get: function(elem) {
        return "something";
    },
    set: function(elem, value) {
        // do something with value
    }
}

流程图如下:

从某种程度上讲,钩子是一系列被设计为以你自己的代码来处理自定义值的回调函数。有了钩子,你可以将差不多任何东西保持在可控范围内。从设计模式的角度而言,这种钩子运用了策略模式。

策略模式:将不变的部分和变化的部分隔开是每个设计模式的主题,而策略模式则是将算法的使用与算法的实现分离开来的典型代表。使用策略模式重构代码,可以消除程序中大片的条件分支语句。在实际开发中,我们通常会把算法的含义扩散开来,使策略模式也可以用来封装一系列的“业务规则”。只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以使用策略模式来封装他们。

策略模式的优点:

  • 策略模式利用组合,委托和多态等技术思想,可以有效的避免多重条件选择语句;
  • 策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的函数中,使得它们易于切换,易于理解,易于扩展。
  • 策略模式中的算法也可以复用在系统的其它地方,从而避免许多重复的复制粘贴工作