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中,函数被处理为浅克隆了。