Skip to main content

Object.assign()

语法

assign<T, U, V>(target: T, source1: U, source2: V): T & U & V;

描述

用于将所有 可枚举非继承 的属性(可以是 Symbol)从一个或多个源对象复制到目标对象,它将返回目标对象。其中第一个参数为目标对象,剩余参数为源对象。

  • 如果目标对象和源对象具有相同的属性,则该属性将被源对象中的属性覆盖;类似地,后面的源对象的属性将覆盖前面的源对象的属性

  • 当源参数是基本数据类型时,null 和 undefined 会被忽略,其他类型会被转换成对象。由于只有字符串的包装对象才可能有自身可枚举属性,所以其他基本类型作为源参数会被忽略

  • 如果源对象某个属性的属性值是 对象,那么目标对象拷贝得到的只是这个对象的引用(浅拷贝)

示例

参数问题

不传入任何参数直接报错:Uncaught TypeError: Cannot convert undefined or null to object.

不能将 undefinednull 作为源对象,否则报错,同屋类型同上,因为两者不能被转换成对象类型。

当参数只有一个且为引用类型时,直接返回该参数;若该参数是除 undefined、null 之外的基本数据类型,将会执行装箱操作。

// 以下三个全部报错
Object.assign()
Object.assign(undefined)
Object.assign(null)

// 当只有一个参数且参数为引用类型时直接返回该参数
Object.assign([1, 2, 3]) // [1, 2, 3]

// 除 undefined 和 null 以外的基本数据类型会被包装成对象形式
Object.assign(100).__proto__ === new Number(100).__proto__ // true

基本数据类型会被包装成对象

当源参数是基本数据类型时,null 和 undefined 会被忽略,其他类型会被转换成对象。由于只有字符串的包装对象才可能有自身可枚举属性,所以其他基本类型作为源参数会被忽略。

// null undefined number boolean sysmbol 将会被忽略
Object.assign({}, 0, 'abc', Symbol('name'), null, undefined, true) // { '0': 'a', '1': 'b', '2': 'c' }

数组会被转换成类数组对象

下面这个例子中, 第一个数组 [1, 2, 3] 被转换成 {'0': 1, '1': 2, '2': 3}, 第二个数组 [4, 5] 被转换成 {'0': 4, '1': 5}, 后面的类数组对象将前面的类数组对象的同名属性覆盖, 故结果为 [4, 5, 3]

数组会被转换成类数组对象

再看一个例子, 第一个是数组, 第二个是对象, 两者 assgin 变成了如下的样子:

数组会被转换成类数组对象

简单的对象复制

const target = {
name: 'Sayaka',
}

// 源对象1会覆盖掉与目标对象相同的属性,所以 name: 'Yancey' 会覆盖掉 name: 'Sayaka'
const source_1 = {
name: 'Yancey',
say() {},
}

// 源对象2因为后于源对象1,所以 name: 'Lucy' 会覆盖掉 name: 'Yancey'
const source_2 = {
name: 'Lucy',
nullType: null,
}

Object.assign(target, source_1, source_2) // { name: 'Lucy', say: [Function: say], nullType: null }

浅拷贝

如果源对象中某个属性的属性值是 对象,那么目标对象拷贝得到的只是这个对象的引用。

看下面这个例子,拷贝一个对象 source 之后,我们分别修改 source.namesource.food.japaneseFood 的值,打印出来发现 copy.name 还是 Yancey,而 copy.food.japaneseFood 却变成了 ['sushi', 'udon', 'natto'],也就是说拷贝之后 copy.foodsource.food 仍然指向的是同一个堆。

const source = {
food: {
japaneseFood: ['sushi', 'udon'],
},
name: 'Yancey',
}

const copy = Object.assign({}, source)

// 分别修改 name 属性和 food 属性
source.name = 'Sayaka'
source.food.japaneseFood = [...source.food.japaneseFood, 'natto']

copy.name // 'Yancey'

copy.food.japaneseFood // ['sushi', 'udon', 'natto']

// 两者指向了同一个堆
copy.food === source.food // true

异常会打断后续拷贝

拷贝的源对象是按顺序读取,一旦中途出错,将停止拷贝。

const target = Object.defineProperty({}, 'foo', {
value: 1,
writable: false,
})

// 直接报错
// TypeError: Cannot assign to read only property 'foo' of object '#<Object>'
Object.assign(
target,
{
bar: 2,
},
{
foo2: 3,
// 错误出在了这一行
foo: 3,
foo3: 3,
},
{
baz: 4,
},
)

target.bar // 2
target.foo2 // 3

// 拷贝终止
target.foo // 1
target.foo3 // undefined
target.baz // undefined

扩展

给类添加属性

传统上我们给类添加属性是这样的,虽然无伤大雅,但还是有些繁琐。所以我们可以使用 Object.assign(),并且 在 ES6 中,当对象的属性和形参一样时,可以省略属性值,这就更方便了。

class Rectangle {
constructor(width, height) {
this.width = width
this.height = height
}
}

// use Object.assign()
class Rectangle {
constructor(width, height) {
Object.assign(this, { width, height })
}
}

给构造函数批量添加方法

回到传统 ES5 的面向对象,我们一般会把方法挂载到构造函数的原型上,老方法是一个一个的在原型上添加,而现在使用 Object.assign() 可以一次性批量添加。

function Dog(name) {
this.name = name
}

Dog.prototype.say = () => {
return 'say something...'
}
Dog.prototype.bark = () => {
return 'yamette...'
}

// use Object.assign()
Object.assign(Dog.prototype, {
say() {
return 'say something...'
},
bark() {
return 'yamette...'
},
})

Object.assign() "四宗罪"

  • 只能拷贝源对象的可枚举的自身属性

  • 无法拷贝属性的特性们

  • 访问器属性会被转换成数据属性

  • 无法拷贝源对象的原型

后面的章节在讲到 Object.getOwnPropertyDescriptors 时会来解决这个问题,具体可以戳 解决 Object.assign() 浅拷贝问题