javascript系列--Object.assign实现浅拷贝的原理以及实现
一、前言
之前在前面一篇学习了赋值,浅拷贝和深拷贝。介绍了这三者的相关知识和区别。
传送门:https://www.mwcxs.top/page/592.html
本文会介绍浅拷贝Object.assign()的实现原理,然后咱们试着实现一个浅拷贝。
二、浅拷贝Object.assign()
什么是浅拷贝?浅拷贝就是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。
浅拷贝Object.assign()是什么?主要将所有可枚举属性的值从一个或者多个数据源对象复制到目标对象,同时返回目标对象。
语法规则:
Object.assign(target,...sources)
其中target是目标对象,source是源对象,可以是多个,修改返回的是目标对象target。
1、如果目标对象中的属性具有相同的属性键,则属性将被源对象中的属性覆盖;
2、源对象的属相将类似覆盖早先的属性。
强调两点:
1、可枚举的属性(自有属性)
2、string或者symbol类型是可以被直接分配的
2.1栗子1
浅拷贝就是拷贝第一层的基本类型值,以及第一层的引用类型地址。
// saucxs
// 第一步
let a = {
name: "advanced",
age: 18
}
let b = {
name: "saucxs",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let c = Object.assign(a, b);
console.log(c);
// {
// name: "saucxs",
// 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: "saucxs",
// age: 18,
// book: {title: "You Don't Know JS", price: "55"}
// }
分析:
1、第一步中,使用Object.assign把源对象b的值复制到目标对象a中,这里把返回值定义为对象c,可以看出b会替换掉a中具有相同键的值,即如果目标对象a中的属性具有相同的键,则属相将被源对象b中的属性覆盖。返回的对象c就是目标对象a。
2、第二步中,修改源对象b的基本类型值(name)和引用类型值(book)。
3、第三步中,浅拷贝之后目标对象a的基本类型值没有改变,但是引用类型值发生了改变,因为Object.assign()拷贝的是属性值。加入源对象的属性值是一个指向对象的引用,只拷贝那个引用地址。
2.2栗子2
string类型和symbol类型的属性都会被拷贝,而且不会跳过那些值为null或undefined的源对象。
// saucxs
// 第一步
let a = {
name: "saucxs",
age: 18
}
let b = {
b1: Symbol("saucxs"),
b2: null,
b3: undefined
}
let c = Object.assign(a, b);
console.log(c);
// {
// name: "saucxs",
// age: 18,
// b1: Symbol(saucxs),
// b2: null,
// b3: undefined
// }
console.log(a === c);
// true
三、Object.assign模拟实现
实现Object.assign模拟实现大致思路:
1、判断原生的Object是否支持assign这个函数,如果不存在的话就会创建一个assign函数,并使用Object.defineProperty将函数绑定到Object上。
2、判断参数是否正确(目标参数不能为空,可以直接设置{}传递进去,但是必须有值)。
3、使用Object()转成对象,并保存为to,最后返回这个对象to。
4、使用for in 循环遍历出所有的可枚举的自有属性,并复制给新的目标对象(使用hasOwnProperty获取自有属性,即非原型链上的属性)
参考原生,实现代码如下,使用assign2代替assign。此处的模拟不支持symbol属性,因为es5中没有symbol。
// saucxs
if (typeof Object.assign2 != 'function') {
// 注意 1
Object.defineProperty(Object, "assign2", {
value: function (target) {
'use strict';
if (target == null) { // 注意 2
throw new TypeError('Cannot convert undefined or null to object');
}
// 注意 3
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) { // 注意 2
// 注意 4
for (var nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
测试一下:
// saucxs
// 测试用例
let a = {
name: "advanced",
age: 18
}
let b = {
name: "saucxs",
book: {
title: "You Don't Know JS",
price: "45"
}
}
let c = Object.assign2(a, b);
console.log(c);
// {
// name: "saucxs",
// age: 18,
// book: {title: "You Don't Know JS", price: "45"}
// }
console.log(a === c);
// true
3.1 注意1:可枚举性
原生情况下挂载在Object上的属性时不可枚举的,但是直接在Object上挂载属性a之后就可以枚举的,所以必须使用Object.defineProperty,并设置`enumerable: false` 以及 `writable: true`,`configurable: true`。
// saucxs
for(var i in Object) {
console.log(Object[i]);
}
// 无输出
Object.keys( Object );
// []
上面说明,原生的Object上的属性不可枚举。
我们可以使用2种方法查看Object.assign是否可枚举,使用Object.getOwnPropertyDescriptor或者Object.propertyIsEnumberable都可以,其中propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true。具体用法如下:
// saucxs
Object.getOwnPropertyDescriptor(Object, "assign");
// {
// value: ƒ,
// writable: true, // 可写
// enumerable: false, // 不可枚举,注意这里是 false
// configurable: true // 可配置
// }
// saucxs
Object.propertyIsEnumerable("assign");
// false
说明Object.assign是不可枚举的。
直接在Object上挂载属性a之后是可以枚举的。我们来看一下代码:
// saucxs
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`。
// saucxs
Object.defineProperty(Object, "b", {
value: function() {
console.log("log b");
}
});
Object.getOwnPropertyDescriptor(Object, "b");
// {
// value: ƒ,
// writable: false, // 注意这里是 false
// enumerable: false, // 注意这里是 false
// configurable: false // 注意这里是 false
// }
模拟实现涉及到代码
// saucxs
// 判断原生 Object 中是否存在函数 assign2
if (typeof Object.assign2 != 'function') {
// 使用属性描述符定义新属性 assign2
Object.defineProperty(Object, "assign2", {
value: function (target) {
...
},
// 默认值是 false,即 enumerable: false
writable: true,
configurable: true
});
}
3.2 注意2:判断参数是否正确
有些文章判断参数是否正确是这样的。
// saucxs
if (target === undefined || target === null) {
throw new TypeError('Cannot convert undefined or null to object');
}
这样肯定没问题,但是这样写没有必要,因为 `undefined` 和 `null` 是相等的(高程 3 P52 ),即 `undefined == null` 返回 `true`,只需要按照如下方式判断就好了。
// saucxs
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
3.3 注意3:原始类型被包装为对象
// saucxs
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");
var obj = Object.assign({}, v1, null, v2, undefined, v3, v4);
// 原始类型会被包装,null 和 undefined 会被忽略。
// 注意,只有字符串的包装对象才可能有自身可枚举属性。
console.log(obj);
// { "0": "a", "1": "b", "2": "c" }
上面代码中的源对象 v2、v3、v4 实际上被忽略了,原因在于他们自身**没有可枚举属性**。
// saucxs
var v1 = "abc";
var v2 = true;
var v3 = 10;
var v4 = Symbol("foo");
var v5 = null;
// 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.keys(..)返回一个数组,包含所有可枚举的属性,只会查找对象直接包含的属性,而不会查找[[prototype]]链。
// 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
上面代码说明:Object.getOwnPropertyNames(..)返回一个数组,保护焊所有属性,无论他们是否可以枚举,只会查找对象直接包含的属性,不查找[[prototype]]链。
但是这样是可以执行的:
// saucxs
var a = "abc";
var b = {
v1: "def",
v2: true,
v3: 10,
v4: Symbol("foo"),
v5: null,
v6: undefined
}
var obj = Object.assign(a, b);
console.log(obj);
// {
// [String: 'abc']
// v1: 'def',
// v2: true,
// v3: 10,
// v4: Symbol(foo),
// v5: null,
// v6: undefined
// }
为什么?因为undefined,true等不适作为对象,而是作为对象b的属性值,对象b是可枚举的。
// saucxs
// 接上面的代码
Object.keys( b ); // [ 'v1', 'v2', 'v3', 'v4', 'v5', 'v6' ]
这里其实又可以看出一个问题来,那就是目标对象是原始类型,会包装成对象,对应上面的代码就是目标对象 a 会被包装成 `[String: 'abc']`,那模拟实现时应该如何处理呢?很简单,使用 `Object(..)` 就可以了。
// saucxs
var a = "abc";
console.log( Object(a) );
// {0: 'a', 1: 'b', 2: 'c'}
我们再来看看下面代码能不能执行:
// saucxs
var a = "abc";
var b = "def";
Object.assign(a, b); // TypeError: Cannot assign to read only property '0' of object '[object String]'
还是会报错的,原因在于:Object('abc')时候,其属性描述符writable为不可写,即writeable: false。
// saucxs
var myObject = Object( "abc" );
Object.getOwnPropertyNames( myObject );
// [ '0', '1', '2', 'length' ]
Object.getOwnPropertyDescriptor(myObject, "0");
// {
// value: 'a',
// writable: false, // 注意这里
// enumerable: true,
// configurable: false
// }
3.4 注意4:存在性
如何在不访问属性值的情况下判断对象中是否存在某个属性,看下面代码:
// saucxs
var anotherObject = {
a: 1
};
// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );
myObject.b = 2;
("a" in myObject); // true
("b" in myObject); // true
myObject.hasOwnProperty( "a" ); // false
myObject.hasOwnProperty( "b" ); // true
使用in和hasOwnProperty方法,区别如下:
1、in 操作符会检查属性是否在对象及其[[propertype]]原型链中;
2、hasOwnProperty(..)只会检查是否在myObject对象中,不会检查[[prototype]]原型链中。
Object.assign方法肯定是不会拷贝原型链上的属性,所以模拟实现时需要用hasOwnProperty(..)判断处理下,但是直接使用myObject.hasOwnProperty(..)是有问题的,因为有的对象可能没有连接到Object.prototype上(通过Object.create(null)来创建),这种情况下,使用myObject.hasOwnProperty(..)就会失败。
// saucxs
var myObject = Object.create( null );
myObject.b = 2;
("b" in myObject);
// true
myObject.hasOwnProperty( "b" );
// TypeError: myObject.hasOwnProperty is not a function
解决办法,使用call就可以了,如下:
// saucxs
var myObject = Object.create( null );
myObject.b = 2;
Object.prototype.hasOwnProperty.call(myObject, "b");
// true
所以具体到本次模拟实现中,相关代码如下。
// saucxs
// 使用 for..in 遍历对象 nextSource 获取属性值
// 此处会同时检查其原型链上的属性
for (var nextKey in nextSource) {
// 使用 hasOwnProperty 判断对象 nextSource 中是否存在属性 nextKey
// 过滤其原型链上的属性
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
// 赋值给对象 to,并在遍历结束后返回对象 to
to[nextKey] = nextSource[nextKey];
}
}
四、参考
javascript系列--Object.assign实现浅拷贝的原理以及实现的更多相关文章
- JavaScript中Object.prototype.toString方法的原理
在JavaScript中,想要判断某个对象值属于哪种内置类型,最靠谱的做法就是通过Object.prototype.toString方法. ? 1 2 var arr = []; console.lo ...
- [转]javascript之Object.assign()痛点
本文转自:http://blog.csdn.net/waiterwaiter/article/details/50267787 最近也一直会用javascript,然后中间使用的一些组件,如Echar ...
- Object.assign()是浅拷贝
浅拷贝: 复制的值指向同一个内存地址 深拷贝:复制的值指向新的内存地址 var a = { xm: { name: 'xiaoming' } } var b = Object.assign({}, a ...
- JavaScript系列-----Object之toString()和valueOf()方法 (2)
深入理解toString()和valueOf()函数 1.我们为什么要了解这两种方法 众所周知,toString()函数和valueOf函数,这两个函数是Object类的对象生来就拥有的,而且他们还可 ...
- JavaScript之Object拆解
转载烦请注明原文链接: https://github.com/Xing-Chuan/blog/blob/master/JavaScript/JavaScript%E4%B9%8BObject%E6%8 ...
- JavaScript系列-----对象基于哈希存储(<Key,Value>之Value篇) (3)
JavaScript系列-----Objectj基于哈希存储<Key,Value>之Value 1.问题提出 在JavaScript系列-----Object之基于Hash<Key, ...
- js中或者vue中 Object.assign()用法详解
Object.assign()是浅拷贝. 合并对象 var o1 = { a: 1 }; var o2 = { b: 2 }; var o3 = { c: 3 }; var obj = Object. ...
- ES6的Object.assign()基本用法
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target). 例如: const target = {a:1}, const source1 ...
- 【JS】307- 复习 Object.assign 原理及其实现
点击上方"前端自习课"关注,学习起来~ }let b = { name: "muyiy", book: { title: " ...
随机推荐
- HBase相关概念
1.Row Key 基本原则是:(1).由于读取数据只能依靠RowKey,所以应把经常使用到的字段作为行键{如手机号+时间戳拼接的字符串} (2).RowKey长度越短越好,最好不要超过16个字节.从 ...
- Firefox管理已经保存的账号和密码
https://support.mozilla.org/en-US/kb/password-manager-remember-delete-change-and-import You can easi ...
- luogu P1029 最大公约数和最小公倍数问题
https://www.luogu.org/problem/show?pid=1029 输入二个正整数x0,y0(2<=x0<100000,2<=y0<=1000000),求出 ...
- [3dSmartSystem] - Java&3d
Java如果简单的做为Dynamic web project的工具来说,实现数据的前端到后端的传递及存储等. 就像一个银行,你去柜台(前端)给服务人员或者机器一些钱,之后输入密码,然后服务人员(后端处 ...
- sudo环境变量问题;程序库函数寻找
1. sudo 和 root不完全等效,继承的环境变量不一样,最主要的区别还是输入的密码不同. 2. 使用sudo去执行一个程序时,出于安全的考虑,这个程序将在一个新的.最小化的环境中执行,也就是说, ...
- 过滤器系列(一)—— Bloom filter
因为要做过滤器相关内容,最近读了一些过滤器方面的文章,准备从中提取主要思想写几篇博客. 作为这系列的第一篇文章,首先得讲一下过滤器是干什么用的.从历史发展来看,过滤器最早出现是作为散列表的替代品,那么 ...
- 【git】提交到github不显示贡献小绿点问题的解决
问题描述: 最近一直在用github来写博客,但是今天发现github上的contributions记录并没有我的提交记录. 经过一番百度和自行捣鼓发现了问题所在. 原因: 最近实习,公司给配电脑.原 ...
- 让黑白的SecureCRT彩色起来
让黑白的SecureCRT彩色起来,如图仿真设置如下:
- 20155236 2016-2017-2 《Java程序设计》第六周学习总结
20155236 2016-2017-2 <Java程序设计>第六周学习总结 教材学习内容总结 InputStream与OutputStream 从应用程序角度来看,如果要将数据从来源取出 ...
- 使用GitHub进行项目创建——初级,非指令版,纯软件操作
主要步骤如下: 1.申请一个GitHub账号,官网按照步骤来就行 2.下载一个GitHub DeskTop(https://desktop.github.com/),命令什么的以后说不定会写把 3.创 ...