此文首发于 https://lijing0906.github.io
上周在总结赋值和深浅拷贝的时候提到了Object.assign这种浅拷贝方式。这周谈谈它的原理以及实现方式。

浅拷贝Object.assign

上篇文章有讲到它的定义和用法,主要是将所有可枚举属性的值从一个或多个源对象中复制到目标对象,同时返回目标对象。
语法如下:

Object.assign(target,  ...source)

其中target是目标对象,...source是源对象,可以是一个或多个,返回修改后的目标对象。
如果目标对象和源对象具有相同属性,则目标对象的该属性将会被源对象的相同属性覆盖,后来的源对象的属性将会类似地覆盖早先的属性。

示例1

我们知道浅拷贝就是拷贝对象的第一层的基本类型值,以及第一层的引用类型地址。

// 第一步
let a = {name: "Kitty",age: 18
}
let b = {name: "Jane",book: {title: "You Don't Know JS",price: "45"}
}
let c = Object.assign(a, b);
console.log(c);
// {//     name: "Jane",
//     age: 18,
//     book: {title: "You Don't Know JS", price: "45"}
// }
console.log(a === c); // true// 第二步
b.name = "change";
b.book.price = "55";
console.log(b);
// {//     name: "change",
//     book: {title: "You Don't Know JS", price: "55"}
// } // 第三步
console.log(a);
// {//     name: "Jane",
//     age: 18,
//     book: {title: "You Don't Know JS", price: "55"}
// }

1、第一步中,使用Object.assign把源对象b中的属性复制到目标对象a中,把改变后的对象定义为c,可以看出b会替换掉a中相同的属性的值。上面的代码需要注意的是,返回对象c就是目标对象a。
2、第二步中,修改源对象b的基本类型值(name)和引用类型值(book)。
3、第三步中,浅拷贝之后目标对象a的基本类型值没有改变,但引用类型值被改变了,因为Object.assign拷贝的是属性值,当属性值是一个指向对象的引用时,它拷贝的那个引用地址。

示例2

String类型和Symbol类型的属性都会被拷贝,而且不会跳过那些值为nullundefined的属性。

let a = {name: "Jane",age: 20
}
let b = {b1: Symbol("Jane"),b2: null,b3: undefined
}
let c = Object.assign(a, b);
console.log(c);
// {//     name: "Jane",
//     age: 20,
//     b1: Symbol(Jane),
//     b2: null,
//     b3: undefined
// }
console.log(a === c); // true

Object.assign模拟实现

实现一个Object.assign大致思路如下:

1、判断原生Object是否支持该函数,如果不存在的话创建一个函数assign,并使用Object.defineProperty将该函数绑定到Object上。
2、判断参数是否正确(目标对象不能为空,我们可以直接设置{}传递进去,但必须设置值)。
3、使用Object()转成对象,并保存为to,最后返回这个对象to
4、使用for...in循环遍历出所有可枚举的自有属性。并复制给新的目标对象(hasOwnProperty返回非原型链上的属性)。
为了方便验证方便,使用assign2代替assign,注意以下模拟实现不支持symbol属性,因为ES5中根本没有symbol,实现代码如下:

