浅拷贝Object.assign()
作用:主要是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时返回目标对象。
语法:
Object.assign(target, ...sources)
其中target是目标对象,sources是源对象,可以有多个,返回修改后的目标对象target。
如果目标对象中的属性具有相同的键,则属性值将被源对象中的属性值覆盖。
示例1:
// 第一步
let a = {
name: "dami",
age: 3
}
let b = {
name: "doudou",
info: {
title: "a cute cat",
price: "6000"
}
}
let c = Object.assign(a, b);
console.log(c);
// {
// name: "doudou",
// age: 3,
// info: {title: "a cute cat", price: "6000"}
// }
console.log(c===a) // true
// 第二步
b.name = 'change'
b.info.price = '10000'
console.log(b)
// {
// name: "change",
// age: 3,
// info: {title: "a cute cat", price: "10000"}
// }
// 第三步
console.log(a)
// {
// name: "doudou",
// age: 3
// info: {title: "a cute cat", price: "10000"}
// }
- 在第一步中,使用Object.assign把源对象b的值复制到目标对象a中,返回值定义为对象c,可以看出b会替换掉a中具有相同键的值,也就是如果目标对象中的属性具有相同的键,则属性将被源对象中的属性值覆盖。注意,返回对象c就是目标对象a。
- 在第二步中,修改源对象b的基本类型值和引用类型值
- 在第三步中,浅拷贝之后目标对象a的基本类型值没有改变,但是引用类型值发生了改变,因为Object.assign()拷贝的是属性值。假如源对象的属性是一个指向对象的引用,它也只拷贝那个引用地址。
示例2: Symbol类型的属性会被拷贝,而且不会跳过那些值为null和undefined的源对象。
let a = {
name: "doudou",
age: 3
}
let b = {
b1: Symbol("doudou"),
b2: null,
b3: undefined
}
let c = Object.assign(a, b);
console.log(c);
// {
// name: "doudou",
// age: 3,
// b1: Symbol(doudou),
// b2: null,
// b3: undefined
// }
console.log(a === c); // true
Object.assign()模拟实现
实现思路大致如下:
- 判断原生Object是否支持该函数,如果不存在的话创建一个函数assign, 并使用Object.defineProperty将该函数绑定到Object上
- 判断参数是否正确(目标对象不能为null或undefined, 可以为{}, 但必须设置值)
- 使用费Object()转成对象,并保存为to, 最后返回这个对象to
- 使用for...in循环遍历出所有可枚举的自有属性,并复制给新的目标对象(使用hasOwnProperty获取自有属性,即非原型链上的属性)
实现代码如下,这里为了验证方便,使用 assign2 代替 assign。注意此模拟实现不支持 symbol 属性,因为ES5 中根本没有 symbol
if(typeof Object.assign2 !=='function') {
// 注意1:原生情况下挂载到Object上的属性是不可枚举的,但是直接Object上挂载属性a之后是可枚举的,所以这里必须使用Object.defineProperty,并设置enumerable: false以及writable: true, configurable :true。当然默认情况下不设置就是 false。
Object.defineProperty(Object, 'assign2', {
value: function(target) {
'use strict'
// 注意2: 判断目标对象不能为undefined或null
if(target == null) {
throw new TypeEoor('Cannot convert null or undefined to object')
}
// 注意3: Object.assign()目标对象是原始类型时,会包装成对象,这里使用Object(..)就可以了
const to = Object(target)
for(let i = 1; i< arguments.length; i++) {
const nextSource = arguments[i]
if(nextSource != null) { // 注意2
// 注意4:Object.assign 方法肯定不会拷贝原型链上的属性,所以模拟实现时需要用 hasOwnProperty(..) 判断处理下,但是直接使用 myObject.hasOwnProperty(..) 是有问题的,因为有的对象可能没有连接到 Object.prototype 上(比如通过 Object.create(null) 来创建),这种情况下,使用 myObject.hasOwnProperty(..) 就会失败,解决方法便是如下所示:
for(const key in nextSource) {
if(Object.prototype.hasOwnProperty.call(nextSource, key)) {
to[key] = nextSource[key]
}
}
}
}
return to
},
writable: true,
configurable: true
})
}
PS: 为什么要使用严格模式? 先看个示例:
const a = "abc";
const b = "def";
Object.assign(a, b); // TypeError: Cannot assign to read only property '0' of object '[object String]'
原因在于 Object("abc") 时,其属性描述符为不可写,即 writable: false。
const myObject = Object( "abc" );
Object.getOwnPropertyNames( myObject );
// [ '0', '1', '2', 'length' ]
Object.getOwnPropertyDescriptor(myObject, "0");
// {
// value: 'a',
// writable: false, // 注意这里
// enumerable: true,
// configurable: false
// }
同理,下面的代码也会报错。
const a = "abc";
const b = {
0: "d"
};
Object.assign(a, b);
// TypeError: Cannot assign to read only property '0' of object '[object String]'
但是并不是说只要 writable: false 就会报错,看下面的代码。
const myObject = Object('abc');
Object.getOwnPropertyDescriptor(myObject, '0');
// {
// value: 'a',
// writable: false, // 注意这里
// enumerable: true,
// configurable: false
// }
myObject[0] = 'd';
// 'd'
myObject[0];
// 'a'
这里并没有报错,原因在于JS对于不可写的属性值的修改静默失败(silently failed),在严格模式下才会提示错误。
'use strict'
const myObject = Object('abc');
myObject[0] = 'd';
// TypeError: Cannot assign to read only property '0' of object '[object String]'
所以我们在模拟实现 Object.assign() 时需要使用严格模式。