Better

Ethan的博客,欢迎访问交流

如何构建自己的工具函数库

在工作中经常有写一些独立的功能模块,对于如何抽象出来构建自己的工具函数库也有些思考,在阅读完资料中的介绍后,有很多醒悟的地方,代码的健壮性,并非一蹴而就,而是汇集了很多人的经验,考虑到了很多我们意想不到的地方。

自己实现

在自己的工作中,我有可能会这么做

(function(app){

    var _ = {};

    app._ = _;

    // 在这里添加自己的方法
    _.reverse = function(string){
        return string.split('').reverse().join('');
    }

})(window)

或者这么操作,直接指向this

(function(){
    var root = this;

    var _ = {};

    root._ = _;

    // 在这里添加自己的方法
    _.reverse = function(string){
        return string.split('').reverse().join('');
    }

})()

整篇文章看完之后才发现,这玩意写的真垃圾,垃圾在哪些地方呢:

  • 对象挂载到全局对象没有对环境做检测,使用情况有限
  • 能否支持面向对象风格,而不仅是函数风格
  • 到处没有考虑到JavaScript模块化下的导出方案

接下来看如何一步一步健壮吧!

全局对象选择

主要需要考虑的情况有如下几种:

  • 在严格模式下,this 返回 undefined,而不是指向Window,且在 ES6 中模块脚本自动采用严格模式,不管有没有声明 use strict。
  • Node环境中,全局对象不是Window,而是global
  • Web Worker 属于 HTML5 中的内容,Web Worker 处在一个自包含的执行环境中,无法访问 Window 对象和 Document 对象,和主线程之间的通信业只能通过异步消息传递机制来实现。
    • typeof window 和 typeof global 的结果都是 undefined
    • 能通过 self 访问到 Worker 环境中的全局对象。且在浏览器中,除了 window 属性,我们也可以通过 self 属性直接访问到 Winow 对象。
  • 在 node 的 vm 模块中,也就是沙盒模块,runInContext 方法中,是不存在 window,也不存在 global 变量的,但是我们却可以通过 this 访问到全局对象
  • 在微信小程序中,window 和 global 都是 undefined,加上又强制使用严格模式,this 为 undefined,挂载就会发生错误,此时直接赋值为{}

最终代码:

var root = (typeof self == 'object' && self.self == self && self) ||
           (typeof global == 'object' && global.global == global && global) ||
           this ||
           {};

函数对象

如果仅仅设置 _ 为一个空对象,我们调用方法的时候,只能使用 _.reverse('hello') 的方式,实际上,underscore 也支持类似面向对象的方式调用,即:

_('hello').reverse(); // 'olleh'

这里underscore的实现方式让我感触很深,可以说是一个比较综合的东西了,实现步骤如下:

  1. _不能是字面量对象,而是一个函数,_函数需要返回一个对象,
    var _ = function(obj) {    
     // 如果不是通过new的方式调用,则强行new
     if (!(this instanceof _)) return new _(obj);
     // 将参数保存为内部属性
     this._wrapped = obj;
    };
    
  2. 此时返回_实例,但是无法调用 _ 函数对象上的方法的,因为没有挂载方法到函数原型上

    1. 找出_对象上所有函数

      _.functions = function(obj) {
      var names = [];
      for (var key in obj) {
        if (_.isFunction(obj[key])) names.push(key);
      }
      return names.sort();
      };
      
    2. mixin

      var ArrayProto = Array.prototype;
      var push = ArrayProto.push;
      _.mixin = function(obj) {
      // 循环所有方法
      _.each(_.functions(obj), function(name) {
        var func = _[name] = obj[name];
        // 将方法挂载到原型上
        _.prototype[name] = function() {
            // 合并参数
            var args = [this._wrapped];
            push.apply(args, arguments);
            // 返回调用结果
            return func.apply(_, args);
        };
      });
      return _;
      };
      _.mixin(_);
      

值得注意的是:因为 _[name] = obj[name] 的缘故,我们可以给 underscore 拓展自定义的方法:

_.mixin({
  addOne: function(num) {
    return num + 1;
  }
});

导出

直接看源码,考虑到模块化情况:

if (typeof exports != 'undefined' && !exports.nodeType) {
    if (typeof module != 'undefined' && !module.nodeType && module.exports) {
        exports = module.exports = _;
    }
    exports._ = _;
} else {
    root._ = _;
}

在这里我们回忆下在node中模块化的方案

  1. 每一个node.js执行文件,都自动创建一个module对象,同时,module对象会创建一个叫exports的属性,初始化的值是 {}
  2. exports和module.exports指向同一块内存,但require()返回的是module.exports而不是exports。
    • 因此我感觉exports存在的意义?简写?
    • API发生了改变?
  3. 一切都很美好,但是如果你不是对exports或module.exports指向的内存中做了修改,而是进行了覆盖,将其指向一块新的内存,就会打断两者之间的引用关系,由于require()返回的是module.exports,因此如果在exports上进行的上述操作,往往就会出现问题了

这就可以解释我们为什么使用exports = module.exports = _了,为了避免后面再修改 exports 而导致不能正确输出,就写成这样,将两者保持统一。

为什么要进行一个 exports.nodeType 判断呢?哈哈,这里确实刷新我的认识了,这是因为如果你在 HTML 页面中加入一个 id 为 exports 的元素,比如

<div id="exports"></div>

就会生成一个 window.exports 全局变量,你可以直接在浏览器命令行中打印该变量。此时在浏览器中,typeof exports != 'undefined' 的判断就会生效,然后 exports._ = _,然而在浏览器中,我们需要将 _ 挂载到全局变量上呐,所以在这里,我们还需要进行一个是否是 DOM 节点的判断。

资料



留言