源碼閱讀:從深克隆、淺克隆到j(luò)Query的.extend()

jQuery有個(gè).extend()方法來(lái)擴(kuò)展一個(gè)類或數(shù)組,語(yǔ)法如下:
jQuery.extend( [deep ], target, object1 [, objectN ] )
第一個(gè)可選參數(shù)deep讓我們選擇是否使用深克隆,默認(rèn)為否。
什么是深克隆、什么是淺克隆呢?

JS中的基本類型(undefined, null, Number, String, Boolean)是按值傳遞的,引用類型(array, object, function)是按址傳遞的。

淺克隆,就是常見(jiàn)的賦值(a = b)或者參數(shù)傳遞,基本類型按值傳遞,引用類型按址傳遞。
深克隆,基本類型和引用類型都按值傳遞,也就是說(shuō),所有的元素都完全克隆,與原來(lái)的元素互相獨(dú)立,之后修改其中的一個(gè)元素不會(huì)影響到另外一個(gè)。
舉個(gè)例子:

var obj = {
  a: [1, 2, 3],
  b: {b1: 1, b2: 2},
  c: 'c'
};
var obj1 = obj; // 淺克隆,引用類型按址傳遞
var obj2 = Object.assign({}, obj); // 淺克隆
var obj3 = JSON.parse(JSON.stringify(obj)); // 深克隆

obj.c = 'C'; // 改變obj
console.log(obj1.c, obj2.c, obj3.c) // C c c

console.log(obj1.a === obj.a) // true, obj.a 和 obj1.a 引用的是同一塊地址
console.log(obj2.a === obj.a) // true
console.log(obj3.a === obj.a) // false

obj.a.push(4);
console.log(obj1.a, obj2.a, obj3.a); // [ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ] [ 1, 2, 3 ]

JSON.parse(JSON.stringify(obj))有點(diǎn)奇技淫巧的意思,但是它有個(gè)小缺陷,就是只能克隆JSON對(duì)象,如果對(duì)象中包含函數(shù),函數(shù)會(huì)被忽略。

var obj = {
  a: [1, 2, 3],
  b: {b1: 1, b2: 2},
  c: function () {}
};
var obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj1) // { a: [ 1, 2, 3 ], b: { b1: 1, b2: 2 } }
// 函數(shù)被忽略

數(shù)組的concat和slice方法看起來(lái)像深克隆,但他們其實(shí)是淺克隆。
他們會(huì)逐個(gè)把數(shù)組中的值拷貝到另一個(gè)數(shù)組中,類似于這樣:

var arr1 = [1, 2, 3, {a: 4}];
var arr2 = [];
for (var i = 0; i < arr1.length; i++) {
  arr2[i] = arr1[i];
}

因此對(duì)原數(shù)組進(jìn)行修改不會(huì)影響到克隆的數(shù)組,但是對(duì)原數(shù)組中引用類型元素的修改,會(huì)影響到克隆的數(shù)組。
也就是說(shuō),雖然兩個(gè)數(shù)組指向的是不同的地址,但是數(shù)組中的引用類型元素卻指向了相同的地址。

var array = [1,2,3, {a: 4}]; 
var array_shallow = array; 
var array_concat = array.concat(); 
var array_slice = array.slice(0); 
console.log(array === array_shallow); //true 
console.log(array === array_slice); //false,“看起來(lái)”像深拷貝
console.log(array === array_concat); //false,“看起來(lái)”像深拷貝

array.push('hahaha'); // 只有array_shallow被波及
console.log(array_shallow, array_concat, array_slice) // [ 1, 2, 3, { a: 4 }, 'hahaha' ] [ 1, 2, 3, { a: 4 } ] [ 1, 2, 3, { a: 4 } ]

array[3].a = 5; // 全都被波及
console.log(array_shallow, array_concat, array_slice) // [ 1, 2, 3, { a: 5 }, 'hahaha' ] [ 1, 2, 3, { a: 5 } ] [ 1, 2, 3, { a: 5 } ]

如何實(shí)現(xiàn)深克隆呢?當(dāng)然是遞歸復(fù)制了。
對(duì)于對(duì)象或者數(shù)組中的每一個(gè)元素,如果元素為基本類型,那么可以直接賦值target[name] = obj[name],如果元素是對(duì)象或者數(shù)組,則遞歸復(fù)制:target[name] = deepClone(obj[name])。

還有一個(gè)需要考慮的是函數(shù),我們知道函數(shù)也是對(duì)象,所以直接賦值也是淺克?。?/p>

var fn = function () {
  console.log(1)
}
fn.a = 1
var fn1 = fn
fn1.a = 2
console.log(fn.a) // 2

雖說(shuō)我們一般不會(huì)給函數(shù)添加屬性,但是為了徹底貫徹“深克隆”的精神,我們可以構(gòu)造一個(gè)新函數(shù)來(lái)實(shí)現(xiàn)復(fù)制:

