underscore 系列之如何写本身的 underscore

underscore系列第一篇,讲解 underscore 的代码组织方式html

前言

《JavaScript 专题系列》 中,咱们写了不少的功能函数,好比防抖、节流、去重、类型判断、扁平数组、深浅拷贝、查找数组元素、通用遍历、柯里化、函数组合、函数记忆、乱序等,能够咱们该如何组织这些函数,造成本身的一个工具函数库呢?这个时候,咱们就要借鉴 underscore 是怎么作的了。node

本身实现

若是是咱们本身去组织这些函数,咱们该怎么作呢?我想我会这样作:git

(function(){
    var root = this;

    var _ = {};

    root._ = _;

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

})()

_.reverse('hello');
=> 'olleh'复制代码

咱们将全部的方法添加到一个名为 _ 的对象上,而后将该对象挂载到全局对象上。github

之因此不直接 window._ = _ 是由于咱们写的是一个工具函数库,不只要求能够运行在浏览器端,还能够运行在诸如 Node 等环境中。web

root

然而 underscore 可不会写得如此简单,咱们从 var root = this 开始提及。小程序

之因此写这一句,是由于咱们要经过 this 得到全局对象,而后将 _ 对象,挂载上去。微信小程序

然而在严格模式下,this 返回 undefined,而不是指向 Window,幸运的是 underscore 并无采用严格模式,但是即使如此,也不能避免,由于在 ES6 中模块脚本自动采用严格模式,无论有没有声明 use strict数组

若是 this 返回 undefined,代码就会报错,因此咱们的思路是对环境进行检测,而后挂载到正确的对象上。咱们修改一下代码:浏览器

var root = (typeof window == 'object' && window.window == window && window) ||
           (typeof global == 'object' && global.global == global && global);复制代码

在这段代码中,咱们判断了浏览器和 Node 环境,但是只有这两个环境吗?那咱们来看看 Web Worker。微信

Web Worker

Web Worker 属于 HTML5 中的内容,引用《JavaScript权威指南》中的话就是:

在 Web Worker 标准中,定义了解决客户端 JavaScript 没法多线程的问题。其中定义的 “worker” 是指执行代码的并行过程。不过,Web Worker 处在一个自包含的执行环境中,没法访问 Window 对象和 Document 对象,和主线程之间的通讯业只能经过异步消息传递机制来实现。

为了演示 Web Worker 的效果,我写了一个 demo,查看代码

在 Web Worker 中,是没法访问 Window 对象的,因此 typeof windowtypeof global 的结果都是 undefined,因此最终 root 的值为 false,将一个基本类型的值像对象同样添加属性和方法,天然是会报错的。

那么咱们该怎么办呢?

虽然在 Web Worker 中不能访问到 Window 对象,可是咱们却能经过 self 访问到 Worker 环境中的全局对象。咱们只是要找全局变量挂载而已,因此彻底能够挂到 self 中嘛。

并且在浏览器中,除了 window 属性,咱们也能够经过 self 属性直接访问到 Winow 对象。

console.log(window.window === window); // true
console.log(window.self === window); // true复制代码

考虑到使用 self 还能够额外支持 Web Worker,咱们直接将代码改为 self:

var root = (typeof self == 'object' && self.self == self && self) ||
           (typeof global == 'object' && global.global == global && global);复制代码

node vm

到了这里,依然没完,让你想不到的是,在 node 的 vm 模块中,也就是沙盒模块,runInContext 方法中,是不存在 window,也不存在 global 变量的,查看代码

可是咱们却能够经过 this 访问到全局对象,因此就有人发起了一个 PR,代码改为了:

var root = (typeof self == 'object' && self.self == self && self) ||
           (typeof global == 'object' && global.global == global && global) ||
           this;复制代码

微信小程序

到了这里,仍是没完,轮到微信小程序登场了。

由于在微信小程序中,window 和 global 都是 undefined,加上又强制使用严格模式,this 为 undefined,挂载就会发生错误,因此就有人又发了一个 PR,代码变成了:

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

这就是如今 v1.8.3 的样子。

虽然做者能够直接讲解最终的代码,可是做者更但愿带着你们看看这看似普通的代码是如何一步步演变成这样的,也但愿告诉你们,代码的健壮性,并不是一蹴而就,而是聚集了不少人的经验,考虑到了不少咱们意想不到的地方,这也是开源项目的好处吧。

函数对象

如今咱们讲第二句 var _ = {};

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

_('hello').reverse(); // 'olleh'复制代码

再举个例子比较下两种调用方式:

// 函数式风格
_.each([1, 2, 3], function(item){
    console.log(item)
});

// 面向对象风格
_([1, 2, 3]).each(function(item){
    console.log(item)
});复制代码

但是该如何实现呢?

既然以 _([1, 2, 3]) 的形式能够执行,就代表 _ 不是一个字面量对象,而是一个函数!

幸运的是,在 JavaScript 中,函数也是一种对象,咱们举个例子:

var _ = function() {};
_.value = 1;
_.log = function() { return this.value + 1 };

console.log(_.value); // 1
console.log(_.log()); // 2复制代码

咱们彻底能够将自定义的函数定义在 _ 函数上!

目前的写法为:

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

var _ = function() {}

root._ = _;复制代码

如何作到 _([1, 2, 3]).each(...)呢?即 函数返回一个对象,这个对象,如何调用挂在 函数上的方法呢?

咱们看看 underscore 是如何实现的:

