underscore原始碼剖析之整體架構

2021-09-13 23:22:06 字數 3981 閱讀 6511

最近打算好好看看underscore原始碼,乙個是因為自己確實水平不夠,另乙個是underscore原始碼比較簡單,比較易讀。

本系列打算對underscore1.8.3中關鍵函式原始碼進行分析,希望做到最詳細的原始碼分析。

今天是underscore原始碼剖析系列第一篇,主要對underscore整體架構和基礎函式進行分析。

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

// 這裡是乙個立即呼叫函式,使用call繫結了外層的this(全域性物件)

(function() ;

// 內部實現省略

var _ = function(obj) {};

// 這裡是各種方法的實現(省略)

// 匯出underscore方法,如果有exports則用exports匯出,如果 沒有,則將其設為全域性變數

if (typeof exports !== 'undefined')

exports._ = _;

} else

// 版本號

_.version = '1.8.3';

// 用amd的形式匯出

if (typeof define === 'function' && define.amd) );

}}.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。

掃一眼原始碼,我們會發現在原始碼中並沒有見到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

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

_.noconflict = function() ;

var underscore = _.noconflict();

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

接下來講到了本文的重點,關於_方法的分析,在看原始碼之前,我們先熟悉一下_的用法。

這裡總結的是我日常的用法,如果有遺漏,希望大家補充。

一種是直接呼叫_上的方法,比如_.map([1, 2, 3]),另一種是通過例項訪問原型上的方法,比如_([1, 2, 3]).map(),這裡和jquery的用法很像,$.extend呼叫jquery物件上的方法,而$("body").click()則是呼叫jquery原型上的方法。

既然_可以使用原型上面的方法,那麼說明執行_函式的時候肯定會返回乙個例項。

這裡來看原始碼:

// instanceof 運算子用來測試乙個物件在其原型鏈中是否存在乙個建構函式的 prototype 屬性。

// 我這裡有個不夠準確但容易理解的說法,就是檢查乙個物件是否為另乙個建構函式的例項,為了更容易理解,下面將全部以***是***的例項的方式來說。

var _ = function(obj) ;

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

var person = function() 

person.say = function()

person.prototype.say = function()

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),這樣避免了迴圈呼叫

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

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

var _ = function(obj) 

_.prototype = ,

name: function(name)

}_.prototype.init.prototype = _.prototype;

var a = _([1, 2, 3])

a.name("ygy"); // ygy

underscore中所有方法都是在_方法上面直接掛載的,並且用mixin方法將這些方法再一次掛載到了原型上面。不過,由於篇幅有限,mixin方法的實現會在後文中給大家講解。

如果本文有錯誤和不足之處,希望大家指出。

underscore原始碼閱讀整理

underscore是我閱讀的第乙份原始碼,這份 比較小巧,只有1500行,我閱讀的版本是1.8.3.underscore裡封裝了很多功能性的函式,和jquery不同,我覺得jquery的特點是對針對dom,而underscore裡都是基於js的功能性函式,比如each,map等等。以下內容僅是我閱...

underscore原始碼分析 1

underscore 版本1.83 最主要的乙個特性是鏈式呼叫 1,2,3 each console.log 1 0 3 1,2,3 2 1 3 1,2,3 3 2 3 1,2,3 我們先簡單的實現鏈式呼叫的功能 實現 each 1,2,3 console.log 是很簡單的 直接 each函式就搞...

underscore 原始碼閱讀 四

keys one two three 檢索object擁有的所有可列舉屬性的名稱。我們知道,在js中本就提供了幾個方法如for.in.object.keys來遍歷物件的屬性,為什麼underscore還是要封裝乙個api呢?這其實是為相容ie9版本下的乙個bug做的封裝 在ie9以下的版本中,以下 ...