深拷贝的实现
为什么会有深拷贝浅拷贝?
这牵扯到JavaScript中的数据类型。一种是基本类型,包括5中基本类型Undefined
,Null
,Boolean
,String
,Number
。这些类型的变量是按值存放的,可以直接访问。
另一种是引用类型,例如对象、数组等,这些变量实际上保存着值得一个引用,通过这个引用来找到值。
所以,浅拷贝就是直接赋值给新的变量,对于引用类型,也仅仅拷贝其一个引用而已。这个实现起来比较容易。
function copy(obj) {
var newObj = {};
for (var key in obj) {
newObj[key] = obj[key];
}
return newObj;
}
ES6中Object.assign
方法其实也是一个浅拷贝,它会将源对象可枚举的属性逐个赋值给新的对象。
浅拷贝会出现什么问题呢?因为引用类型的变量始终保存的都是一个引用,所以浅拷贝中的新的引用对象与源引用对象指向的还是一个对象,那么如果修改其任意一个对象的属性,在另一个指向该对象的变量中,也会被修改掉。
var oldObj = {prop: {innerProp: 'test prop'}};
var newObj = copy(oldObj);
console.log(newObj); //{ prop: { innerProp: 'test prop' } }
oldObj.prop.innerProp = 'changed';
console.log(oldObj); //{ prop: { innerProp: 'changed' } }
console.log(newObj); //{ prop: { innerProp: 'changed' } }
解决变量引用的问题,可以使用深拷贝。
解决问题的思路是将所有的引用类型,通过不断的递归调用,将引用类型转换为基本数据值类型保存即可。
(function ($) {
"use strict";
var types = 'Array Object String Date RegExp Function Boolean Number Null Undefined'.split(' ');
function type() {
return Object.prototype.toString.call(this).slice(8, -1);
}
for (var i = types.length; i--;) {
$['is' + types[i]] = (function (self) {
return function (elem) {
return type.call(elem) === self;
}
})(types[i]);
}
function deepCopy(deep, obj) {
if (obj === null || (typeof obj !== 'object' && !$.isFunction(obj))) {
return obj;
}
if ($.isFunction(obj)) {
return new Function("return " + obj.toString())();
} else {
var name, target = $.isArray(obj) ? [] : {}, value;
for (name in obj) {
value = obj[name];
//var a = {name: b}
//var b = {name: a}
//var c = $.extend(a, b)
//相互引用时,防止无限展开
if (value === obj) {
continue;
}
if (deep && ($.isArray(value) || $.isObject(value))) {
if (!$.isFunction(value)) {
target[name] = deepCopy(deep, value);
} else {
target[name] = new Function("return " + value.toString())();
}
} else if (value !== undefined) {
//过滤掉undefined的属性值
target[name] = value;
}
}
return target;
}
}
var testObj = [1, 2, {
word: 'hello',
name: 'Jason',
c: undefined
}, [2, 3, 4, 5]];
var objCopied = deepCopy(true, testObj);
console.log('objCopied', objCopied);//objCopied [ 1, 2, { word: 'hello', name: 'Jason' }, [ 2, 3, 4, 5 ] ]
testObj[2].name = '张三';
console.log('objOrigin', testObj);//objOrigin [ 1,2,{ word: 'hello', name: '张三', c: undefined },[ 2, 3, 4, 5 ] ]
console.log('objCopied', objCopied);//objCopied [ 1, 2, { word: 'hello', name: 'Jason' }, [ 2, 3, 4, 5 ] ]
}({}));
上述方法所示,当类型为基本数据类型时,直接返回保存数据,当为Function类型时,新建一个Function对象返回,当为引用类型时,再次调用自身,将引用类型传入,直到返回基本数据类型为止。
参考jQuery源码,将上述代码修改为可传递多参数的方式。
MyQuery.extend = MyQuery.fn.extend = function () {
var i = 1,
options, //非target参数对象,用于缓存当前遍历对象
src, //待拷贝数据对象,来自options
copy, //target中的数据对象
clone, //引用类型对象副本
name,
length = arguments.length || 0,
target = arguments[0] || {},
deep = false;
if (typeof target === 'boolean') {
deep = target;
//第一个参数为Boolean,使用第二个参数作为target
target = arguments[1] || {};
//为之后的循环跳过为boolean的参数位置
i++;
}
//参数比较奇怪的情况 $.extend('a string', {obj: 'an object'})
if (typeof target !== 'object' && Object.prototype.toString.call(target) !== '[object Function]') {
target = {};
}
//$.extend(obj)情况
if (length === i) {
target = this;
i--;
}
for (; i < length; i++) {
if ((options = arguments[i]) !== null) {
for (name in options) {
src = options[name];
copy = target[name];
//防止循环引用无限展开
//var obj1 = {a: obj2}
//var obj2 = {b: obj1}
//$.extend(obj1, obj2)
if (copy === options) {
continue;
}
if (deep) {
if (Object.prototype.toString.call(src) === '[object Object]' || Object.prototype.toString.call(src) === '[object Array]') {
clone = Object.prototype.toString.call(src) === '[object Object]' ? src : {};
clone = Object.prototype.toString.call(src) === '[object Array]' ? src : [];
target[name] = this.extend(deep, clone, src);
} else if (Object.prototype.toString.call(src) === '[object Function]') {
target[name] = new Function('return ' + src.toString())();
} else {
target[name] = src;
}
} else if (src !== undefined) {
target[name] = src
}
}
}
}
return target;
};
文章参考地址:https://github.com/wengjq/Blog/issues/3
如果您觉得本文对您有用,欢迎捐赠或留言~
- 本博客所有文章除特别声明外,均可转载和分享,转载请注明出处!
- 本文地址:https://www.leevii.com/?p=1045