为何写拷贝这篇文章?同事有一天提到了拷贝,他说赋值就是一种浅拷贝方式,另外一个同事说赋值和浅拷贝并不相同。
我也有些疑惑,因而我去MDN搜一下拷贝相关内容,发现并无关于拷贝的实质概念,没有办法只能经过实践了,同时去看一些前辈们的文章总结了这篇关于拷贝的内容,本文也属于公众号【程序员成长指北】学习路线中【JS必知必会】内容。javascript
栈内存
中(不包含闭包
中的变量)堆内存
中。而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址(引用),引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中得到实体。注意:前端
闭包
中的变量并不保存在栈内存中,而是保存在堆内存中。这一点比较好想,若是闭包
中的变量保存在了栈内存
中,随着外层中的函数从调用栈中销毁,变量确定也会被销毁,可是若是保存在了堆内存中,内存函数仍能访问外层已销毁函数中的变量。看一段对应代码理解下:function A() { let a = 'koala' function B() { console.log(a) } return B }
看一段代码java
let a ='koala'; let b = a; b='程序员成长指北'; console.log(a); // koala
基本数据类型复制配图:程序员
结论:在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。api
看一段代码数组
let a = {x:'kaola', y:'kaola1'} let b = a; b.x = '程序员成长指北'; console.log(a.x); // 程序员成长指北
引用数据类型复制配图:闭包
结论:引用类型的复制,一样为新的变量b分配一个新的值,报错在栈内存中,不一样的是这个变量对应的具体值不在栈中,栈中只是一个地址指针。两个变量地址指针相同,指向堆内存中的对象,所以b.x发生改变的时候,a.x也发生了改变。koa
不知道的api我通常比较喜欢看MDN,浅拷贝的概念MDN官方并无给出明肯定义,可是搜到了一个函数Array.prototype.slice,官方说它能够实现原数组的浅拷贝。
对于官方给的结论,咱们经过两段代码验证一下,并总结出浅拷贝的定义。函数
var a = [ 1, 3, 5, { x: 1 } ]; var b = Array.prototype.slice.call(a); b[0] = 2; console.log(a); // [ 1, 3, 5, { x: 1 } ]; console.log(b); // [ 2, 3, 5, { x: 1 } ];
从输出结果能够看出,浅拷贝后,数组a[0]并不会随着b[0]改变而改变,说明a和b在栈内存中引用地址并不相同。学习
var a = [ 1, 3, 5, { x: 1 } ]; var b = Array.prototype.slice.call(a); b[3].x = 2; console.log(a); // [ 1, 3, 5, { x: 2 } ]; console.log(b); // [ 1, 3, 5, { x: 2 } ];
从输出结果能够看出,浅拷贝后,数组中对象的属性会根据修改而改变,说明浅拷贝的时候拷贝的已存在对象的对象的属性引用。
经过这个官方的slice
浅拷贝函数分析浅拷贝定义
:
新的对象复制已有对象中非对象属性的值和对象属性的引用。若是这种说法不理解换一种一个新的对象直接拷贝已存在的对象的对象属性的引用,即浅拷贝。
语法:Object.assign(target, ...sources)
ES6中拷贝对象的方法,接受的第一个参数是拷贝的目标target
,剩下的参数是拷贝的源对象sources
(能够是多个)
let target = {}; let source = {a:'koala',b:{name:'程序员成长指北'}}; Object.assign(target ,source); console.log(target); // { a: 'koala', b: { name: '程序员成长指北' } } source.a = 'smallKoala'; source.b.name = '程序员成长指北哦' console.log(source); // { a: 'smallKoala', b: { name: '程序员成长指北哦' } } console.log(target); // { a: 'koala', b: { name: '程序员成长指北哦' } }
从打印结果能够看出,Object.assign
是一个浅拷贝,它只是在根属性(对象的第一层级)建立了一个新的对象,可是对于属性的值是对象的话只会拷贝一份相同的内存地址。
undefined
和null
没法转成对象,它们不能做为Object.assign
参数,可是能够做为源对象Object.assign(undefined) // 报错 Object.assign(null) // 报错 let obj = {a: 1}; Object.assign(obj, undefined) === obj // true Object.assign(obj, null) === obj // true
Symbol
值的属性,能够被Object.assign拷贝。这个函数在浅拷贝概念定义的时候已经进行了分析,看上文。
var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])
参数:将数组和/或值链接成新数组
let array = [{a: 1}, {b: 2}]; let array1 = [{c: 3},{d: 4}]; let array2=array.concat(array1); array1[0].c=123; console.log(array2);// [ { a: 1 }, { b: 2 }, { c: 123 }, { d: 4 } ] console.log(array1);// [ { c: 123 }, { d: 4 } ]
Array.prototype.concat也是一个浅拷贝,只是在根属性(对象的第一层级)建立了一个新的对象,可是对于属性的值是对象的话只会拷贝一份相同的内存地址。
var cloneObj = { ...obj };
let obj = {a:1,b:{c:1}} let obj2 = {...obj}; obj.a=2; console.log(obj); //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}} obj.b.c = 2; console.log(obj); //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
扩展运算符也是浅拷贝,对于值是对象的属性没法彻底拷贝成2个不一样对象,可是若是属性都是基本类型的值的话,使用扩展运算符也是优点方便的地方。
补充说明:以上4中浅拷贝方式都不会改变原数组,只会返回一个浅拷贝了原数组中的元素的一个新数组。
实现原理:新的对象复制已有对象中非对象属性的值和对象属性的引用
,也就是说对象属性并不复制到内存。
function cloneShallow(source) { var target = {}; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } return target; }
for in
for...in语句以任意顺序遍历一个对象自有的、继承的、可枚举的
、非Symbol的属性。对于每一个不一样的属性,语句都会被执行。
hasOwnProperty
语法:obj.hasOwnProperty(prop)
prop是要检测的属性字符串
名称或者Symbol
该函数返回值为布尔值,全部继承了 Object 的对象都会继承到 hasOwnProperty 方法,和 in 运算符不一样,该函数会忽略掉那些从原型链上继承到的属性和自身属性。
说了赋值操做和浅拷贝操做,你们是否是已经能想到什么是深拷贝了,下面直接说深拷贝的定义。
深拷贝会另外拷贝一份一个如出一辙的对象,从堆内存中开辟一个新的区域存放新对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
JSON.stringify()是前端开发过程当中比较经常使用的深拷贝方式。原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用JSON.parse()反序列化将JSON字符串变成一个新的对象
let arr = [1, 3, { username: ' koala' }]; let arr4 = JSON.parse(JSON.stringify(arr)); arr4[2].username = 'smallKoala'; console.log(arr4);// [ 1, 3, { username: 'smallKoala' } ] console.log(arr);// [ 1, 3, { username: ' koala' } ]
实现了深拷贝,当改变数组中对象的值时候,原数组中的内容并无发生改变。JSON.stringify()虽然能够实现深拷贝,可是还有一些弊端好比不能处理函数等。
深拷贝,主要用到的思想是递归,遍历对象、数组直到里边都是基本数据类型,而后再去复制,就是深度拷贝。
实现代码:
//定义检测数据类型的功能函数 function isObject(obj) { return typeof obj === 'object' && obj != null; } function cloneDeep(source) { if (!isObject(source)) return source; // 非对象返回自身 var target = Array.isArray(source) ? [] : {}; for(var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { if (isObject(source[key])) { target[key] = cloneDeep(source[key]); // 注意这里 } else { target[key] = source[key]; } } } return target; }
该简单深拷贝未考虑内容:
遇到循环引用,会陷入一个循环的递归过程,从而致使爆栈
// RangeError: Maximum call stack size exceeded
小伙伴们有没有什么好办法呢,能够写下代码在评论区一块儿讨论哦!
该函数库也有提供_.cloneDeep用来作 Deep Copy(lodash是一个不错的第三方开源库,有好多不错的函数,也能够看具体的实现源码)
var _ = require('lodash'); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; var obj2 = _.cloneDeep(obj1); console.log(obj1.b.f === obj2.b.f); // false
用一张图总结
今天就分享这么多,若是对分享的内容感兴趣,能够关注公众号「程序员成长指北」,或者加入技术交流群,你们一块儿讨论。
进阶技术路线
加入咱们一块儿学习吧!