Blog icon indicating copy to clipboard operation
Blog copied to clipboard

underscore 系列之如何写自己的 underscore

Open mqyqingfeng opened this issue 6 years ago • 43 comments

前言

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

自己实现

如果是我们自己去组织这些函数,我们该怎么做呢?我想我会这样做:

(function(){
    var root = this;

    var _ = {};

    root._ = _;

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

})()

_.reverse('hello');
=> 'olleh'

我们将所有的方法添加到一个名为 _ 的对象上,然后将该对象挂载到全局对象上。

之所以不直接 window._ = _ 是因为我们写的是一个工具函数库,不仅要求可以运行在浏览器端,还可以运行在诸如 Node 等环境中。

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 吧:

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

console.log(exports.num) // 1

exports.num = '2'

console.log(module.exports.num) // 2
// 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
// 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 系列目录地址:https://github.com/mqyqingfeng/Blog

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

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

mqyqingfeng avatar Nov 15 '17 03:11 mqyqingfeng

虽然我看完了 underscore 源码,但是并没有这么深入,真是惭愧

dongliang1993 avatar Nov 16 '17 01:11 dongliang1993

我一直认为underscore作为一个工具库,支持面向对象风格很奇怪。不知道博主怎么看这件事。

jiangshanmeta avatar Nov 16 '17 06:11 jiangshanmeta

@dongliang1993 可以看这个系列的文章查漏补缺,相互交流呀~

mqyqingfeng avatar Nov 17 '17 02:11 mqyqingfeng

@jiangshanmeta 我倒没有奇怪的感觉……支持面向对象风格应该是方便实现链式调用吧

mqyqingfeng avatar Nov 17 '17 02:11 mqyqingfeng

博主,你好,在这段代码中

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

你说 执行 this instanceof _,this 指向 window ,可是我打印this 并不等于window啊 而是 _ 的实例,这个地方博主能不能解释到底是怎么回事?

yangjunfocus avatar Nov 17 '17 08:11 yangjunfocus

@yangjunfocus

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

_([1, 2, 3])

查看打印结果为:

default

第一次 this 指向 window 第二次 this 指向 _ 的实例

mqyqingfeng avatar Nov 17 '17 08:11 mqyqingfeng

谢谢博主解疑!

yangjunfocus avatar Nov 17 '17 08:11 yangjunfocus

希望博主写完underscore之后写es6 再react循序渐进 加油

zhangruinian avatar Nov 22 '17 03:11 zhangruinian

@zhangruinian 一路走来,深感其中不易,非常感谢鼓励~

mqyqingfeng avatar Nov 22 '17 05:11 mqyqingfeng

@zhangruinian 一定要感谢下大家对我的支持~

mqyqingfeng avatar Nov 22 '17 05:11 mqyqingfeng

@mqyqingfeng 其实你可以探索一下在严格模式下,this 仍然返回全局对象的黑科技

ghost avatar Nov 22 '17 06:11 ghost

@liuliangsir 这是什么黑科技?能举个例子不?

mqyqingfeng avatar Nov 24 '17 08:11 mqyqingfeng

看的真的很舒服,思维都开阔了

liuxinqiong avatar Dec 04 '17 06:12 liuxinqiong

@liuxinqiong 好久不见<( ̄︶ ̄)>

mqyqingfeng avatar Dec 05 '17 02:12 mqyqingfeng

@mqyqingfeng 一直在默默学习中,等哪天知识吸收了,且有自己的观点了,我要和博主华山论剑

liuxinqiong avatar Dec 06 '17 07:12 liuxinqiong

@liuxinqiong 哈哈,好啊,期待这一天早日来临,不过我也不是止步不前哦~ 在学习的路上与你共勉 []~( ̄▽ ̄)~*

mqyqingfeng avatar Dec 06 '17 08:12 mqyqingfeng

感谢博主,读完这篇文章的收获: 1._([1, 2, 3]) 返回一个对象,为 {_wrapped: [1, 2, 3]},该对象的原型指向 _.prototype,看完这篇文章有助于理解这句话,即p.__proto__ 的原型指向 Person.prototype; 2.why: exports = module.exports = func , because: exports是module.exports的引用,可以看官方文档

MuYunyun avatar Dec 20 '17 17:12 MuYunyun

@MuYunyun 感谢分享哈~ 我补充下第二点,exports 是 module.exports 的引用,但是如果我要导出一个函数,其实我只要 module.exports = func 就可以了,为什么还要 exports = module.exports 呢?我当时困惑的是这一点, 这个在官方文档其实也有说明啦:

When the module.exports property is being completely replaced by a new object, it is common to also reassign exports

mqyqingfeng avatar Dec 22 '17 02:12 mqyqingfeng

楼主,麻烦问下,这里为什么要这么写呢

if (callback.call(obj[i], obj[i], i) === false) {
                    break;
}

dowinweb avatar Mar 16 '18 06:03 dowinweb

@dowinweb 这个可以参考 JavaScript专题之jQuery通用遍历方法each的实现,主要原因是 ES5 的 forEach 函数,在遍历中是不会停止遍历,也就是说,有的时候,我已经找到合适的值了,不想在接着往下遍历其他的数据了,但是如果用 forEach ,依然会接着遍历,所以如果使用 for 循环,我可以判断函数返回的值是否是 false,如果是 false ,我就 break,就不会接着遍历了~

mqyqingfeng avatar Mar 28 '18 05:03 mqyqingfeng

厉害了,看懂基本结构再看源码逻辑清晰很多。

byr-gdp avatar May 04 '18 03:05 byr-gdp

@jiangshanmeta 其实 jQuery 全程都在 OOP 的(

WangNianyi2001 avatar Jul 28 '18 05:07 WangNianyi2001

@mqyqingfeng 博主~你说的 forEach 循环无法跳出的问题我看有的源码里直接抛出一个空的 Error 也可以达到目的,当然使用 for 循环更好,仅提供一个意见

Sphinm avatar Aug 14 '18 10:08 Sphinm

@mqyqingfeng 博主可以参考你的文章写博客么?

jack-hate-titanic avatar Sep 29 '18 07:09 jack-hate-titanic

exports 是 module.exports 的一个引用,当你使用了 module.exports = function(){},实际上覆盖了 module.exports,但是 exports 并未发生改变。 这不太通吧? 一个引用,module.exports改变了,exports怎么可能没变呢? 或许我没get到你的意思?

wudao370859172 avatar Dec 11 '18 02:12 wudao370859172

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

为什么不利用这种方式来创建 _ ,这样还能省一个mix的逻辑。

wudao370859172 avatar Dec 11 '18 03:12 wudao370859172

写的真棒,看完之后收获颇丰

hankanon avatar Feb 20 '19 10:02 hankanon

感谢感谢,十分感谢!!!

hfutpath avatar Mar 30 '19 11:03 hfutpath

没明白小程序里root = {}如和实现挂载到全局的

tr2v avatar May 14 '19 06:05 tr2v

就光环境的判断 学到很多 思维也开阔不少

fangyinghua avatar Jul 14 '19 03:07 fangyinghua