手写Object.assign()

1,519 阅读4分钟

浅拷贝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"}
// } 
  1. 在第一步中,使用Object.assign把源对象b的值复制到目标对象a中,返回值定义为对象c,可以看出b会替换掉a中具有相同键的值,也就是如果目标对象中的属性具有相同的键,则属性将被源对象中的属性值覆盖。注意,返回对象c就是目标对象a。
  2. 在第二步中,修改源对象b的基本类型值和引用类型值
  3. 在第三步中,浅拷贝之后目标对象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()模拟实现

实现思路大致如下:

  1. 判断原生Object是否支持该函数,如果不存在的话创建一个函数assign, 并使用Object.defineProperty将该函数绑定到Object上
  2. 判断参数是否正确(目标对象不能为null或undefined, 可以为{}, 但必须设置值)
  3. 使用费Object()转成对象,并保存为to, 最后返回这个对象to
  4. 使用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() 时需要使用严格模式。

参考:muyiy.cn/blog/4/4.2.…