本文详细解析JavaScript中基本类型与引用类型的存储机制,深入探讨浅拷贝与深拷贝的本质区别。通过Object.assign、Array.slice/concat、JSON序列化及jQuery.extend等方法,对比分析各实现方式的优缺点及局限性,帮助开发者根据实际需求选择合适的拷贝策略。
JavaScript变量类型与存储机制
1、JavaScript的变量类型
(1)基本类型:
JavaScript包含五种基本数据类型:Undefined、Null、Boolean、Number 和 String。这些类型的变量值直接存储在栈内存中,属于简单的数据段,可以直接访问。
(2)引用类型:
引用类型(如对象、数组等)存储在堆内存中,变量保存的是一个指向堆内存中实际对象的指针。访问引用类型值时,需先从栈中获取该指针,再通过指针从堆内存中读取具体数据。
由于JavaScript存储对象时均存储地址,浅拷贝会导致源对象与拷贝对象指向同一块内存地址。修改其中一方的内容,实际上是在原内存上操作,从而导致两个对象同时发生改变。而深拷贝会开辟一块新的内存地址,将原对象的各个属性逐个复制进去,使得对拷贝对象和源对象的操作互不影响。
例如:数组拷贝
//浅拷贝,双向改变,指向同一片内存空间
var arr1 = [1, 2, 3];
var arr2 = arr1;
arr1[0] = 'change';
console.log('shallow copy: ' + arr1 + " ); //shallow copy: change,2,3
console.log('shallow copy: ' + arr2 + " ); //shallow copy: change,2,3
浅拷贝的实现方式
2、浅拷贝的实现
2.1、简单的引用复制
function shallowClone(copyObj) {
var obj = {};
for ( var i in copyObj) {
obj[i] = copyObj[i];
}
return obj;
}
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = shallowClone(x);
console.log(y.b.f === x.b.f); // true
2.2、Object.assign()方法
Object.assign() 方法可以将任意多个源对象自身的可枚举属性拷贝给目标对象,并返回目标对象。需要注意的是,该方法会跳过值为 null 或 undefined 的源对象。
var x = {
a: 1,
b: { f: { g: 1 } },
c: [ 1, 2, 3 ]
};
var y = Object.assign({}, x);
console.log(y.b.f === x.b.f); // true
深拷贝的实现与局限
3、深拷贝的实现
3.1、Array的slice和concat方法
Array的slice和concat方法不会修改原数组,而是返回一个包含原数组元素的新数组。虽然它们常被误认为是深拷贝,但实际上执行的是浅拷贝。原数组元素的拷贝规则如下:
- 如果元素是对象引用(即实际的对象),slice会拷贝该引用到新数组。此时,两个数组中的元素引用同一个对象。若被引用的对象发生修改,新数组和原数组中的该元素也会随之改变。
- 对于字符串、数字及布尔值(非String、Number或Boolean对象实例),slice会拷贝这些基本值到新数组。修改新数组中的这些基本类型值,不会影响原数组。
向任一数组添加新元素,均不会影响另一个数组。具体示例如下:
var array = [1,2,3];
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,“看起来”像深拷贝
从上述示例可以看出,concat和slice返回的是不同的数组实例,这与直接引用复制(指向同一地址)不同。然而,这并非真正的深拷贝,因为数组中的对象元素(如Object、Array等)仅复制了引用。具体表现如下:
var array = [1, [1,2,3], {name:"array"}];
var array_concat = array.concat();
var array_slice = array.slice(0);
array_concat[1][0] = 5; //改变array_concat中数组元素的值
console.log(array[1]); //[5,2,3]
console.log(array_slice[1]); //[5,2,3]
array_slice[2].name = "array_slice"; //改变array_slice中对象元素的值
console.log(array[2].name); //array_slice
console.log(array_concat[2].name); //array_slice
3.2、JSON对象的parse和stringify方法
JSON对象是ES5引入的新类型(支持IE8+浏览器)。JSON.parse方法可将JSON字符串反序列化为JS对象,JSON.stringify方法可将JS对象序列化为JSON字符串。利用这两个方法可以实现对象的深拷贝。
//例1
var source = { name:"source", child:{ name:"child" } }
var target = JSON.parse(JSON.stringify(source));
target.name = "target"; //改变target的name属性
console.log(source.name); //source
console.log(target.name); //target
target.child.name = "target child"; //改变target的child
console.log(source.child.name); //child
console.log(target.child.name); //target child
//例2
var source = { name:function(){console.log(1);}, child:{ name:"child" } }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
//例3
var source = { name:function(){console.log(1);}, child:new RegExp("e") }
var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
console.log(target.child); //Object {}
该方法使用简单,能满足基本的深拷贝需求,且能处理JSON格式支持的所有数据类型。但其局限性在于:无法处理正则表达式、函数类型等(拷贝后会直接丢失相应值);此外,它会抛弃对象的constructor,即深拷贝后的对象构造函数将变为Object。同时,若对象存在循环引用,该方法也无法正确处理。
jQuery.extend源码分析与自定义实现
4、jQuery.extend()方法源码实现
jQuery的源码 - src/core.js #L121源码及分析如下:
jQuery.extend = jQuery.fn.extend = function() { //给jQuery对象和jQuery原型对象都添加了extend扩展方法
var options, name, src, copy, copyIsArray, clone,
target = arguments[0] || {},
i = 1,
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;
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;
};
jQuery的extend方法基于基本的递归思路实现了浅拷贝和深拷贝。但该方法同样无法处理源对象内部的循环引用,例如:
var a={"name":"aaa"};
var b={"name":"bbb"};
a.child=b;
b.parent=a;
$.extend(true,{},a);//直接报了栈溢出。Uncaught RangeError: Maximum call stack size exceeded
5、自己动手实现一个拷贝方法
var $ = (function(){
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]);
}
return $;
})();//类型判断
function copy(obj,deep){
if(obj === null || typeof obj !== "object"){
return obj;
}
var name, target = $.isArray(obj) ? [] : {}, value;
for(name in obj){
value = obj[name];
if(value === obj) {
continue;
}
if(deep && ($.isArray(value) || $.isObject(value))){
target[name] = copy(value,deep);
}else{
target[name] = value;
}
}
return target;
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。