源码阅读:从深克隆、浅克隆到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中,函数被处理为浅克隆了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容