学一下对象浅拷贝常用的Object.assign
对象浅拷贝有不少实现方法,下面就来学习一下Object.assign。
基本用法
1 const one = {a: 1, b: 2} 2 const two = {c: '3', d: '4'} 3 var three = Object.assign({e: 5}, one, two) 4 console.log(three) 5 6 //Object 7 e: 5 8 a: 1b: 2 9 c: "3" 10 d: "4" 11 __proto__: Object
Object.assign返回参数中各个对象属性合并后的一个对象
注意事项:
- 存在同名属性,后面的属性值会覆盖前面的属性值
1 var o1 = { a: 1, b: 1, c: 1 }; 2 var o2 = { b: 2, c: 2 }; 3 var o3 = { c: 3 }; 4 5 var obj = Object.assign({}, o1, o2, o3); 6 console.log(obj); // { a: 1, b: 2, c: 3 },屬性c為o3.c的值,最後一個出現的屬性c。
- symbol属性会被复制
var o1 = { a: 1 }; var o2 = { [Symbol('foo')]: 2 }; var obj = Object.assign({}, o1, o2); console.log(obj); console.log(Object.getOwnPropertySymbols(obj)); //{a: 1, Symbol(foo): 2} //[Symbol(foo)]
- 不可枚举的对象不会被合并
1 var obj = Object.create({ foo: 1 }, { // foo 是 obj 的屬性鏈。也不会被Object.aaign复制 2 bar: { 3 value: 2 // bar 是不可列舉的屬性,因為enumerable預設為false。Objecr.creat默认 4 }, 5 baz: { 6 value: 3, 7 enumerable: true // baz 是自身可列舉的屬性。 8 } 9 }); 10 11 var copy = Object.assign({}, obj); 12 console.log(copy); // { baz: 3 }
-
如果参数不是对象,则会先转成对象,然后返回。由于
undefined
和null
无法转成对象,所以如果它们作为参数,就会报错。如果只有一个参数,Object.assign
会直接返回该参数。如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果
undefined
和null
不在首参数,就不会报错。其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。1 const obj = {a: 1}; 2 Object.assign(obj) === obj // true 3 4 typeof Object.assign(2) // "object" 5 6 Object.assign(undefined) // 报错 7 Object.assign(null) // 报错 8 9 let obj = {a: 1}; 10 Object.assign(obj, undefined) === obj // true 11 Object.assign(obj, null) === obj // true
b = Object.assign({a:1, b:3 }, {a:2,b:null} ) console.log(b) //{a: 2, b: null} //注意这种情况可能会导致bug,所以后端尽量不返回null和undefined
const v1 = 'abc'; const v2 = true; const v3 = 10; const obj = Object.assign({}, v1, v2, v3); console.log(obj); // { "0": "a", "1": "b", "2": "c" } Object(true) // {[[PrimitiveValue]]: true} Object(10) // {[[PrimitiveValue]]: 10} Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
上面代码中,
v1
、v2
、v3
分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略。这是因为只有字符串的包装对象,会产生可枚举属性。布尔值、数值、字符串分别转成对应的包装对象,可以看到它们的原始值都在包装对象的内部属性[[PrimitiveValue]]
上面,这个属性是不会被Object.assign
拷贝的。只有字符串的包装对象,会产生可枚举的实义属性,那些属性则会被拷贝。 - 出现异常终止复制
1 var target = Object.defineProperty({}, 'foo', { 2 value: 1, 3 writable: false 4 }); // target.foo 是 read-only (唯讀)屬性 5 6 Object.assign(target, { bar: 2 }, { foo2: 3, foo: 3, foo3: 3 }, { baz: 4 }); 7 // TypeError: "foo" 是 read-only 8 // 在指派值給 target.foo 時,異常(Exception)會被拋出。 9 10 console.log(target.bar); // 2, 第一個來源物件複製成功。 11 console.log(target.foo2); // 3, 第二個來源物件的第一個屬性複製成功。 12 console.log(target.foo); // 1, 異常在這裡拋出。 13 console.log(target.foo3); // undefined, 複製程式已中斷,複製失敗。 14 console.log(target.baz); // undefined, 第三個來源物件也不會被複製。
- 对于单层属性,目标对象和源对象属性值的更改不会相互影响
对于深层属性,即属性的key作为引用指向另一个对象。目标拷贝的是源对象的引用,目标对象和源对象属性值的更改会相互影1 const one = {a: 1, b: {c: 2, d: 3}} 2 单层拷贝: 3 var two = Object.assign({}, one) 4 two.a = 1.1 5 one.a = 1.2 6 打印结果: 7 one = {a: 1.2, b: {c: 2, d: 3}} 8 two = {a: 1.1, b: {c: 2, d: 3}} 9 10 深层拷贝: 11 var two = Object.assign({}, one) 12 two.b.c = 10 13 one.b.d = 5 14 打印结果: 15 one = {a: 1.2, b: {c: 10, d: 5}} 16 two = {a: 1.1, b: {c: 10, d: 5}}
Object.assign
可以用来处理数组,但是会把数组视为对象。1 Object.assign([1, 2, 3], [4, 5]) 2 // [4, 5, 3]
上面代码中,
Object.assign
把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4
覆盖了目标数组的 0 号属性1
。Object.assign
只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。1 const source = { 2 get foo() { return 1 } 3 }; 4 const target = {}; 5 6 Object.assign(target, source) 7 // { foo: 1 }
上面代码中,
source
对象的foo
属性是一个取值函数,Object.assign
不会复制这个取值函数,只会拿到值以后,将这个值复制过去。
使用场景
- 为对象添加属性
class Point { constructor(x, y) { Object.assign(this, {x, y}); } } 等同于: class Point { constructor(x, y) { this.x = x this.y = y } }
- 为对象添加方法
1 Object.assign(SomeClass.prototype, { 2 someMethod(arg1, arg2) { 3 ··· 4 }, 5 anotherMethod() { 6 ··· 7 } 8 }); 9 10 // 等同于下面的写法 11 SomeClass.prototype.someMethod = function (arg1, arg2) { 12 ··· 13 }; 14 SomeClass.prototype.anotherMethod = function () { 15 ··· 16 };
- 克隆对象
1 function clone(origin) { 2 return Object.assign({}, origin); 3 } 4 上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。 5 6 不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。 7 8 9 function clone(origin) { 10 let originProto = Object.getPrototypeOf(origin); 11 return Object.assign(Object.create(originProto), origin); 12 }
4.为属性指定默认值
1 const DEFAULTS = { 2 logLevel: 0, 3 outputFormat: 'html' 4 }; 5 6 function processContent(options) { 7 options = Object.assign({}, DEFAULTS, options); 8 console.log(options); 9 // ... 10 }
上面代码中,DEFAULTS
对象是默认值,options
对象是用户提供的参数。Object.assign
方法将DEFAULTS
和options
合并成一个新对象,如果两者有同名属性,则option
的属性值会覆盖DEFAULTS
的属性值。
注意,由于存在浅拷贝的问题,DEFAULTS
对象和options
对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,DEFAULTS
对象的该属性很可能不起作用。
1 const DEFAULTS = {
2 url: {
3 host: 'example.com',
4 port: 7070
5 },
6 };
7
8 processContent({ url: {port: 8000} })
9 // {
10 // url: {port: 8000}
11 // }
上面代码的原意是将 url.port
改成 8000,url.host
不变。实际结果却是options.url
覆盖掉DEFAULTS.url
,所以url.host
就不存在了。
课外探究
除了Object.assign,展开操作符也可以用作浅拷贝
1 let obj = { 2 one: 1, 3 two: 2, 4 } 5 6 let newObj = { ...z }; 7 8 // { one: 1, two: 2 }
展开操作符和Object.assign相似,同名属性后面覆盖前面,只复制可枚举属性,多层对象内层拷贝的是引用(即浅拷贝)。但性能上,展开操作符却要强很多。
如图所示,在拷贝较多对象时,不建议使用Object.assign,展开运算符在性能上也许是更好的选择。
思考:编写函数实现展开运算符功能
1 "use strict"; 2 3 var _extends = Object.assign || function (target) { 4 for (var i = 1; i < arguments.length; i++) { 5 var source = arguments[i]; 6 for (var key in source) { 7 if (Object.prototype.hasOwnProperty.call(source, key)) { 8 target[key] = source[key]; 9 } 10 } 11 } 12 return target; 13 }; 14 15 var obj = { a: 1, b: 2, c: { d: 3 } }; 16 var shallowCopiedObj = _extends({}, obj);