var fn1 = new Function ('return ' + fn.toString())()
fn1.a = 2
console.log(fn.a) // 1

于是現(xiàn)在可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的深克隆函數(shù)了:

function deepClone(obj) { // 深克隆
  if (typeof obj === 'function') { // 函數(shù)
    return new Function('return ' + obj.toString())()
  }
  if (typeof obj !== 'object') { // 基本類型
    return obj
  }
  // 對(duì)象,數(shù)組
  var value, target = {}
  if (Object.prototype.toString.call(obj) === '[object Array]') { // 數(shù)組
    target = []
  }
  for (var name in obj) {
    value = obj[name]
    if (value === obj) { // 避免死循環(huán)
      continue;
    }
    if (typeof obj[name] === 'function' || typeof obj[name] === 'object') { // 函數(shù)或者對(duì)象/數(shù)組則遞歸復(fù)制
      target[name] = deepClone(obj[name])
    } else {
      target[name] = obj[name]
    }
  }
  return target

}

var obj1 = deepClone(obj); // 對(duì)象克隆test
console.log(obj.c === obj1.c) // false
obj.a.push(4);
console.log(obj, obj1) // { a: [ 1, 2, 3, 4 ], b: { b1: 1, b2: 2 }, c: [Function: c] } { a: [ 1, 2, 3 ], b: { b1: 1, b2: 2 }, c: [Function] }

var arr = [1, 2, 3, {a: 4}] // 數(shù)組克隆test
var arr1 = deepClone(arr);
console.log(arr === arr1) // false
arr[3].a = 5
console.log(arr1[3].a) // 4

var fn = function () {
  console.log('a')
} // 函數(shù)克隆test
var fn1 = deepClone(fn)
console.log(fn, fn1) // [Function: fn] [Function]
fn(); // a
fn1(); // a
console.log(fn === fn1) // false

有了上面的基礎(chǔ),看懂.extend()的源碼就不難了:

 //給jQuery對(duì)象和jQuery原型對(duì)象都添加了extend擴(kuò)展方法
jQuery.extend = jQuery.fn.extend = function() {
  var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
  i = 1, // 下一個(gè)要處理的參數(shù)是argument[i]
  length = arguments.length,
  deep = false; // 是否為深克隆
  //以上其中的變量:options是一個(gè)緩存變量,用來(lái)緩存arguments[i],name是用來(lái)接收將要被擴(kuò)展對(duì)象的key,src改變之前target對(duì)象上每個(gè)key對(duì)應(yīng)的value。
  //copy傳入對(duì)象上每個(gè)key對(duì)應(yīng)的value,copyIsArray判定copy是否為一個(gè)數(shù)組,clone深拷貝中用來(lái)臨時(shí)存對(duì)象或數(shù)組的src。

  // 處理深拷貝的情況
  if (typeof target === "boolean") {
    deep = target;
    target = arguments[1] || {};
    //跳過(guò)布爾值和目標(biāo) 
    i++;
  }

  // 控制當(dāng)target不是object或者function時(shí),變成空對(duì)象
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }

  // 當(dāng)參數(shù)列表長(zhǎng)度等于i的時(shí)候,也就是沒(méi)有要被包含的對(duì)象了,那么擴(kuò)展jQuery對(duì)象自身。
  if (length === i) {
    target = this; --i;
  }
  for (; i < length; i++) {
    if ((options = arguments[i]) != null) {
      // 擴(kuò)展基礎(chǔ)對(duì)象
      for (name in options) {
        src = target[name];  // 擴(kuò)展的對(duì)象上的該屬性
        copy = options[name]; // 當(dāng)前對(duì)象上的該屬性

        // 防止死循環(huán),這里舉個(gè)例子,如var i = {};i.a = i;$.extend(true,{},i);如果沒(méi)有這個(gè)判斷變成死循環(huán)了
        if (target === copy) {
          continue;
        }
        // 元素為普通對(duì)象或者數(shù)組
        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
          if (copyIsArray) { // 元素為數(shù)組
            copyIsArray = false;
            // 這里可以看出對(duì)于 對(duì)象/數(shù)組 里面的 (對(duì)象/數(shù)組)元素,jq也是擴(kuò)展而不是替換
            clone = src && jQuery.isArray(src) ? src: []; // 如果src存在且是數(shù)組的話就讓clone副本等于src否則等于空數(shù)組。
          } else {
            clone = src && jQuery.isPlainObject(src) ? src: {}; // 如果src存在且是對(duì)象的話就讓clone副本等于src否則等于空對(duì)象。
          }
          // 遞歸拷貝
          target[name] = jQuery.extend(deep, clone, copy);
        } else if (copy !== undefined) { // 其他情況直接賦值
          target[name] = copy; // 若原對(duì)象存在name屬性,則直接覆蓋掉;若不存在,則創(chuàng)建新的屬性。
        }
      }
    }
  }
  // 返回修改的對(duì)象
  return target;
};

從源碼可以看出在jq中,函數(shù)被處理為淺克隆了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容