摘要:不過的實現(xiàn)中,多了很多細節(jié)上的判斷,比如第一個參數(shù)是否是布爾值,是否是一個對象,不傳參數(shù)時的默認值等。
前言JavaScritp 專題系列第七篇,講解如何從零實現(xiàn)一個 jQuery 的 extend 函數(shù)
jQuery 的 extend 是 jQuery 中應用非常多的一個函數(shù),今天我們一邊看 jQuery 的 extend 的特性,一邊實現(xiàn)一個 extend!
extend 基本用法先來看看 extend 的功能,引用 jQuery 官網(wǎng):
Merge the contents of two or more objects together into the first object.
翻譯過來就是,合并兩個或者更多的對象的內容到第一個對象中。
讓我們看看 extend 的用法:
jQuery.extend( target [, object1 ] [, objectN ] )
第一個參數(shù) target,表示要拓展的目標,我們就稱它為目標對象吧。
后面的參數(shù),都傳入對象,內容都會復制到目標對象中,我們就稱它們?yōu)榇龔椭茖ο蟀伞?/p>
舉個例子:
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 // }
當兩個對象出現(xiàn)相同字段的時候,后者會覆蓋前者,而不會進行深層次的覆蓋。
extend 第一版結合著上篇寫得 《JavaScript專題之深淺拷貝》,我們嘗試著自己寫一個 extend 函數(shù):
// 第一版 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; };extend 深拷貝
那如何進行深層次的復制呢?jQuery v1.1.4 加入了一個新的用法:
jQuery.extend( [deep], target, object1 [, objectN ] )
也就是說,函數(shù)的第一個參數(shù)可以傳一個布爾值,如果為 true,我們就會進行深拷貝,false 依然當做淺拷貝,這個時候,target 就往后移動到第二個參數(shù)。
還是舉這個例子:
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 // }
因為采用了深拷貝,會遍歷到更深的層次進行添加和覆蓋。
extend 第二版我們來實現(xiàn)深拷貝的功能,值得注意的是:
需要根據(jù)第一個參數(shù)的類型,確定 target 和要合并的對象的下標起始值。
如果是深拷貝,根據(jù) copy 的類型遞歸 extend。
// 第二版 function extend() { // 默認不進行深拷貝 var deep = false; var name, options, src, copy; var length = arguments.length; // 記錄要復制的對象的下標 var i = 1; // 第一個參數(shù)不傳布爾值的情況下,target默認是第一個參數(shù) var target = arguments[0] || {}; // 如果第一個參數(shù)是布爾值,第二個參數(shù)是才是target if (typeof target == "boolean") { deep = target; target = arguments[i] || {}; i++; } // 如果target不是對象,我們是無法進行復制的,所以設為{} if (typeof target !== "object") { target = {} } // 循環(huán)遍歷要復制的對象們 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; };
在實現(xiàn)上,核心的部分還是跟上篇實現(xiàn)的深淺拷貝函數(shù)一致,如果要復制的對象的屬性值是一個對象,就遞歸調用 extend。不過 extend 的實現(xiàn)中,多了很多細節(jié)上的判斷,比如第一個參數(shù)是否是布爾值,target 是否是一個對象,不傳參數(shù)時的默認值等。
接下來,我們看幾個 jQuery 的 extend 使用效果:
target 是函數(shù)在我們的實現(xiàn)中,typeof target 必須等于 object,我們才會在這個 target 基礎上進行拓展,然而我們用 typeof 判斷一個函數(shù)時,會返回function,也就是說,我們無法在一個函數(shù)上進行拓展!
什么,我們還能在一個函數(shù)上進行拓展?。?/p>
當然啦,畢竟函數(shù)也是一種對象嘛,讓我們看個例子:
function a() {} a.target = "b"; console.log(a.target); // b
實際上,在 underscore 的實現(xiàn)中,underscore 的各種方法便是掛在了函數(shù)上!
所以在這里我們還要判斷是不是函數(shù),這時候我們便可以使用《JavaScript專題之類型判斷(上)》中寫得 isFunction 函數(shù)
我們這樣修改:
if (typeof target !== "object" && !isFunction(target)) { target = {}; }類型不一致
其實我們實現(xiàn)的方法有個小 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 } } }
讓我們細細分析為什么會導致這種情況:
首先我們在函數(shù)的開始寫一個 console 函數(shù)比如:console.log(1),然后以上面這個 demo 為例,執(zhí)行一下,我們會發(fā)現(xiàn) 1 打印了三次,這就是說 extend 函數(shù)執(zhí)行了三遍,讓我們捋一捋這三遍傳入的參數(shù):
第一遍執(zhí)行到遞歸調用時:
var src = { c: 2 }; var copy = { c: [5]}; target[name] = extend(true, src, copy);
第二遍執(zhí)行到遞歸調用時:
var src = 2; var copy = [5]; target[name] = extend(true, src, copy);
第三遍進行最終的賦值,因為 src 是一個基本類型,我們默認使用一個空對象作為目標值,所以最終的結果就變成了對象的屬性!
為了解決這個問題,我們需要對目標屬性值和待復制對象的屬性值進行判斷:
判斷目標屬性值跟要復制的對象的屬性值類型是否一致:
如果待復制對象屬性值類型為數(shù)組,目標屬性值類型不為數(shù)組的話,目標屬性值就設為 []
如果待復制對象屬性值類型為對象,目標屬性值類型不為對象的話,目標屬性值就設為 {}
結合著《JavaScript專題之類型判斷(下)》中的 isPlainObject 函數(shù),我們可以對類型進行更細致的劃分:
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; }循環(huán)引用
實際上,我們還可能遇到一個循環(huán)引用的問題,舉個例子:
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; // 第一個參數(shù)不傳布爾值的情況下,target 默認是第一個參數(shù) var target = arguments[0] || {}; // 如果第一個參數(shù)是布爾值,第二個參數(shù)是 target if (typeof target == "boolean") { deep = target; target = arguments[i] || {}; i++; } // 如果target不是對象,我們是無法進行復制的,所以設為 {} if (typeof target !== "object" && !isFunction(target)) { target = {}; } // 循環(huán)遍歷要復制的對象們 for (; i < length; i++) { // 獲取當前對象 options = arguments[i]; // 要求不能為空 避免 extend(a,,b) 這種情況 if (options != null) { for (name in options) { // 目標屬性值 src = target[name]; // 要復制的對象的屬性值 copy = options[name]; // 解決循環(huán)引用 if (target === copy) { continue; } // 要遞歸的對象必須是 plainObject 或者數(shù)組 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專題系列預計寫二十篇左右,主要研究日常開發(fā)中一些功能點的實現(xiàn),比如防抖、節(jié)流、去重、類型判斷、拷貝、最值、扁平、柯里、遞歸、亂序、排序等,特點是研(chao)究(xi) underscore 和 jQuery 的實現(xiàn)方式。
如果有錯誤或者不嚴謹?shù)牡胤?,請務必給予指正,十分感謝。如果喜歡或者有所啟發(fā),歡迎 star,對作者也是一種鼓勵。
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://www.ezyhdfw.cn/yun/83989.html
摘要:專題系列共計篇,主要研究日常開發(fā)中一些功能點的實現(xiàn),比如防抖節(jié)流去重類型判斷拷貝最值扁平柯里遞歸亂序排序等,特點是研究專題之函數(shù)組合專題系列第十六篇,講解函數(shù)組合,并且使用柯里化和函數(shù)組合實現(xiàn)模式需求我們需要寫一個函數(shù),輸入,返回。 JavaScript 專題之從零實現(xiàn) jQuery 的 extend JavaScritp 專題系列第七篇,講解如何從零實現(xiàn)一個 jQuery 的 ext...
摘要:寫在前面專題系列是我寫的第二個系列,第一個系列是深入系列。專題系列自月日發(fā)布第一篇文章,到月日發(fā)布最后一篇,感謝各位朋友的收藏點贊,鼓勵指正。 寫在前面 JavaScript 專題系列是我寫的第二個系列,第一個系列是 JavaScript 深入系列。 JavaScript 專題系列共計 20 篇,主要研究日常開發(fā)中一些功能點的實現(xiàn),比如防抖、節(jié)流、去重、類型判斷、拷貝、最值、扁平、柯里...
摘要:專題系列第十八篇,講解遞歸和尾遞歸定義程序調用自身的編程技巧稱為遞歸。然而非尾調用函數(shù),就會創(chuàng)建多個執(zhí)行上下文壓入執(zhí)行上下文棧。所以我們只用把階乘函數(shù)改造成一個尾遞歸形式,就可以避免創(chuàng)建那么多的執(zhí)行上下文。 JavaScript 專題系列第十八篇,講解遞歸和尾遞歸 定義 程序調用自身的編程技巧稱為遞歸(recursion)。 階乘 以階乘為例: function factorial(n...
摘要:前端日報精選從設計到源碼用強類型語言增強通過一個場景實例了解前端處理大數(shù)據(jù)的無限可能專題之從零實現(xiàn)的表單驗證第一部分使用和技巧對表單進行約束驗證中文譯即將到來的正則表達式新特性掘金個快速編程技巧個人文章周刊第期相信控制像一樣使用 2017-07-15 前端日報 精選 Redux從設計到源碼用強類型語言GraphQL增強React通過一個場景實例 了解前端處理大數(shù)據(jù)的無限可能JavaSc...
摘要:寫在前面深入系列共計篇已經(jīng)正式完結,這是一個旨在幫助大家,其實也是幫助自己捋順底層知識的系列。深入系列自月日發(fā)布第一篇文章,到月日發(fā)布最后一篇,感謝各位朋友的收藏點贊,鼓勵指正。 寫在前面 JavaScript 深入系列共計 15 篇已經(jīng)正式完結,這是一個旨在幫助大家,其實也是幫助自己捋順 JavaScript 底層知識的系列。重點講解了如原型、作用域、執(zhí)行上下文、變量對象、this、...
閱讀 2167·2021-10-11 10:59
閱讀 996·2021-09-23 11:21
閱讀 3740·2021-09-06 15:02
閱讀 1689·2021-08-19 10:25
閱讀 3513·2021-07-30 11:59
閱讀 2461·2019-08-30 11:27
閱讀 2652·2019-08-30 11:20
閱讀 3044·2019-08-29 13:15