博客 / 詳情

返回

underscore源碼剖析之整體架構

underscore源碼分析之整體架構


最近打算好好看看underscore源碼,一個是因為自己確實水平不夠,另一個是underscore源碼比較簡單,比較易讀。
本系列打算對underscore1.8.3中關鍵函數源碼進行分析,希望做到最詳細的源碼分析。
今天是underscore源碼剖析系列第一篇,主要對underscore整體架構和基礎函數進行分析。

基礎模塊

首先,我們先來簡單的看一下整體的代碼:

// 這裏是一個立即調用函數,使用call綁定了外層的this(全局對象)
(function() {

  var root = this;
  
  // 保存當前環境中已經存在的_變量(在noConflict中用到)
  var previousUnderscore = root._;
  
  // 用變量保存原生方法的引用,以防止這些方法被重寫,也便於壓縮
  var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype;

  var
    push             = ArrayProto.push,
    slice            = ArrayProto.slice,
    toString         = ObjProto.toString,
    hasOwnProperty   = ObjProto.hasOwnProperty;

  var
    nativeIsArray      = Array.isArray,
    nativeKeys         = Object.keys,
    nativeBind         = FuncProto.bind,
    nativeCreate       = Object.create;

  var Ctor = function(){};
  // 內部實現省略
  var _ = function(obj) {};
  
    // 這裏是各種方法的實現(省略)
    
  // 導出underscore方法,如果有exports則用exports導出,如果    沒有,則將其設為全局變量
  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = _;
    }
    exports._ = _;
  } else {
    root._ = _;
  }
  
  // 版本號
  _.VERSION = '1.8.3';
  
  // 用amd的形式導出
  if (typeof define === 'function' && define.amd) {
    define('underscore', [], function() {
      return _;
    });
  }
}.call(this))

全局對象

這段代碼整體比較簡單,不過我看後來的underscore版本有一些小改動,主要是將var root = this;替換為下面這句:

var root = typeof self == 'object' && self.self === self && self || typeof global == 'object' && global.global === global && global || this;

這裏增加了對self和global的判斷,self屬性可以返回對窗口自身的引用,等價於window,這裏主要是為了兼容web worker,因為web worker中是沒有window的,global則是為了兼容node,而且在嚴格模式下,立即執行函數內部的this是undefined。

void(0) ? undefined

掃一眼源碼,我們會發現在源碼中並沒有見到undefined的出現,反而是用void(0)或者void 0來代替的,那麼這個void到底是什麼?為什麼不能直接用undefined呢?
關於void的解釋,我們可以看這裏:MDN
void 運算符通常只用於獲取 undefined的原始值,一般使用void(0),因為undefined不是保留字,在低版本瀏覽器或者局部作用域中是可以被當做變量賦值的,這樣就會導致我們拿不到正確的undefined值,在很多壓縮工具中都是將undefined用void 0來代替掉了。
其實這裏不僅是void 0可以拿到undefined,還有其他很多方法也可以拿到,比如0["ygy"]、Object.__undefined__、Object.__ygy__,這些原理都是訪問一個不存在的屬性,所以最後一定會返回undefined

noConflict

也許有時候我們會碰到這樣一種情況,_已經被當做一個變量聲明瞭,我們引入underscore後會覆蓋這個變量,但是又不想這個變量被覆蓋,還好underscore提供了noConflict這個方法。

_.noConflict = function() {
    root._ = previousUnderscore;
    return this;
};
var underscore = _.noConflict();

顯而易見,這裏正常保留原來的_變量,並返回了underscore這個方法(this就是_方法)

_

接下來講到了本文的重點,關於_方法的分析,在看源碼之前,我們先熟悉一下_的用法。
這裏總結的是我日常的用法,如果有遺漏,希望大家補充。
一種是直接調用_上的方法,比如_.map([1, 2, 3]),另一種是通過實例訪問原型上的方法,比如_([1, 2, 3]).map(),這裏和jQuery的用法很像,$.extend調用jQuery對象上的方法,而$("body").click()則是調用jQuery原型上的方法。

既然_可以使用原型上面的方法,那麼説明執行_函數的時候肯定會返回一個實例。

這裏來看源碼:

// instanceof 運算符用來測試一個對象在其原型鏈中是否存在一個構造函數的 prototype 屬性。
// 我這裏有個不夠準確但容易理解的説法,就是檢查一個對象是否為另一個構造函數的實例,為了更容易理解,下面將全部以XXX是XXX的實例的方式來説。
var _ = function(obj) {
    // 如果obj是_的實例(這種情況我真的沒碰到過)
    if (obj instanceof _) return obj;
    // 如果this不是_構造函數的實例,那就以obj為參數 new一個實例(相等於修改了_函數)
    if (!(this instanceof _)) return new _(obj);
    // 對應_([1,2,3])這種情況
    this._wrapped = obj;
  };

我先從源碼上來解釋,這裏可以看出來_是一個構造函數,我們都知道,我既可以在構造函數上面增加方法,還可以在原型上面增加方法,前者只能通過構造函數本身訪問到,後者由於原型鏈的存在,可以在構造函數的實例上面訪問到。

var Person = function() {
    this.name = "ygy";
    this.age = 22;
}
Person.say = function() {
    console.log("hello")
}
Person.prototype.say = function() {
    console.log("world")
}
var ygy = new Person();
Person.say(); // hello
ygy.say(); // world

所以我們平時用的_.map就是Person.say()這種用法,而_([1, 2, 3]).map則是ygy.say()這種用法。

在繼續講這個之前,我們再來複習一下原型的知識,當我們new一個實例的時候到處發生了什麼?

首先,這裏會先創建一個空對象,這個空對象繼承了構造函數的原型(或者理解為空對象上增加一個指向構造函數原型的指針__proto__),之後會根據實例傳入的參數執行一遍構造函數,將構造函數內部的this綁定到這個新對象中,最後返回這個對象,過程和如下類似:

var ygy = {};
ygy.__proto__ = Person.prototype 
// 或者var ygy = Object.create(Person.prototype)
Person.call(ygy);

這樣就很好理解了,要是想調用原型上面的方法,必須先new一個實例出來。我們再來分析_方法的源碼:
_接收一個對象作為參數,如果這個對象是_的一個實例,那麼直接返回這個對象。(這種情況我倒是沒見過)

如果this不是_的實例,那麼就會返回一個新的實例new _(obj),這個該怎麼理解?
我們需要結合例子來看這句話,在_([1, 2, 3])中,obj肯定是指[1, 2, 3]這個數組,那麼this是指什麼呢?我覺得this是指window,不信你直接執行一下上面例子中的Person()?你會發現在全局作用域中是可以拿到name和age兩個屬性的。

那麼既然this指向window,那麼this肯定不是_的實例,所以this instanceof _必然會返回false,這樣的話就會return一個new _([1, 2, 3]),所以_([1, 2, 3])就是new _([1, 2, 3]),從我們前面對new的解釋來看,這個過程表現如下:

var obj = {}
obj.__proto__ = _.prototype
// 此時_函數中this的是new _(obj),this instanceof _是true,所以就不會重新return一個new _(obj),這樣避免了循環調用
_.call(obj) // 實際上做了這一步: obj._wrapped = [1, 2, 3]

這樣我們就理解了為什麼_([1, 2, 3]).map中map是原型上的方法,因為_([1, 2, 3])是一個實例。

我這裏再提供一個自己實現的_思路,和jQuery的實現類似,這裏就不作解釋了:

var _ = function(obj) {
    return new _.prototype.init(obj)
}
_.prototype = {
    init: function(obj) {
        this.__wrapped = obj
        return this
    },
    name: function(name) {
        console.log(name)
    }
}
_.prototype.init.prototype = _.prototype;
var a = _([1, 2, 3])
a.name("ygy"); // ygy

underscore中所有方法都是在_方法上面直接掛載的,並且用mixin方法將這些方法再一次掛載到了原型上面。不過,由於篇幅有限,mixin方法的實現會在後文中給大家講解。
如果本文有錯誤和不足之處,希望大家指出。

user avatar ziyeliufeng 頭像 hightopo 頭像 coderleo 頭像 codepencil 頭像 flymon 頭像 uncletong_doge 頭像 frontoldman 頭像 lesini 頭像 iymxpc3k 頭像 79px 頭像 pangsir8983 頭像 mrqueue 頭像
42 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.