jQuery原始碼分析之 $.isEmptyObject和$.isPlainObject的區別以及自己的一些看法
最近一段時間一直在努力的去看jQuery的原始碼,這是一個非常富有挑戰的過程,在這個過程中也收穫頗多,就想把自己在看jQuery原始碼的時候遇到的一些困惑和讀出的一些心得體會總結出來弄成一個jQuery原始碼分析系列分享給大家,希望對大家也能有所幫助。對自己而言也是一份讀完之後的總結,歸納。
jquery“類”和jquery例項的擴充套件
在看jquery原始碼的時候,我們必須先要知道,jquery中有很多方法是擴充套件在jquery這個“類”中的,而有些方法是擴充套件在jquery這個例項物件上的。這兩個是完全不一樣的。
擴充套件在類上面的比如今天所說的這兩個函式,再比如$.type() $.isNumeric()等等方法,這些方法是直接從jquery這個"類"中去取出來使用的。
還有另一種是擴充套件在jquery例項上的方法,比如$(ele).addClass(),$(ele).removeClass() $(ele).eq()...等等這些方法是需要從jquery的返回例項上去呼叫這些方法的。
那麼這兩種分別是怎麼擴充套件的呢。?
在jquery中,如果是從jquery“類”中擴充套件的方法都會寫成: jQuery.extend({ })
而如果是在jquery例項上擴充套件的方法則是寫在jQuery.fn = jQuery.prototype = { }
我們來看下jquery原始碼:
通過上兩幅圖中jquery的原始碼部分我們可以很清楚的看到是分別在jquery.fn和jquery.extend來實現的。
那今天就先來說說jQuery中的$.isPlainObject()和$.isEmptyObject()兩個方法的原始碼分析。
$.isEmptyObject()
首先先說比較簡單的$.isEmptyObject()這個方法,他的功能是檢測一個物件是否是一個空物件。
廢話不多說,先上原始碼:
isEmptyObject: function( obj ) {
var name;
for ( name in obj ) {
return false;
}
return true;
},
我們可以看到其實關於isEmptyObject方法的原始碼很簡單,其實就是in運算子。
首先會將傳入的引數用in運算子去遍歷傳入物件的屬性,如果遍歷出這個引數物件中有屬性的話,那自然就不會是一個空物件了,但是這兒需要注意的有兩點。
第一點是in運算子是可以一直遍歷物件上原型鏈上的屬性,(與hasOwnProperty()方法只能遍歷自身屬性不同),所以即便本身物件上沒有屬性,但是如果原型鏈上的物件有屬性值的話,那麼isEmptyObject()這個方法返回的是false而不是true。
第二點如果傳入的引數不是一個物件,那麼首先會將傳入的引數轉化為物件,比如你傳入的是$.isEmptyObject('name')這麼個字串的話,那麼會首先將傳入的基本資料型別字串用基本包裝型別String()包裝(即所謂的“裝箱”過程)。
裝箱:將number string boolea這三種基本資料型別呼叫各自的基本包裝方法 Number()、String()、Boolean()的過程稱之為“裝箱”
下面用兩個demo來說明上面寫的兩點:
var parent = {
age:27
}
var son = Object.create(parent)
console.log($.isEmptyObject(son)) //false
console.log($.isEmptyObject('name')) //false
我們可以看到這兩個放回的都是false,因為傳如的‘name’會被裝箱之後遍歷,in自然是能遍歷到屬性的,所以返回是false.
我們看一下son是用Object.create()函式返回的一個物件,那麼son._proto_原型物件就會指向parent.。如下圖:
我們剛剛在上面也說明了,in運算子不僅能遍歷物件本身的屬性,還能沿著原型鏈去遍歷所有[[Enumerable]]值為true的屬性。
所以我們在控制檯上看到son自己本身雖然是沒有任何屬性的,看起來像是一個‘空物件’但是他的原型物件parent上是有age屬性的,所以他的返回值也是false。
上面所說的兩點基本包含了大部分用到$.isEmptyObject()這個方法的場景了。
之所以說是“大部分”的確還有兩個地方,是比較特殊的地方。那就是當闖入的引數是undefined 和null的時候。for in是不會去執行的。直接返回true了,那前後的邏輯就會變得非常奇怪了,也就是說當你$.isEmptyObject(null)或者$.isEmptyObject(undefined)的時候總是返回true,可是null和undefined壓根就不是一個物件,既然都不是一個物件,那怎麼能說他們是一個空的“物件呢”。(null在js中被認為是一個物件是一個歷史悠久的bug,null本身就是一個基本型別,並不是物件。)
我原本以為for in在遇到null 和undefined的時候會有其他的機制,可是我查閱了很多資料,最終在在javascript 高階程式設計(第三版)第三章第六節 3.6.5 這一小節中發現關於for in 遇到null 和undefined的描述如下:
如果要迭代的物件變數是null 或undefined的話,for-in語句會丟擲錯誤,ECMAScript5更正了這一行為,對這種情況不在丟擲錯誤,而是不執行迴圈體。為了保證最大限度的相容性,建議在使用for-in迴圈之前,先檢測確認該物件的值不是null或undefined.
上述這段話是高程3的原話。那這就有意思了。如果使用者在使用$.isEmptyObject()這個方法的時候。傳入的是null或者是undefined,呵呵噠,那就壓根不會執行for-in,而是直接返回true了。我自己覺得這是jquery中沒考慮周全的部分。應當判斷一下傳入的引數是否是undefined和null的情況。至於怎麼檢測,那這兒還有個小坑,不能用type of null去檢測,(瀏覽器總是返回object)這是歷史悠久的bug,可以用jquery內部方法$.type()去判斷,關於$.type()的方法我們後面也會寫一篇文章講述,這兒我們只需要知道使用$.type()會返回準確的資料型別就可以了。寫到這兒我有點激動了,因為發現了一個jquery不合理的地方,至少我覺得是有待提高的地方。哈哈。這也是探索jquery原始碼的樂趣之一吧。
$.isPlainObject()
下面我們來看下另一個$.isPlainObject()這個方法,這個方法比上面的方法要略複雜些,他的用處是用來判斷一個物件是否是一個“純粹”的物件,那麼什麼是“純粹”呢,就是用{}或者new Object() 在加上一個特例就是用Object.create(null)來構造出的物件。
關於這部分的原始碼如下:
isPlainObject: function( obj ) {
var proto, Ctor;
// Detect obvious negatives
// Use toString instead of jQuery.type to catch host objects
if ( !obj || toString.call( obj ) !== "[object Object]" ) {
return false;
}
proto = getProto( obj );
// Objects with no prototype (e.g., `Object.create( null )`) are plain
if ( !proto ) {
return true;
}
// Objects with prototype are plain iff they were constructed by a global Object function
Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
},
這就是$.isPlainObject()的原始碼部分了。這兒有幾個全域性變數
var getProto = Object.getPrototypeOf;
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
這而的getProto其實就是Object中的getPrototyoeOf的方法,用來獲得傳入引數物件的原型物件。
hasOwn就是一個物件上的hasOwnProperty()這個方法,用來獲取物件本身上面是否具有某個屬性與in 不同,hasOwnProperty會忽略原型鏈上的屬性。
toString就是一個物件上呼叫toString()這個方法,轉化為字串。
我們首先看第一個if判斷
if ( !obj || toString.call( obj ) !== "[object Object]" ) {
return false;
}
這個判斷傳入的引數可以過濾掉非物件的情況,因為要要判斷一個物件是否是“純粹”的物件,的前提需要他是物件。
proto = getProto( obj );這句程式碼就是獲取傳入引數物件的原型物件。如果我們沒有給一個物件設定他的原型物件,那麼proto就會等於全域性物件下的Object,因為所有的物件都繼承Object這個物件。(除了object.create(null)之外)
// Objects with no prototype (e.g., `Object.create( null )`) are plain if ( !proto ) { return true; }
這句話是處理那些沒有原型物件的物件,在這兒你可能會詫異一點,不是所有的物件都會繼承Object麼,怎麼會有物件沒有原型物件呢,有的,就是當我們使用Object.create(null)的方式去建立一個物件的時候,所建立出的物件是沒有原型物件的,因為一般情況下,Object.create(param)所建立的物件的原型物件都是param,而此時param是null的話,就自然沒有原型物件了。
所以,這句程式碼就是用來處理這種情況的,當我們使用Object.create(null)創建出的物件時,就返回true,實際上使用Object.create(null)所創建出的物件,恐怕是最“乾淨”,最“純粹”的。因為如上所說,所有物件預設情況下都會繼承Object這個物件,而使用這種方式創造出的物件是Object都不會繼承的。
Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
這一句程式碼略長,但是也不難,就是先判斷proto(傳入引數的原型物件)是否含有constructor這個屬性,實際上這個屬性對應的就是建構函式。如果有的話,就取出proto中的建構函式然後吧這個函式的引用賦值給Ctor。
return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
我們得到了proto的建構函式之後,就用這個建構函式來判斷這個物件是否是一個純粹的物件,因為我們使用{}或者new Object()的方式去構造一個物件的話,構造出的物件都會繼承Object這個全域性物件,這個物件中的constructor屬性是一個內部的構造方法。var fnToString = hasOwn.toString;
var ObjectFunctionString = fnToString.call( Object );
所以這句程式碼其實就是判斷我們傳入引數物件的原型物件中的建構函式是否和全域性變數中Object中的建構函式完全一樣,如果完全一樣,那麼就返回true,反之則返回false.
下面看幾個demo來說明上面所說的內容:
var a = $.isPlainObject({})
var b = $.isPlainObject(new Object())
var c = $.isPlainObject(Object.create(null))
function Foo() {
}
var d = $.isPlainObject(new Foo())
console.log(a,b,c,d) //true true true false
我們可以看到a,b,c都分別是使用{}, new Object() 和Object.create(null)來構造出的物件,所以返回值是true.即被認為是一個“純粹”的物件,而d這個例項物件則是通過建構函式Foo()生成的,因為在通過new 呼叫建構函式的時候,這個返回的例項物件的原型物件指向的是建構函式,而建構函式中的constructor是指向自己的,並不等於全域性物件Object中的建構函式,所以返回的結果是false。
小結:
- $.isEmptyObject():用於判斷一個物件是否是“空”物件,(不僅自己空,自己的原型上也不能有可被for-in遍歷到的屬性),用之前最好先用$.type()去相容下null和undefined的兩種情況(jQuery原始碼的“小坑”)。
- $.isPlainObject():用於判斷一個物件是否“純粹”(即用 "{}", "new Object()", "Object.create(null)")創建出來的。
兩個方法從名字上看好像差不多,實際上所做的事情是完全不一樣的。
好了,關於$.isEmptyObject()和$.isPlainObject()的原始碼就分析完了,如果大家有不同的見解,歡迎一起溝通交流。