JavaScritp 專題系列第七篇,講解如何從零實現一個 jQuery 的 extend 函數git
jQuery 的 extend 是 jQuery 中應用很是多的一個函數,今天咱們一邊看 jQuery 的 extend 的特性,一邊實現一個 extend!github
先來看看 extend 的功能,引用 jQuery 官網:數組
Merge the contents of two or more objects together into the first object.函數
翻譯過來就是,合併兩個或者更多的對象的內容到第一個對象中。spa
讓咱們看看 extend 的用法:翻譯
jQuery.extend( target [, object1 ] [, objectN ] )
第一個參數 target,表示要拓展的目標,咱們就稱它爲目標對象吧。code
後面的參數,都傳入對象,內容都會複製到目標對象中,咱們就稱它們爲待複製對象吧。對象
舉個例子:排序
var obj1 = { a: 1, b: { b1: 1, b2: 2 } }; var obj2 = { b: { b1: 3, b3: 4 }, c: 3 }; var obj3 = { d: 4 } console.log($.extend(obj1, obj2, obj3)); // { // a: 1, // b: { b1: 3, b3: 4 }, // c: 3, // d: 4 // }
當兩個對象出現相同字段的時候,後者會覆蓋前者,而不會進行深層次的覆蓋。遞歸
結合着上篇寫得 《JavaScript專題之深淺拷貝》,咱們嘗試着本身寫一個 extend 函數:
// 初版 function extend() { var name, options, src, copy; var length = arguments.length; var i = 1; var target = arguments[0]; for (; i < length; i++) { options = arguments[i]; if (options != null) { for (name in options) { src = target[name]; copy = options[name]; if (copy !== undefined){ target[name] = copy; } } } } return target; };
那如何進行深層次的複製呢?jQuery v1.1.4 加入了一個新的用法:
jQuery.extend( [deep], target, object1 [, objectN ] )
也就是說,函數的第一個參數能夠傳一個布爾值,若是爲 true,咱們就會進行深拷貝,false 依然當作淺拷貝,這個時候,target 就日後移動到第二個參數。
仍是舉這個例子:
var obj1 = { a: 1, b: { b1: 1, b2: 2 } }; var obj2 = { b: { b1: 3, b3: 4 }, c: 3 }; var obj3 = { d: 4 } console.log($.extend(true, obj1, obj2, obj3)); // { // a: 1, // b: { b1: 3, b2: 2, b3: 4 }, // c: 3, // d: 4 // }
由於採用了深拷貝,會遍歷到更深的層次進行添加和覆蓋。
咱們來實現深拷貝的功能,值得注意的是:
須要根據第一個參數的類型,肯定 target 和要合併的對象的下標起始值。
若是是深拷貝,根據 copy 的類型遞歸 extend。
// 第二版 function extend() { // 默認不進行深拷貝 var deep = false; var name, options, src, copy; var length = arguments.length; // 記錄要複製的對象的下標 var i = 1; // 第一個參數不傳佈爾值的狀況下,target默認是第一個參數 var target = arguments[0] || {}; // 若是第一個參數是布爾值,第二個參數是纔是target if (typeof target == 'boolean') { deep = target; target = arguments[i] || {}; i++; } // 若是target不是對象,咱們是沒法進行復制的,因此設爲{} if (typeof target !== 'object') { target = {} } // 循環遍歷要複製的對象們 for (; i < length; i++) { // 獲取當前對象 options = arguments[i]; // 要求不能爲空 避免extend(a,,b)這種狀況 if (options != null) { for (name in options) { // 目標屬性值 src = target[name]; // 要複製的對象的屬性值 copy = options[name]; if (deep && copy && typeof copy == 'object') { // 遞歸調用 target[name] = extend(deep, src, copy); } else if (copy !== undefined){ target[name] = copy; } } } } return target; };
在實現上,核心的部分仍是跟上篇實現的深淺拷貝函數一致,若是要複製的對象的屬性值是一個對象,就遞歸調用 extend。不過 extend 的實現中,多了不少細節上的判斷,好比第一個參數是不是布爾值,target 是不是一個對象,不傳參數時的默認值等。
接下來,咱們看幾個 jQuery 的 extend 使用效果:
在咱們的實現中,typeof target
必須等於 object
,咱們纔會在這個 target
基礎上進行拓展,然而咱們用 typeof
判斷一個函數時,會返回function
,也就是說,咱們沒法在一個函數上進行拓展!
什麼,咱們還能在一個函數上進行拓展!!
固然啦,畢竟函數也是一種對象嘛,讓咱們看個例子:
function a() {} a.target = 'b'; console.log(a.target); // b
實際上,在 underscore 的實現中,underscore 的各類方法即是掛在了函數上!
因此在這裏咱們還要判斷是否是函數,這時候咱們即可以使用《JavaScript專題之類型判斷(上)》中寫得 isFunction 函數
咱們這樣修改:
if (typeof target !== "object" && !isFunction(target)) { target = {}; }
其實咱們實現的方法有個小 bug ,不信咱們寫個 demo:
var obj1 = { a: 1, b: { c: 2 } } var obj2 = { b: { c: [5], } } var d = extend(true, obj1, obj2) console.log(d);
咱們預期會返回這樣一個對象:
{ a: 1, b: { c: [5] } }
然而返回了這樣一個對象:
{ a: 1, b: { c: { 0: 5 } } }
讓咱們細細分析爲何會致使這種狀況:
首先咱們在函數的開始寫一個 console 函數好比:console.log(1),而後以上面這個 demo 爲例,執行一下,咱們會發現 1 打印了三次,這就是說 extend 函數執行了三遍,讓咱們捋一捋這三遍傳入的參數:
第一遍執行到遞歸調用時:
var src = { c: 2 }; var copy = { c: [5]}; target[name] = extend(true, src, copy);
第二遍執行到遞歸調用時:
var src = 2; var copy = [5]; target[name] = extend(true, src, copy);
第三遍進行最終的賦值,由於 src 是一個基本類型,咱們默認使用一個空對象做爲目標值,因此最終的結果就變成了對象的屬性!
爲了解決這個問題,咱們須要對目標屬性值和待複製對象的屬性值進行判斷:
判斷目標屬性值跟要複製的對象的屬性值類型是否一致:
若是待複製對象屬性值類型爲數組,目標屬性值類型不爲數組的話,目標屬性值就設爲 []
若是待複製對象屬性值類型爲對象,目標屬性值類型不爲對象的話,目標屬性值就設爲 {}
結合着《JavaScript專題之類型判斷(下)》中的 isPlainObject 函數,咱們能夠對類型進行更細緻的劃分:
var clone, copyIsArray; ... if (deep && copy && (isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) { if (copyIsArray) { copyIsArray = false; clone = src && Array.isArray(src) ? src : []; } else { clone = src && isPlainObject(src) ? src : {}; } target[name] = extend(deep, clone, copy); } else if (copy !== undefined) { target[name] = copy; }
實際上,咱們還可能遇到一個循環引用的問題,舉個例子:
var a = {name : b}; var b = {name : a} var c = extend(a, b); console.log(c);
咱們會獲得一個能夠無限展開的對象,相似於這樣:
爲了不這個問題,咱們須要判斷要複製的對象屬性是否等於 target,若是等於,咱們就跳過:
... src = target[name]; copy = options[name]; if (target === copy) { continue; } ...
若是加上這句,結果就會是:
{name: undefined}
function extend() { // 默認不進行深拷貝 var deep = false; var name, options, src, copy, clone, copyIsArray; var length = arguments.length; // 記錄要複製的對象的下標 var i = 1; // 第一個參數不傳佈爾值的狀況下,target 默認是第一個參數 var target = arguments[0] || {}; // 若是第一個參數是布爾值,第二個參數是 target if (typeof target == 'boolean') { deep = target; target = arguments[i] || {}; i++; } // 若是target不是對象,咱們是沒法進行復制的,因此設爲 {} if (typeof target !== "object" && !isFunction(target)) { target = {}; } // 循環遍歷要複製的對象們 for (; i < length; i++) { // 獲取當前對象 options = arguments[i]; // 要求不能爲空 避免 extend(a,,b) 這種狀況 if (options != null) { for (name in options) { // 目標屬性值 src = target[name]; // 要複製的對象的屬性值 copy = options[name]; // 解決循環引用 if (target === copy) { continue; } // 要遞歸的對象必須是 plainObject 或者數組 if (deep && copy && (isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) { // 要複製的對象屬性值類型須要與目標屬性值相同 if (copyIsArray) { copyIsArray = false; clone = src && Array.isArray(src) ? src : []; } else { clone = src && isPlainObject(src) ? src : {}; } target[name] = extend(deep, clone, copy); } else if (copy !== undefined) { target[name] = copy; } } } } return target; };
若是以爲看明白了上面的代碼,想一想下面兩個 demo 的結果:
var a = extend(true, [4, 5, 6, 7, 8, 9], [1, 2, 3]); console.log(a) // ???
var obj1 = { value: { 3: 1 } } var obj2 = { value: [5, 6, 7], } var b = extend(true, obj1, obj2) // ??? var c = extend(true, obj2, obj1) // ???
JavaScript專題系列目錄地址:https://github.com/mqyqingfeng/Blog。
JavaScript專題系列預計寫二十篇左右,主要研究平常開發中一些功能點的實現,好比防抖、節流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特色是研(chao)究(xi) underscore 和 jQuery 的實現方式。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。