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,對做者也是一種鼓勵。

相關文章
相關標籤/搜索