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方法的實現會在後文中給大家講解。
如果本文有錯誤和不足之處,希望大家指出。