源码阅读:从深克隆、浅克隆到jQuery的.extend()

jQuery有个.extend()方法来扩展一个类或数组,语法如下:
jQuery.extend( [deep ], target, object1 [, objectN ] )
第一个可选参数deep让我们选择是否使用深克隆,默认为否。
什么是深克隆、什么是浅克隆呢?

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

浅克隆,就是常见的赋值(a = b)或者参数传递,基本类型按值传递,引用类型按址传递。
深克隆,基本类型和引用类型都按值传递,也就是说,所有的元素都完全克隆,与原来的元素互相独立,之后修改其中的一个元素不会影响到另外一个。
举个例子:

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))有点奇技淫巧的意思,但是它有个小缺陷,就是只能克隆JSON对象,如果对象中包含函数,函数会被忽略。

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 } }
// 函数被忽略

数组的concat和slice方法看起来像深克隆,但他们其实是浅克隆。
他们会逐个把数组中的值拷贝到另一个数组中,类似于这样:

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

因此对原数组进行修改不会影响到克隆的数组,但是对原数组中引用类型元素的修改,会影响到克隆的数组。
也就是说,虽然两个数组指向的是不同的地址,但是数组中的引用类型元素却指向了相同的地址。

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,“看起来”像深拷贝
console.log(array === array_concat); //false,“看起来”像深拷贝

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 } ]

如何实现深克隆呢?当然是递归复制了。
对于对象或者数组中的每一个元素,如果元素为基本类型,那么可以直接赋值target[name] = obj[name],如果元素是对象或者数组,则递归复制:target[name] = deepClone(obj[name])

还有一个需要考虑的是函数,我们知道函数也是对象,所以直接赋值也是浅克隆:

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

虽说我们一般不会给函数添加属性,但是为了彻底贯彻“深克隆”的精神,我们可以构造一个新函数来实现复制:

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

于是现在可以实现一个简单的深克隆函数了:

function deepClone(obj) { // 深克隆
  if (typeof obj === 'function') { // 函数
    return new Function('return ' + obj.toString())()
  }
  if (typeof obj !== 'object') { // 基本类型
    return obj
  }
  // 对象,数组
  var value, target = {}
  if (Object.prototype.toString.call(obj) === '[object Array]') { // 数组
    target = []
  }
  for (var name in obj) {
    value = obj[name]
    if (value === obj) { // 避免死循环
      continue;
    }
    if (typeof obj[name] === 'function' || typeof obj[name] === 'object') { // 函数或者对象/数组则递归复制
      target[name] = deepClone(obj[name])
    } else {
      target[name] = obj[name]
    }
  }
  return target

}

var obj1 = deepClone(obj); // 对象克隆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}] // 数组克隆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')
} // 函数克隆test
var fn1 = deepClone(fn)
console.log(fn, fn1) // [Function: fn] [Function]
fn(); // a
fn1(); // a
console.log(fn === fn1) // false

有了上面的基础,看懂.extend()的源码就不难了:

 //给jQuery对象和jQuery原型对象都添加了extend扩展方法
jQuery.extend = jQuery.fn.extend = function() {
  var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
  i = 1, // 下一个要处理的参数是argument[i]
  length = arguments.length,
  deep = false; // 是否为深克隆
  //以上其中的变量:options是一个缓存变量,用来缓存arguments[i],name是用来接收将要被扩展对象的key,src改变之前target对象上每个key对应的value。
  //copy传入对象上每个key对应的value,copyIsArray判定copy是否为一个数组,clone深拷贝中用来临时存对象或数组的src。

  // 处理深拷贝的情况
  if (typeof target === "boolean") {
    deep = target;
    target = arguments[1] || {};
    //跳过布尔值和目标 
    i++;
  }

  // 控制当target不是object或者function时,变成空对象
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }

  // 当参数列表长度等于i的时候,也就是没有要被包含的对象了,那么扩展jQuery对象自身。
  if (length === i) {
    target = this; --i;
  }
  for (; i < length; i++) {
    if ((options = arguments[i]) != null) {
      // 扩展基础对象
      for (name in options) {
        src = target[name];  // 扩展的对象上的该属性
        copy = options[name]; // 当前对象上的该属性

        // 防止死循环,这里举个例子,如var i = {};i.a = i;$.extend(true,{},i);如果没有这个判断变成死循环了
        if (target === copy) {
          continue;
        }
        // 元素为普通对象或者数组
        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
          if (copyIsArray) { // 元素为数组
            copyIsArray = false;
            // 这里可以看出对于 对象/数组 里面的 (对象/数组)元素,jq也是扩展而不是替换
            clone = src && jQuery.isArray(src) ? src: []; // 如果src存在且是数组的话就让clone副本等于src否则等于空数组。
          } else {
            clone = src && jQuery.isPlainObject(src) ? src: {}; // 如果src存在且是对象的话就让clone副本等于src否则等于空对象。
          }
          // 递归拷贝
          target[name] = jQuery.extend(deep, clone, copy);
        } else if (copy !== undefined) { // 其他情况直接赋值
          target[name] = copy; // 若原对象存在name属性,则直接覆盖掉;若不存在,则创建新的属性。
        }
      }
    }
  }
  // 返回修改的对象
  return target;
};

从源码可以看出在jq中,函数被处理为浅克隆了。

    原文作者:Lxylona
    原文地址: https://www.jianshu.com/p/6859fb401c2a
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