if (typeof Object.assign2 != 'function') {Object.defineProperty(Object, 'assign2', { // 注意点1value: function(target) {'use strict';if (target == null) { // 注意点2throw new Error('Cannot convert undefined or null to object');}var to = Object(target); // 注意点3for (var index = 1; index < arguments.length; index++) {var nextSource = arguments[index];if (nextSource != null) { // 注意点2// 注意点4for (var nextKey in nextSource) {if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {to[nextKey] = nextSource[nextKey];}}}}return to;},writable: true,configurable: true})
}

测试一下:

let a = {name: "advanced",age: 18
}
let b = {name: "Jane",book: {title: "You Don't Know JS",price: "45"}
}
let c = Object.assign2(a, b);
console.log(c);
// {//     name: "Jane",
//     age: 18,
//     book: {title: "You Don't Know JS", price: "45"}
// }
console.log(a === c); // true

注意点1 可枚举型

原生情况下挂载在Object上的属性是不可枚举的,但是直接在Object上挂载属性a之后是可枚举的,所以必须使用Object.defineProperty,并设置enumerable: falsewritable: true, configurable: true
以下代码说明Object上的属性不可枚举:

for (var i in Object) {console.log(Object[i]);
}
// 无输出
Object.keys(Object); // []

我们可以使用2种方法查看Object.assign是否可枚举,使用Object.getOwnPropertyDescriptor或者Object.propertyIsEnumerable都可以,其中propertyIsEnumerable(...)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable: true。具体用法如下:

Object.getOwnPropertyDescriptor(Object, 'assign');
// {//     value: ƒ,
//     writable: true,     // 可写
//     enumerable: false,  // 不可枚举,注意这里是 false
//     configurable: true  // 可配置
// }
Object.propertyIsEnumerable('assign'); // false

来看看直接在Object上挂载属性a之后可枚举的情况:

Object.a = function() {console.log('log a');
}
Object.getOwnpropertyDescriptor(Object, 'a');
// {//      value: ƒ,
//      writable: true,
//      enumerable: true,  // 注意这里是 true
//      configurable: true
// }
Object.propertyIsEnumerable('a'); // true

因为Object.assign是不可枚举的,所以不能用直接挂载的方式(可枚举)来模拟实现,必须用Object.defineProperty来设置writable: true, enumerable: false, configurable: true,当然默认情况下都是false

Object.defineProperty(Object, 'b', {value: function() {console.log('log b');}
})
Object.getOwnPropertyDescriptor(Object, 'b');
// {//     value: ƒ,
//     writable: false,     // 可写
//     enumerable: false,  // 不可枚举,注意这里是 false
//     configurable: false  // 可配置
// }

注意点2 判断参数是否正确

因为undefinednull是相等的,即undefined == null返回true,只需要按照如下方式判断就好了。

if (target == null) { // TypeError if undefined or nullthrow new TypeError('Cannot convert undefined or null to object');
}

注意点3 原始类型被包装为对象

var v1 = 'abc';
var v2 = true;
var v3 = 10;
var v4 = Symbol('foo');
var obj = Object.assgin({}, v1, null, v2, undefined, v3, v4);
// 原始类型会被包装,null和undefined会被忽略
// 注意,只有字符串的包装对象才可能有自身可枚举属性
console.log(obj); // {'0': 'a', '1': 'b', '2': 'c'}

上面的代码可以看出v1、v2、v3实际上被忽略了,原因在于他们自身没有可枚举属性

var v1 = 'abc';
var v2 = true;
var v3 = 10;
var v4 = Symbol('foo');// Object.keys() 返回一个数组,包含所有可枚举属性
// 只会查找对象直接包含的属性,不查找[[Prototype]]链
Object.keys( v1 ); // [ '0', '1', '2' ]
Object.keys( v2 ); // []
Object.keys( v3 ); // []
Object.keys( v4 ); // []
Object.keys( v5 ); // TypeError: Cannot convert undefined or null to object// Object.getOwnPropertyNames(..) 返回一个数组,包含所有属性,无论它们是否可枚举
// 只会查找对象直接包含的属性,不查找[[Prototype]]链
Object.getOwnPropertyNames( v1 ); // [ '0', '1', '2', 'length' ]
Object.getOwnPropertyNames( v2 ); // []
Object.getOwnPropertyNames( v3 ); // []
Object.getOwnPropertyNames( v4 ); // []
Object.getOwnPropertyNames( v5 ); // TypeError: Cannot convert undefined or null to object

但是下面的代码是可以执行的:

var a = 'abc';
var b = {v1: 'def',v2: true,v3: 10,v4: Symbol('foo'),v5: null,v6: undefined
}
var obj = Objec.assign(a, b);
console.log(obj);
// {//     [String: 'abc']
//     v1: 'def',
//     v2: true,
//     v3: 10,
//     v4: Symbol('foo'),
//     v5: null,
//     v6: undefined
// }

原因很简单,因为此时undefinedtrue等不适 作为对象,而是作为对象b的属性值,对象b是可枚举的。

Object.keys(b); // [ 'v1', 'v2', 'v3', 'v4', 'v5', 'v6' ]

这里其实又可以看出一个问题来,那就是目标对象如果是原始类型,会被包装成对象,对应上面的代码就是目标对象a会被包装成[String: 'abc'],那模拟实现时应该如何处理呢?很简单,使用Object()就OK。

var a = 'abc';
console.log(Object(a)); // [String: 'abc']

到这里已经介绍了很多知识了,让我们再来延伸一下。

var a = 'abc';
var b = 'def';
Object.assign(a, b); // TypeError: Cannot assign to read only property '0' of object '[object String]'

报错的原因在于Object.assgin()时,其属性描述符为不可写,即writable: false

var myObject = Object('abc');
Object.getOwnPropertyNames(myObject);
// [ '0', '1', '2', 'length' ]Object.getOwnPropertyDescriptor(myObject, '0');
// {
//   value: 'a',
//   writable: false, // 注意这里
//   enumerable: true,
//   configurable: false
// }

注意点4 存在性

如何在不访问属性值的情况下判断对象中是否存在某个属性呢:

var anotherObject = {a: 1
};// 创建一个关联到anotherObject的对象
var myObject = Object.create(anotherObject);
myObject.b = 2;('a' in myObject); // true
('b' in myObject); // truemyObject.hasOwnProperty('a'); // false
myObject.hasOwnProperty('b'); // true

上边用in操作符和hasOwnProperty方法,区别如下:

  1. in操作符会检查属性是否在对象及其[[Prototype]]原型链上
  2. hasOwnProperty()只会检查属性是否存在于myObject对象中,不会检查[[Prototype]]原型链
    Object.assign方法肯定不会拷贝原型链上的属性,所以模拟实现时需要用hasOwnProperty()判断处理下,但是直接使用myObject.hasOwnProperty()是有问题的,因为有的对象可能没有连接到Object.prototype上(比如通过 Object.create(null)来创建),这种情况下,使用myObject.hasOwnProperty()就会失败。
var myObject = Object.create(null);
myObject.b = 2;('b' in myObject); // truemyObject.hasOwnProperty( 'b' ); // TypeError: myObject.hasOwnProperty is not a function

解决方法也很简单,使用之前介绍的call就可以了,使用如下:

var myObject = Object.create(null);
myObject.b = 2;Object.prototype.hasOwnProperty.call(myObject, 'b'); // true

参考文章

Object.assign的原理及其实现方式相关推荐

  1. Object.assign 原理及其实现

    引言 上篇文章介绍了赋值.浅拷贝和深拷贝,其中介绍了很多赋值和浅拷贝的相关知识以及两者区别,限于篇幅只介绍了一种常用深拷贝方案. 本篇文章会先介绍浅拷贝 Object.assign 的实现原理,然后带 ...

  2. 【进阶4-2期】Object.assign 原理及其实现

    引言 上篇文章介绍了赋值.浅拷贝和深拷贝,其中介绍了很多赋值和浅拷贝的相关知识以及两者区别,限于篇幅只介绍了一种常用深拷贝方案. 本篇文章会先介绍浅拷贝 Object.assign 的实现原理,然后带 ...

  3. 【JS】307- 复习 Object.assign 原理及其实现

    点击上方"前端自习课"关注,学习起来~ 引言 上篇文章介绍了赋值.浅拷贝和深拷贝,其中介绍了很多赋值和浅拷贝的相关知识以及两者区别,限于篇幅只介绍了一种常用深拷贝方案. 本篇文章会 ...

  4. JavaScript系列—Object.assign()介绍以及原理实现

    Object.assign()主要是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时返回目标对象 语法如下所示: Object.assign(target, ...sources) 其中 t ...

  5. JavaScript 复制对象与Object.assign方法无法实现深复制

    在JavaScript这门语言中,数据类型分为两大类:基本数据类型和复杂数据类型.基本数据类型包括Number.Boolean.String.Null.String.Symbol(ES6 新增),而复 ...

  6. 对展开运算符和object.assign()的理解

    Object.assign Object.assign是用来合并对象是浅拷贝,实现原理主要是将所有可枚举属性的值从一个或多个源对象复制到目标对象,同时返回目标对象.主要是用使用 for-in 循环遍历 ...

  7. assign深拷贝_经典前端面试题: Object.assign 是浅拷贝还是深拷贝?实现深拷贝的方法有哪些?...

    Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象.它将返回目标对象. 如果目标对象中的属性具有相同的键,则属性将被源对象中的属性覆盖.后面的源对象的属性将类 ...

  8. Object.create()和Object.assign()

    一.Object.create() 该方法用于创建一个新对象,并为其指定原型对象和属性. 语法:Object.create(prototype,description); proto:(必须),表示新 ...

  9. ES6实用方法Object.assign、defineProperty、Symbol

    文章目录 1.合并对象 - Object.assign() 介绍 进阶 注意 用途 2.定义对象 - Object.defineProperty(obj, prop, descriptor) 3.新数 ...

最新文章

  1. Spring官方都推荐使用的@Transactional事务,为啥我不建议使用!
  2. .gitmodule中没有为非子模块的路径找到子模块映射
  3. I2C总线学习(二)--数据传送格式
  4. 订单库存是否与库存地有关
  5. 【电路原理】学习笔记(1):电路模型的基本变量
  6. 合并百度影音的离线数据 with python 第二版 基于yield
  7. 几行Python代码,爱上Python编程
  8. bootstrap-table使用 带条件查询翻页及数据更新的问题。
  9. 1.登录mysql数据库_MySql使用全记录1 -----使用命令登录数据库
  10. 华为服务器软件维护,服务器系统软件维护
  11. SAP Brazil J1BTAX 为税收例外创建税收组(翻译)
  12. 离散数学 习题篇 —— 集合相等与子集关系判断
  13. qca956x flash usb
  14. 注意力、自注意力和多头注意力
  15. Echarts---介绍/柱形图和扇形图
  16. 使用for循环编写倒立反方向直角三角形
  17. 共轭转置matlab,何为共轭转置?
  18. 达梦8 DCA学习笔记
  19. C++上机实验三第2题
  20. bugku md5加密相等绕过

热门文章

  1. [高中作文赏析]跋涉与成功
  2. 2021年中国集成电路市场现状及封测行业重点企业对比分析:通富微电vs华天科技vs长电科技
  3. linux 中nohup命令的作用
  4. 你对贝叶斯统计都有怎样的理解?
  5. 2022杭电多校第八场题解
  6. [转载]Java Web 服务,第 1 部分: Java Web 服务在未来一年内的发展
  7. 快速傅里叶变换使用方法
  8. 加速度传感器灵敏度的几种表示方式
  9. 大数据HBase(四):HBase的相关操作-客户端命令式
  10. mysql:列类型之时间日期