一、赋值
- 基本数据类型:值传递,赋值后互不影响
1 | let a = 'test'; |
- 引用数据类型:址传递,两个变量具有相同的引用,指向同一个内存地址,互相影响
1 | let obj1 = { |
通常在开发中不希望改变变量a之后会影响变量b,因此需要用的深拷贝和浅拷贝
二、浅拷贝
概念
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝
如果属性是基本类型,拷贝的就是值;如果属性是引用类型,拷贝的就是内存地址
简单来说,浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。
使用场景
1.Object.assign()
Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象,并且返回目标对象。
1 | let obj1 = { |
2.展开语法 Spread
和Object.assign()效果相同
1 | let obj1 = { |
3.Array.prototype.slice()
slice() 方法返回一个新的数组对象,这一对象是一个由 begin和 end(不包括end)决定的原数组的浅拷贝,原始类型数据不会被改变。
1 | let a = [0, "1", [2, 3]]; |
如何实现一个浅拷贝?
一个简单的实现:遍历对象属性
1 | function shadowCopy(source) { |
⚠️深入探究:Object.assign()实现
实现思路:
1.判断原生 Object 是否支持该函数,如果不存在的话创建一个函数 assign,并使用Object.defineProperty 将该函数绑定到 Object 上。
2.判断参数是否正确(目标对象不能为空,我们可以直接设置{}传递进去,但必须设置值)。
3、使用 Object() 转成对象,并保存为 to,最后返回这个对象 to。
4、使用 for..in 循环遍历出所有可枚举的自有属性。并复制给新的目标对象(使用 hasOwnProperty 获取自有属性,即非原型链上的属性)。
5、此模拟实现不支持 symbol 属性。
1 | if (typeof Object.assign2 !== 'function') { |
测试一下:
1 | let a = { name: 123 }; |
⚠️关于以上代码的说明:
注意1:可枚举性
原生情况下挂载在 Object 上的属性是不可枚举的,但是直接在 Object 上挂载属性 a 之后是可枚举的,所以这里必须使用 Object.defineProperty,并设置 enumerable: false 以及 writable: true, configurable: true。
如何查看 Object.assign 是否可枚举?
两种方法:Object.getOwnPropertyDescriptor或者Object.propertyIsEnumerable
1 | Object.getOwnPropertyDescriptor(Object, "assign"); |
再看直接在Object上挂载属性a
1 | Object.a = function () { |
通过Object.defineProperty挂载属性b, writable、enumerable、configurable默认都是false
1 | // 通过 |
注意2:判断参数是否正确
因为 undefined 和 null 相等,undefined == null 返回 true,所以只需判断是否等于null即可
1 | if (target == null) { // TypeError if undefined or null |
注意3:原始类型被包装为对象
1 | var v1 = "abc"; |
v2、v3、v4 实际上被忽略了,原因在于他们自身没有可枚举属性。
1 | var v1 = "abc"; |
目标对象是原始类型,会包装成对象,对应上面的代码就是目标对象 a 会被包装成 [String: ‘abc’],那模拟实现时应该如何处理?使用 Object(..) 就可以了
⚠️注意: Object(“abc”) 时,其属性描述符为不可写,即 writable: false。
1 | var myObject = Object( "abc" ); |
1 | var a = "abc"; |
⚠️ JS 对于不可写的属性值的修改静默失败(silently failed),在严格模式下才会提示错误,所以模拟实现要开启严格模式才会有相同效果报错。
注意4:存在性
补充知识:如何在不访问属性值的情况下判断对象中是否存在某个属性?
1 | var anotherObj = { |
⚠️ 这里涉及in操作符和hasOwnProperty 方法区别
- in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中。
- 、hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 原型链。
Object.assign 方法肯定不会拷贝原型链上的属性,所以模拟实现时需要用 hasOwnProperty(..) 判断处理下,但是直接使用 myObject.hasOwnProperty(..) 是有问题的,因为有的对象可能没有连接到 Object.prototype 上(比如通过 Object.create(null) 来创建),这种情况下,使用 myObject.hasOwnProperty(..) 就会失败。
1 | var myObject = Object.create( null ); |
解决方法:call方法
1 | var myObject = Object.create( null ); |
三、深拷贝
概念
深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。
使用场景
JSON.parse(JSON.stringify(object))
1 | let obj1 = { |
⚠️ 这个方法存在以下几个问题:
1.会忽略undefined
2.忽略symbol类型
3.不能序列化函数
1 | let obj = { |
4.不能解决循环引用的对象
1 | let obj = { |
5.不能正确处理new Date()
1 | new Date(); |
解决方法:date转成字符串或者时间戳
1 | let date = (new Date()).valueOf(); |
6.不能处理正则
1 | let obj = { |
思考:如何实现深拷贝?
参考文章:
你不知道的js(上)
掘金