这篇文章是对于,在 JS 中 深浅拷贝 问题的一个总结 :
前置知识 :
- JS 中的数据类型 ?
- 数据分类 :
- 基本数据类型 :(String,Number,Boolean,Null,Undefined,Symbol (ES6 中新增))。
- 引用数据类型 :(统称为 Object 类型,细分则有:Object,Array,Date,RegExp,Function)。
- 特点 :
- 基本数据类型 的特点 :直接存储在 栈(stack)中 的数据。
- 引用数据类型 的特点 :存储的是该对象在 栈中的引用,真实的数据 存放在 堆 内存中。
- 总结 :引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
JS 中深浅拷贝相关 :
- 深浅拷贝的含义 ?
首先我们要明确的一点是,深浅拷贝只是针对于 Object 和 Array 这样的引用数据类型的。
浅拷贝 :只复制指向某个对象的指针,而不是赋值对象本身,新的和旧的对象还是共享同一块内存 (地址)。
深拷贝 : 会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象的时候不会改到原对象 (地址和值)。
- 赋值和浅拷贝有什么区别 ?
赋值 :
当我们把一个对象赋值给一个新的变量,赋的其实是该对象在栈中的地址,并不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的改变,两个对象是联动的(地址)。
浅拷贝 :
浅拷贝是按位拷贝对象,它会创建一个新的对象,这个对象有着原始对象属性值的一份精准拷贝。如果属性是基本类型,拷贝的就是基本类型的值 (值);如果属性是内存地址(引用类型),拷贝的就是内存地址(地址),因此如果其中一个对象改变了这个地址,就会影响到另外一个对象。既:默认拷贝构造函数只是对对象进行浅拷贝复制(每个成员依次拷贝),既只复制对象空间而不复制资源。
- 浅拷贝的实现方式 :
方法一 :ES6方法 Object.assign()
Object.assign() 方法把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的只是对象的属性的引用,而不是对象本身。
let obj = {a:{a:'wuhao',b:521}};
let newObj = Object.assign({},obj);
newObj.a.a = "hello";
console.log(obj.a.a);//hello
注意 :当 object 只有一层的时候,它等同于深拷贝
let obj = {username:'wuhao'};
let newObj = Object.assign({},obj);
newObj.username = 'hello';
console.log(obj.username);//'hello'
方法二 :Array.prototype.concat()
只适用于类数组对象
let arr = [1,3,{username="wuhao"}];
let newArr = arr.concat();
newArr[2].username = 'hello';
console.log(arr[2].username);//'hello'
方法三 :Array.prototype.slice()
只适用于类数组对象
let arr = [1,3,{username:"wuhao"}];
let newArr = arr.slice();
newArr[2].username = "hello";
console.log(arr[2].username);//"hello"
关于 Array 的 slice 和 concat 方法的补充说明 :
Array 的 slice 和 concat 方法不会修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
原数组的元素会按照下述规则拷贝 :
- 如果该元素是对象引用(不是实际的对象),slice 和 concat 会拷贝这个对象引用到新的数组中。两个对象引用都引用了同一个对象。如果被引用的对象发生变化,则新的和原来的数组中的这个元素也会发生改变。
- 对于字符串,数字,布尔值,slice 和 concat 会拷贝这些值到新的数组中,在别的数组中修改这些字符串或者数字或者布尔值,将不会影响另一个数组。
let arr = [1, 3, {username: ' kobe'}];
let arr3 = arr.slice();
arr3[1] = 2
console.log(arr,arr3);//[1,3,{username:'kobe'}],[1,2,{username:"kebe"}]
方法四 :ES6扩展运算符
var obj = { a:1, arr: [2,3] };
var obj1 = {...obj}
方法五 :遍历赋值实现
//方法一
function deepClone(obj) {
// 只拷贝对象
if (typeof obj !== 'object') return;
// 根据obj的类型判断是新建一个数组还是对象
var newObj = obj instanceof Array ? [] : {};
// 遍历obj,并且判断是obj的属性才拷贝
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}
//=================================================//
//方法二
function deepClone(obj) {
if (obj == null) return null;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof obj !== 'object') return obj;
let t = new obj.constructor
for (let key in obj) {
t[key] = deepClone(obj[key])
}
return t;
}
- 深拷贝的实现方式 :
方法一 :JSON.parse(JSON.stringify())
原理 :用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse() 把字符串解析成 对象。一去一来,新的对象就产生了,而且对象会开辟新的栈,实现深拷贝。
let arr = [1,3,{username:'wuhao'}];
let newArr = JSON.parse(JSON.stringify(arr));
newArr[2].username = 'hello';
console.log(arr[2].username);//'wuhao'
注意 :这种方法只能实现 数组和对象的深拷贝,会忽略 undefined,symbol,不能序列化函数,不能解决循环引用的对象。
let arr = [1,3,{username:'wuhao'},function(){}];
let newArr = JSON.parse(JSON.stringify(arr));
newArr[2].username = 'hello';
console.log(arr,newArr);//[1,3,{username:'wuhao'},function(){}],[1,3,{username:'hello'}]
//这是因为JSON.stringify() 方法是将一个JavaScript值(对象或者数组)转换为一个 JSON字符串,不能接受函数
方法二 : 手写递归函数
原理 :递归方式 实现深度克隆原理:遍历对象,数组直到里面都是基本数据类型,然后再去复制,就是深度拷贝。
function deepClone(obj, cloneObj) {
var cloneObj = cloneObj || {};
for(var i in obj) {
// 通过遍历判断属性是否为引用类型,此处注意null因为历史遗留bug通过typeof输出为object
if(typeof obj[i] === 'object' && typeof obj[i] !== null) {
// 判断引用值是否为数据 obj[i] instanceof Array
cloneObj[i] = (obj[i].constructor === Array) ? [] : {};
// 进行递归
deepClone(obj[i], cloneObj[i]);
}else {
cloneObj[i] = obj[i];
}
}
return cloneObj;
}
方法三 : 函数库 lodash
函数库 lodash :该函数库中提供 _.cloneDeep 用来做深拷贝。
let _ = require('lodash');
let obj = {
a:1,
b:{f:{g:1}},
c:[1,2,3]
};
let newObj = _cloneDeep(obj);
console.log(obj.b.f === newObj.b.f);//true
方法四 :JQ的 extend 方法
let a=[0,1,[2,3],4],
b=$.extend(true,[],a);
a[0]=1;
a[2][0]=1;
console.log(a,b);
补充 :hasOwnProperty 方法 :
hasOwnProperty 表示是否有自己的属性。这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链。
var obj = {
a: 1,
fn: function(){},
c:{d: 5}
};
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('fn')); // true
console.log(obj.hasOwnProperty('c')); // true
console.log(obj.c.hasOwnProperty('d')); // true
console.log(obj.hasOwnProperty('d')); // false, obj对象没有d属性
var str = new String();
// split方法是String这个对象的方法,str对象本身是没有这个split这个属性的
console.log(str.hasOwnProperty('split')); // false
console.log(String.prototype.hasOwnProperty('split')); // true
Symbol :
【Symbol是一种数据类型】,表示独一无二的值!
Symbol 值是通过Symbol( )函数生成的,也就是说从现在起,对象的属性名称不只是字符串了,多了一种Symbol值,并且保证是独一无二的。
具体详情请点击链接查看:详情链接;