clone
underScore中暴露除了一个方法clone:
// Create a (shallow-cloned) duplicate of an object.
function clone(obj) {
if (!isObject(obj)) return obj;
return isArray(obj) ? obj.slice() : extend({}, obj);
}
// Is a given variable an object?
function isObject(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
}
如果是数组,直接slice()一下,返回一个新的数组,跟原数组内容相同。两个数组的指针不同,这样就实现了一次简单深拷贝。
var arr = [3, 4, 3, 4]
var newArr = arr.slice(arr)
console.log(newArr) // [3, 4, 3, 4]
console.log(newArr === arr) // false
如果是obj,调用了extend方法,这个方法在underScore专题-源码分析迭代器中讲到过,就是实现了一个浅拷贝。
extend({}, obj);
var obj = { name: '麦乐', params: { code: 300 }, list: [2, 3, [3]] }
var cloneObj = _.clone(obj)
console.log(cloneObj)
console.log(cloneObj === obj) // false
console.log(cloneObj.params === obj.params) // true
cloneObj.params.code = 200;
console.log(obj.params) // 200
cloneObj的打印结果:
副本的cloneObj跟obj不相等,指针不再指向同一个,但是,如果属性值是引用数据类型,拿到的就是对象的引用:
console.log(cloneObj.params === obj.params) // true
对cloneObj.params对象属性的修改会直接影响到原对象。这样的clone不属于真正的clone,我们要做到跟原对象完全解藕,这样才是干净的clone。
下面参考jQuery的extend方法,实现一下深度clone:
辅助函数:判断是不是数组,这里用一个柯里化函数tagTester,先固定一个参数,生成一个函数,再接受剩下的参数,实现参数复用。
柯里化函数:Currying(果然是满满的英译中的既视感),是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。
// Internal function for creating a toString-based type tester.
function tagTester(name) {
return function(obj) {
return toString.call(obj) === '[object ' + name + ']';
};
}
// Is a given value an array?
// Delegates to ECMA5's native Array.isArray
var isArray = nativeIsArray || tagTester('Array');
要实现深度拷贝,其实很简单,就是判断一下属性值是不是对象,如果是,递归处理,如果不是,直接赋值:
function deepClone(obj) {
if (!isObject(obj)) return obj;
var target = {}, copy = ''
for (var key in obj) {
isObject(obj[key]) ? copy = deepClone(obj[key]) : copy = obj[key]
target[key] = copy
}
return target;
}
字面意思理解就这么简单,就能实现深度拷贝,跟原对象解藕,但是这样写会不会有问题?验证一下:
function deepClones(obj) {
if (!isObject(obj)) return obj;
var target = {}, copy = ''
for (var key in obj) {
isObject(obj[key]) ? copy = deepClone(obj[key]) : copy = obj[key]
target[key] = copy
}
return target;
}
var obj = { name: '麦乐', params: { code: 300, list: [4, 3, 2] }, list: [2, 3, [3]] }
var cloneObj = deepClones(obj)
console.log(cloneObj)
console.log(cloneObj === obj) // false
console.log(cloneObj.params === obj.params) // false
console.log(cloneObj.params.list === obj.params.list) // false
不管嵌套多么深,引用类型指针都不相同,也就是实现了完全解藕。
但是,看cloneObj的打印结果:
list原本是数组,却被拷贝成了对象,内部的数组还保持这原来数组,发现了漏洞, isObject是判断数据是不是对象,包裹数组,对象和函数, for in 循环的即有数组也有对象,而拷贝的过程中,并不知道到底是对象还是数组,所以这里就出现了问题:
isObject(obj[key]) ? copy = deepClone(obj[key]) : copy = obj[key]
为了解决上面的漏洞,这样来实现克隆函数:
function deepClone(obj) {
if (!isObject(obj)) return obj;
var target = {}, copy = ''
for (var key in obj) {
var source = obj[key];
// 这个拷贝的过程,会将[3,2] -> {0:3,1:2}这样的对象
isObject(source) ? copy = deepClone(source) : copy = source
if (isArray(source) && !copy.length) {
// 把copy,也就是{0:3,1:2}转换为类数组
// 类数组是带有length的对象,并且key 跟数组的下标一致
copy.length = source.length;
// 把类数组转换为数组
copy = Array.prototype.slice.call(copy)
}
target[key] = copy
}
return target;
}
原对象中的值如果是数组并且拷贝过来后没有length,也是就是拷贝成了对象,就增加length属性,变成类数组,再转化为数组。
如果说值是一个函数:
var obj = { name: '麦乐', params: { code: function() { }, list: [4, 3, 2] }, list: [2, 3, [3]] }
好像并没有拷贝出来,这是因为isObject函数,把函数也列入来里面。
function isObject(obj) {
var type = typeof obj;
return type === 'function' || type === 'object' && !!obj;
}
如果把调整一下这个函数:
function isObject(obj) {
var type = typeof obj;
return type === 'object' && !!obj;
}
var obj = { name: '麦乐', params: { code: function() { }, list: [4, 3, 2] }, list: [2, 3, [3]] }
var cloneObj = deepClone(obj)
console.log(cloneObj)
console.log(cloneObj === obj) // false
console.log(cloneObj.params === obj.params) // false
console.log(cloneObj.params.list === obj.params.list) //false
console.log(cloneObj.params.code === obj.params.code) // true
函数的引用指向的是同一个。这里就先不考虑函数了。只考虑是对象,数组,非null
function isObject(obj) {
var type = typeof obj;
return type === 'object' && !!obj;
}
当然这只是解决问题的一种方式,稍后会一起来了解一下jQuery中的实现方式。
上面问题可以换一种更简单的方式来解决:
function deepCopy(obj) {
let target = Array.isArray(obj) ? [] : {}
for(let key in obj) {
const sourceV = obj[key]
if(sourceV && typeof sourceV === 'object') {
target[key] = deepCopy(sourceV)
} else {
target[key] = sourceV;
}
}
return target
}