javascript ·

Object.assign应用详解

Object.assign的主要作用就是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时将目标对象返回。如果目标对象是一个已经存在的对象,此对象将被改变

语法及使用

Object.assign(target, ...sources)

语法其实很简单,target是目标对象,也就是说将后面sources的对象都复制到target中,sources可以是多个对象。我们来看看下面的例子

let obj1 = {a:1,b:2}
let obj2 = {c:3,d:4}
let obj3 = {c:3,b:4}
let overObj = Object.assign(obj1,obj2,obj3)
console.log(obj1,obj2,obj3,overObj)

输出结果为
Object.assign应用详解
根据输出结果,我们可以发现obj1变成了{a:1,b:4,c:3,d:4},这个结果是三个对象合起来以后的结果,也就是说复制到目标对象以后,他会合并覆盖相同的key和value。同样的,obj1和最终返回的overObj结果是相同的。

然后我们去改变一下overObj,看一下obj1是否会改变

overObj.a = "oecom";
console.log(overObj);
console.log(obj1);

Object.assign应用详解
根据上面的输出结果我们可以发现,overObj改变了以后,obj1也相应的改变了,这说明这两个对象指向的是同一个地址。于是当我们想拷贝一个对象的时候,我们应该将target对象设置为空对象,以防修改了原始对象。

Object.assign({},obj1,obj2,obj3);

深浅拷贝

既然说到了拷贝,我们应该考虑的是这个方法实现的是深拷贝还是浅拷贝。还是以实例来看结果比较直观。

let obj4 = {
    a:1,
    b:{
        name:"oecom",
        age:4
    }
}
let overObj1 = Object.assign({},obj4);
overObj1.b.name = "oecom.cn";
overObj1.a = "网站"
console.log(overObj1)
console.log(obj4)

输出结果如下:
Object.assign应用详解
我们可以发现这是典型的浅拷贝,针对深拷贝,需要使用其他办法,在此不过多介绍,详细可以参阅js的深拷贝和浅拷贝。所以假如源对象的属性值是一个对象的引用,那么复制拷贝结果也只指向那个引用。

在使用过程中,我们还需要注意一点继承属性和不可枚举属性是不能拷贝的。如下代码,我们采用Object.create来创建一个对象。

let obj = Object.create({foo: 1}, { // foo 是个继承属性。
    bar: {
        value: 2  // bar 是个不可枚举属性。
    },
    baz: {
        value: 3,
        enumerable: true  // baz 是个自身可枚举属性。
    }
});

let copy = Object.assign({}, obj);
console.log(copy); // { baz: 3 }

这里的输出结果
Object.assign应用详解
很明显,继承属性和不可枚举属性是不能拷贝的,这一原理其实和JSON.stringify是相同的,返回的都是自身可枚举属性。

sources的类型

这里的sources并不仅仅局限于对象,他还可以是字符串或数组,但是如果你传入的是null,布尔,数字,undefined等其他类型,则会被忽略掉。

var se = [1,2,3,4]
var msde="absd"
Object.assign({},se,msde)

输出结果如下
Object.assign应用详解
其实字符串也可以按数组来看,这样就个变量se相同了,key值为其下标。

从上面的一系列例子中我们也可以看出,Object.assign执行是有顺序的,从左往右依次执行复制操作,但是如果中间复制出现异常,则后续的复制操作则会被打断。

let target = Object.defineProperty({}, "foo", {
    value: 1,
    writable: false
}); // target 的 foo 属性是个只读属性。

Object.assign(target, {bar: 2}, {foo2: 3, foo: 3, foo3: 3}, {baz: 4});
// TypeError: "foo" is read-only
// 注意这个异常是在拷贝第二个源对象的第二个属性时发生的。

console.log(target.bar);  // 2,说明第一个源对象拷贝成功了。
console.log(target.foo2); // 3,说明第二个源对象的第一个属性也拷贝成功了。
console.log(target.foo);  // 1,只读属性不能被覆盖,所以第二个源对象的第二个属性拷贝失败了。
console.log(target.foo3); // undefined,异常之后 assign 方法就退出了,第三个属性是不会被拷贝到的。
console.log(target.baz);  // undefined,第三个源对象更是不会被拷贝到的

对于访问器的拷贝

上面已经说过了属性、继承属性、是否可枚举的操作结果,那么我们再来看看对于访问器的操作。

let obj = {
  foo: 1,
  get bar() {
    return 2;
  }
};

let copy = Object.assign({}, obj); 
console.log(copy); 

Object.assign应用详解
这里copy.bar的值来自obj.bar的getter函数的返回值,那么该如何将访问器这类属性描述也拷贝过去呢?

思路其实很简单,首先需要循环获取源对象的key,然后通过reduce累加器,在其中使用getOwnPropertyDescriptor获取属性描述并存储起来,最后通过defineProperties设置属性描述即可。


function completeAssign(target, ...sources) { sources.forEach(source => { let keysArr = Object.keys(source) let descriptors keysArr.reduce((descriptors, key) => { descriptors[key] = Object.getOwnPropertyDescriptor(source, key); return descriptors; }, {}); // Object.assign 默认也会拷贝可枚举的Symbols Object.getOwnPropertySymbols(source).forEach(sym => { let descriptor = Object.getOwnPropertyDescriptor(source, sym); if (descriptor.enumerable) { descriptors[sym] = descriptor; } }); Object.defineProperties(target, descriptors); }); return target; }

结论

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

Object.assign 方法只会拷贝源对象自身的并且可枚举的属性到目标对象。该方法使用源对象的[[Get]]和目标对象的[[Set]],所以它会调用相关 getter 和 setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用Object.getOwnPropertyDescriptor()和Object.defineProperty() 。

String类型和 Symbol 类型的属性都会被拷贝。

在出现错误的情况下,例如,如果属性不可写,会引发TypeError,如果在引发错误之前添加了任何属性,则可以更改target对象。

注意,Object.assign 不会跳过那些值为 null 或 undefined 的源对象。

浏览器兼容性

Object.assign应用详解

参与评论