JavaScript进阶笔记(四):深浅拷贝原理

一、什么是浅拷贝?

如果拷贝的值是基本数据类型,拷贝的是基本类型的值。如果是引用类型拷贝的是内存地址。

浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址。

也就是说:只能保证第一层数据为基本数据类型时,不会随原数据改变。原数据中包含子对象时,随原数据变化。

1.1 浅拷贝的使用场景

1. Object.assign()

该方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let a = {
name: 'owenli',
book: {
title:'前端进阶之路',
price: '66'
}
}
let b = Object.assign({}, a)
console.log('b原始值')
console.log(b)
a.name = '啦啦啦'
a.book.price = '88'
console.log('修改 name 和 price 后')
console.log(a)
console.log(b) // b 中的book.price也是 88。

注意:Object.assign() 不是深拷贝。

2. 展开运算符

1
2
3
4
5
let c = {...a}
//...
console.log('展开运算符...')
console.log(c)
console.log(a)

展开运算符的效果和 Object.assign 相同。

另外,slice 、concat 等也是浅拷贝。

二、深拷贝

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。

2.1 使用场景

1. JSON.parse(JSON.stringify(object))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let a = {
name: 'owenli',
book: {
title: '啦啦啦',
price: '44'
}
}
let b = JSON.parse(JSON.stringify(a)) // 深拷贝
console.log(a)

a.name = '啦啦啦'
a.book.price = '66'

console.log(a)
console.log(b)
// a 和 b 之间互不影响。

注意:undefined、Symbol 和 函数三种情况会直接忽略。不能处理 new Date() 和正则表达式。

三、浅拷贝 Object.assigin 的原理及实现

1
Object.assign(target, ...sources)

其中 target 是目标对象,sources 是源对象,可以有多个,返回修改后的目标对象 target。
如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖。后来的源对象的属性将类似地覆盖早先的属性。

3.1 Object.assign 模拟实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 第一步判断 Object 是否支持 assign2 
if (typeof Object.assign2 != 'function') {
// 第二步:不存在assign2, 使用Object.definePerperty 创建并将函数绑定到Object上。
Object.defineProperty(Object, 'assign2', {
value: function (target) {
'use strict' // 在严格模式下对不可写的属性进行修改时会报错。默认情况下静默失败。
// 第三步:判断参数是否正确,target 不可以为空可以是 {}。
if (target == null) {
throw new TypeError('Cannot convert undefined or null to object')
}
// 第四步:使用Object() 转为对象,保存为 to,最后返回 to。将原始类型包装成对象。
var to = Object(target)
for (var index = 1; index < arguments.length; index++){
var nextSource = arguments[index]
if (nextSource != null) {
// 第五步:for...in遍历出所有可枚举的自有属性。并赋值个新的变量
for (var nextKey in nextSource) {
// 可访问性:hasOwnProperty 判断对象 nextSoruce 中是否存在 nextKey 属性。
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
// 赋值给 对象 to。
to[nextKey] = nextSource[nextKey]
}
}
}
}
return to
},
// enumerable: false, // 不可枚举,原生Object属性是不可以枚举的。默认为 false。
writable: true,
configurable: true
})
}

let a = {
name: '1111',
age: 19
}
let b = {
name: 'owenli',
book: {
title: 'lalalal',
price: 20
}
}
let c = Object.assign2(a, b)
console.log(c)

console.log(a === c)

3.2 可枚举性

原生的Object中的属性是不可以枚举的。Object.assign是否可以被枚举。

1
2
3
4
Object.getOwnPropertyDescriptor(Object, 'assign')
// {value: ƒ, writable: true, enumerable: false, configurable: true} enumerable:false 说明是不可以被枚举。
Object.propertyIsEnumerable('assign')
// false

直接向 Object 上挂载属性是可以枚举的。