var _ = function(obj) {
    if (!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
};

_([1, 2, 3]);复制代码

咱们分析下 _([1, 2, 3]) 的执行过程:

  1. 执行 this instanceof _,this 指向 window ,window instanceof _ 为 false,!操做符取反,因此执行 new _(obj)
  2. new _(obj) 中,this 指向实例对象,this instanceof _ 为 true,取反后,代码接着执行
  3. 执行 this._wrapped = obj, 函数执行结束
  4. 总结,_([1, 2, 3]) 返回一个对象,为 {_wrapped: [1, 2, 3]},该对象的原型指向 _.prototype

示意图以下:

_()示意图
_()示意图

而后问题来了,咱们是将方法挂载到 _ 函数对象上,并无挂到函数的原型上呐,因此返回了的实例,实际上是没法调用 _ 函数对象上的方法的!

咱们写个例子:

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

    var _ = function(obj) {
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
    }

    root._ = _;

    _.log = function(){
        console.log(1)
    }

})()

_().log(); // _(...).log is not a function复制代码

确实有这个问题,因此咱们还须要一个方法将 _上的方法复制到 _.prototype 上,这个方法就是 _.mixin

_.functions

为了将 _ 上的方法复制到原型上,首先咱们要得到 _ 上的方法,因此咱们先写个 _.functions 方法。

_.functions = function(obj) {
    var names = [];
    for (var key in obj) {
        if (_.isFunction(obj[key])) names.push(key);
    }
    return names.sort();
};复制代码

isFunction 函数能够参考 《JavaScript专题之类型判断(下)》

_.mixin

如今咱们能够写 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(_);复制代码

each 方法能够参考 《JavaScript专题jQuery通用遍历方法each的实现》

值得注意的是:由于 _[name] = obj[name] 的缘故,咱们能够给 underscore 拓展自定义的方法:

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

_(2).addOne(); // 3复制代码

至此,咱们算是实现了同时支持面向对象风格和函数风格。

导出

终于到了讲最后一步 root._ = _,咱们直接看源码:

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

为了支持模块化,咱们须要将 _ 在合适的环境中做为模块导出,可是 nodejs 模块的 API 曾经发生过改变,好比在早期版本中:

// add.js
exports.addOne = function(num) {
  return num + 1
}

// index.js
var add = require('./add');
add.addOne(2);复制代码

在新版本中:

// add.js
module.exports = function(1){
    return num + 1
}

// index.js
var addOne = require('./add.js')
addOne(2)复制代码

因此咱们根据 exports 和 module 是否存在来选择不一样的导出方式,那为何在新版本中,咱们还要使用 exports = module.exports = _ 呢?

这是由于在 nodejs 中,exports 是 module.exports 的一个引用,当你使用了 module.exports = function(){},实际上覆盖了 module.exports,可是 exports 并未发生改变,为了不后面再修改 exports 而致使不能正确输出,就写成这样,将二者保持统一。

写个 demo 吧:

第一个 demo:

// exports 是 module.exports 的一个引用
module.exports.num = '1'

console.log(exports.num) // 1

exports.num = '2'

console.log(module.exports.num) // 2复制代码

第二个 demo:

// addOne.js
module.exports = function(num){
    return num + 1
}

exports.num = '3'

// result.js 中引入 addOne.js
var addOne = require('./addOne.js');

console.log(addOne(1)) // 2
console.log(addOne.num) // undefined复制代码

第三个 demo:

// addOne.js
exports = module.exports = function(num){
    return num + 1
}

exports.num = '3'

// result.js 中引入 addOne.js
var addOne = require('./addOne.js');

console.log(addOne(1)) // 2
console.log(addOne.num) // 3复制代码

最后为何要进行一个 exports.nodeType 判断呢?这是由于若是你在 HTML 页面中加入一个 id 为 exports 的元素,好比

<div id="exports"></div>复制代码

就会生成一个 window.exports 全局变量,你能够直接在浏览器命令行中打印该变量。

此时在浏览器中,typeof exports != 'undefined' 的判断就会生效,而后 exports._ = _,然而在浏览器中,咱们须要将 _ 挂载到全局变量上呐,因此在这里,咱们还须要进行一个是不是 DOM 节点的判断。

源码

最终的代码以下,有了这个基本结构,你能够自由添加你须要使用到的函数了:

(function() {

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

    var ArrayProto = Array.prototype;

    var push = ArrayProto.push;

    var _ = function(obj) {
        if (obj instanceof _) return obj;
        if (!(this instanceof _)) return new _(obj);
        this._wrapped = obj;
    };

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

    _.VERSION = '0.1';

    var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;

    var isArrayLike = function(collection) {
        var length = collection.length;
        return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
    };

    _.each = function(obj, callback) {
        var length, i = 0;

        if (isArrayLike(obj)) {
            length = obj.length;
            for (; i < length; i++) {
                if (callback.call(obj[i], obj[i], i) === false) {
                    break;
                }
            }
        } else {
            for (i in obj) {
                if (callback.call(obj[i], obj[i], i) === false) {
                    break;
                }
            }
        }

        return obj;
    }

    _.isFunction = function(obj) {
        return typeof obj == 'function' || false;
    };

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

    /** * 在 _.mixin(_) 前添加本身定义的方法 */
    _.reverse = function(string){
        return string.split('').reverse().join('');
    }

    _.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(_);

})()复制代码

相关连接

  1. 《JavaScript专题之类型判断(下)》

  2. 《JavaScript专题jQuery通用遍历方法each的实现》

underscore 系列

underscore 系列目录地址:github.com/mqyqingfeng…

underscore 系列预计写八篇左右,重点介绍 underscore 中的代码架构、链式调用、内部函数、模板引擎等内容,旨在帮助你们阅读源码,以及写出本身的 undercore。

若是有错误或者不严谨的地方,请务必给予指正,十分感谢。若是喜欢或者有所启发,欢迎star,对做者也是一种鼓励。

相关文章
相关标签/搜索