1
2
3
4
5
6
Object.a = function () {
//...
}
Object.getOwnPropertyDescriptor(Object, 'a')
// {value: ƒ, writable: true, enumerable: true, configurable: true}
// 直接挂载 a ,是可以枚举的。

所以要想实现 Object.assign 就需要使用 Object.defineProperty,并设置 writable: true, enumerable: false, configurable: true。默认都是 false。

1
2
3
4
5
6
7
Object.defineProperty(Object, 'b', {
value: function() {
console.log('b')
}
})
Object.getOwnPropertyDescriptor(Object, 'b')
// {value: ƒ, writable: false, enumerable: false, configurable: false}

综上:Object.assign2 实现是需要设置 writable: true, configurable: true。因为默认是 enumerable 是 false。

3.3 参数判断

1
undefined == null // true。

在使用判断的时候直接用 target == null

3.4 原始类型包装成对象

1
2
3
4
5
6
7
8
9
10
var v1 = 'abc'
var v2 = true
var v3 = 10
var v4 = Symbol('foo')
// 原始类型会被包装,null 和 undefined 会被忽略。
var obj = Object.assign({}, v1, null, v2, undefined, v3, v4)

console.log(obj)

// {0: "a", 1: "b", 2: "c"}

v2, v3, v4 被忽略,原因是自身没有可枚举的属性。
通过 Object.keys() 和 Object.getOwnPropertyNames() 可以查看枚举属性和所有的属性。

1
2
3
4
5
6
7
8
9
// 通过 Object.keys() 测试,是否含有可枚举属性。
console.log(Object.keys(v1)) // ["0", "1", "2"]
console.log(Object.keys(v2)) // []
console.log(Object.keys(v3)) // []

// 获取所有属性 Object.getOwnPropertyNames(),无论是不是枚举类型。

console.log(Object.getOwnPropertyNames(v1)) // ["0", "1", "2", "length"]
console.log(Object.getOwnPropertyNames(v2)) // []

那没 Object.assign 是如何实现:

1
2
3
4
5
6
7
8
9
10
11
12
var a = 'abc'
var b = {
v1: 'def',
v2: true,
v3: 10,
v4: Symbol('foot'),
v5: null,
v6: undefined
}
var objc = Object.assign(a, b)
console.log(objc) // k可以获取到 v2, v3, v4 .. 的值。

原因:因为 undefined , true 不是作为对象,而是作为对象 b 的属性值,对象 b 是可以枚举的。

1
console.log(Object.keys(b)) // 可枚举

这里其实又可以看出一个问题来,那就是目标对象是原始类型,会包装成对象,对应上面的代码就是目标对象 a 会被包装成 [String: ‘abc’],那模拟实现时应该如何处理呢?很简单,使用 Object(..) 就可以了。

另外,在严格模式下向不可修改的对象属性赋值会报错。默认情况静默失败。

3.5 存在性

如何在不访问对象属性的情况下判断属性是否存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var obj = {
a: 1
}
// newObj 是关联到 obj 的对象。
var newObj = Object.create(obj)
newObj.b = 10

console.log(newObj)

// in 操作符会检查属性是否在对象及其 `[[Prototype]]`原型链中。
console.log('a' in newObj) // true
console.log('b' in newObj) // true
// 只检测是否在 newObj 中,不检查原型链。
console.log(newObj.hasOwnProperty('a')) // false
console.log(newObj.hasOwnProperty('b')) // true


两种方法的区别:

  1. in 操作符检查 对象 和 原型链。
  2. hasOwnProperty 值检查 对象,不检查原型链。

模拟实现 Object.assign 只需检测对象中属性,不需要检查原型链。

1
2
3
4
5
6
var obj1 = Object.create(null)
obj1.b = 2
// 问题: b 没有连接到prototype上
console.log(obj1.hasOwnProperty('b')) // false
console.log(Object.prototype.hasOwnProperty.call(obj1, 'b')) // true

参考

木易杨前端进阶