一、拷贝对象或将其作为函数参数传递时,所拷贝的是引用,而不是对象本身。
1、拷贝一个对象变量会又创建一个对相同对象的引用。
2、所有通过被拷贝的引用的操作(如添加、删除属性)都作用在同一个对象上。
二、这时需要创建“真正的拷贝”(一个克隆),包括浅拷贝/深拷贝。
拷贝的定义
浅拷贝
一、浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制。
- 也就是说只会赋值目标对象的第一层属性。 对于目标对象第一层为基本数据类型的数据,就是直接赋值,即「传值」;
而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址,即「传值」。
把对象重新开辟一个内存地址拷贝过来
二、一般来说,在JavaScript中考虑复合类型的深层复制的时候,往往就是指对于 Date 、Object 与 Array 这三个复合类型的处理。
js及库实现拷贝
js:手动实现一份浅拷贝加扩展的函数
一、实现代码
function _isPlainObject(target) {return (typeof target === 'object' && !!target && !Array.isArray(target));}function shallowExtend() {var args = Array.prototype.slice.call(arguments);// 第一个参数作为targetvar target = args[0];var src;target = _isPlainObject(target) ? target : {};for (var i=1; i<args.length; i++) {src = args[i];if (!_isPlainObject(src)) {continue;}for(var key in src) {if (src.hasOwnProperty(key)) {if (src[key] != undefined) {target[key] = src[key];}}}}return target;}
二、测试用例
// 初始化引用数据类型变量var target = {key: 'value',num: 1,bool: false,arr: [1, 2, 3],obj: {objKey: 'objValue'},};// 拷贝+扩展var result = shallowExtend({}, target, {key: 'valueChanged',num: 2,bool: true,});// 对原引用类型数据做修改target.arr.push(4);target.obj['objKey2'] = 'objValue2';// 比较基本数据类型的属性值result === target; // falseresult.key === target.key; // falseresult.num === target.num; // falseresult.bool === target.bool;// false// 比较引用数据类型的属性值result.arr === target.arr; // trueresult.obj === target.obj; // true
jQuery:jQuery.extend 实现深浅拷贝加扩展功能
一、贴下 jQuery@3.3.1 中 jQuery.extend 的实现:
jQuery.extend = jQuery.fn.extend = function() {var options,name,src,copy,copyIsArray,clone,target = arguments[0] || {},i = 1,length = arguments.length,deep = false;// 如果第一个参数是布尔值,则为判断是否深拷贝的标志变量if (typeof target === "boolean") {deep = target;// 跳过 deep 标志变量,留意上面 i 的初始值为1target = arguments[i] || {};// i 自增1i++;}// 判断 target 是否为 object / array / function 以外的类型变量if (typeof target !== "object" && !isFunction(target)) {// 如果是其它类型变量,则强制重新赋值为新的空对象target = {};}// 如果只传入1个参数;或者是传入2个参数,第一个参数为 deep 变量,第二个为 target// 所以 length 的值可能为 1 或 2,但无论是 1 或 2,下段 for 循环只会运行一次if (i === length) {// 将 jQuery 本身赋值给 targettarget = this;// i 自减1,可能的值为 0 或 1i--;}for (; i < length; i++) {// 以下拷贝操作,只针对非 null 或 undefined 的 arguments[i] 进行if ((options = arguments[i]) != null) {// Extend the base objectfor (name in options) {src = target[name];copy = options[name];// 避免死循环的情况if (target === copy) {continue;}// Recurse if we're merging plain objects or arrays// 如果是深拷贝,且copy值有效,且copy值为纯object或纯arrayif (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) {if (copyIsArray) {// 数组情况copyIsArray = false;clone = src && Array.isArray(src) ? src : [];} else {// 对象情况clone = src && jQuery.isPlainObject(src) ? src : {};}// 克隆copy对象到原对象并赋值回原属性,而不是重新赋值// 递归调用target[name] = jQuery.extend(deep, clone, copy);// Don't bring in undefined values} else if (copy !== undefined) {target[name] = copy;}}}}// Return the modified objectreturn target;};
1、该方法的作用是用一个或多个其他对象来扩展一个对象,返回被扩展的对象。
2、如果不指定target,则给jQuery命名空间本身进行扩展。这有助于插件作者为jQuery增加新方法。
3、如果第一个参数设置为true,则jQuery返回一个深层次的副本,递归地复制找到的任何对象;否则的话,副本会与原对象共享结构。 未定义的属性将不会被复制,然而从对象的原型继承的属性将会被复制。
拷贝的方法
浅拷贝 / 简单克隆的方法
一、简单克隆的2种方法:
1、创建新对象、遍历现有属性,复制新对象
2、Object.assign()
遍历 for…in
一、创建一个新对象,并通过遍历现有属性的结构,在原始类型值的层面,将其复制到新对象,以复制已有对象的结构。
【示例1】
let user = {name: "John",age: 30};let clone = {}; // 新的空对象// 将 user 中所有的属性拷贝到其中for (let key in user) {clone[key] = user[key];}// 现在 clone 是带有相同内容的完全独立的对象clone.name = "Pete"; // 改变了其中的数据alert( user.name ); // 原来的对象中的 name 属性依然是 John
es6 的Object.assign()方法
一、Object.assign 方法可以把 任意多个的源对象所拥有的自身可枚举属性 拷贝给目标对象,然后返回目标对象。
1、对于访问器属性,该方法会执行那个访问器属性的 getter 函数,然后把得到的值拷贝给目标对象,如果你想拷贝访问器属性本身,请使用 Object.getOwnPropertyDescriptor() 和 Object.defineProperties() 方法;
2、字符串类型和 symbol 类型的属性都会被拷贝;
3、在属性拷贝过程中可能会产生异常,比如目标对象的某个只读属性和源对象的某个属性同名,这时该方法会抛出一个 TypeError 异常,拷贝过程中断,已经拷贝成功的属性不会受到影响,还未拷贝的属性将不会再被拷贝;
4、该方法会跳过那些值为 null 或 undefined 的源对象;
二、语法是:
Object.assign(dest, [src1, src2, src3...])
- 第一个参数dest是指目标对象。
- 更后面的参数src1, …, srcN(可按需传递多个参数)是源对象。
- 该方法将所有源对象的属性拷贝到目标对象dest中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。
- 调用结果返回dest。
【示例1】我们可以用它来合并多个对象:
let user = { name: "John" };let permissions1 = { canView: true };let permissions2 = { canEdit: true };// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中Object.assign(user, permissions1, permissions2);// 现在 user = { name: "John", canView: true, canEdit: true }
1、如果被拷贝的属性的属性名已经存在,那么它会被覆盖:
let user = { name: "John" };Object.assign(user, { name: "Pete" });alert(user.name); // 现在 user = { name: "Pete" }
2、我们也可以用Object.assign代替for..in循环来进行简单克隆:
let user = {name: "John",age: 30};let clone = Object.assign({}, user);
(1)它将user中的所有属性拷贝到了一个空对象中,并返回这个新的对象。
三、20210511 aSuncat:有说法是说“当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝”,但我觉得,有能力进行递归复制的这个方法才叫深拷贝,即能实现二级属性的拷贝才叫深拷贝,所以Object.assign()是浅拷贝。
对象展开运算符…(ES2018)
一、将对象的所有可枚举属性拷贝到新构造的对象中,类似于Object.assign()
| 【示例】```javascript var obj1 = { foo: { a: 1, b: 2, }, x: 42 };
var clonedObj = { …obj1 };
clonedObj.foo.a = ‘testt’ console.log(obj1); / { foo: { a: ‘testt’, b: 2, }, x: 42 } /
|| --- |<a name="zkJvV"></a>### 数组展开运算符...(ES6)一、展开语法和Object.assign()行为一致,执行的都是浅拷贝(即只遍历一层)| 【示例】a是多层数组,b只拷贝了第一层,对于第二层依旧和a持有同一个地址,所以对b的修改会影响到a```javascriptvar a = [[1], [2], [3]]var b = [...a];b.shift().shift(); // 1console.log(a)// [[], [2], [3]];
| | —- |
拷贝访问器属性本身:Object.defineProperties,Object.getOwnPropertyDescriptors
见属性标志和属性描述符#克隆对象https://www.yuque.com/tqpuuk/yrrefz/xg43a2
拷贝对象及原型链上的所有属性:Object.create
见Object方法#Object.create-克隆对象:https://www.yuque.com/tqpuuk/yrrefz/tx2i78#lZLUs
深拷贝 / 深层克隆
一、到现在为止,我们都假设user的所有属性均为原始类型。但属性可以是对其他对象的引用。那应该怎样处理它们呢?
【示例1】
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
1、现在这样拷贝clone.sizes = user.sizes已经不足够了,因为user.sizes是个对象,它会以引用形式被拷贝。因此clone和user会共用一个 sizes:
【示例1】就像这样:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = Object.assign({}, user);
alert( user.sizes === clone.sizes ); // true,同一个对象
// user 和 clone 分享同一个 sizes
user.sizes.width++; // 通过其中一个改变属性值
alert(clone.sizes.width); // 51,能从另外一个看到变更的结果
二、为了解决此问题,我们应该使用会检查每个user[key]的值的克隆循环,如果值是一个对象,那么也要复制它的结构。这就叫“深拷贝”。
遍历 for…in
一、我们能想到的最常用的方法就是先创建一个空的新对象,然后递归遍历旧对象,直到发现基础类型的子节点才赋予到新对象对应的位置。
1、不过这种方法会存在一个问题,就是 JavaScript 中存在着神奇的原型机制,并且这个原型会在遍历的时候出现,然后需要考虑原型应不应该被赋予给新对象。那么在遍历的过程中,我们可以考虑使用 hasOwnProperty 方法来判断是否过滤掉那些继承自原型链上的属性。
二、实现代码
function cloneDeep(arr){
var obj=arr.constructor == Array ? [] : {};
//第二种方法 var obj=arr instanceof Array?[]:{}
// 第三种方法 var obj = Array.isArray(source) ? [] : {}
for(var item in arr){
if(typeof arr[item]==="object"){
obj[item]=cloneDeep(arr[item]);
}else{
obj[item]=arr[item];
}
}
return obj;
}
1、用例
const obj1 = {
name: 'John',
sizes: {
width: 100,
height: 200,
}
}
const obj2 = cloneDeep(obj1);
console.log(obj2);
jQuery的extend方法
let $ = require('jquery');
let obj1 = {
a: 1,
b: {
f: {
g: 1
}
},
c: [1, 2, 3]
};
let obj2 = $.extend(true, {}, obj1);
JSON.stringify和JSON.parse
一、可以利用 JSON 进行忽略原型链的深拷贝
1、该方法会忽略掉值为 undefined 的属性以及函数表达式,但不会忽略值为 null 的属性。
var dest = JSON.parse(JSON.stringify(target));
lodash.cloneDeep()
let _ = require('lodash');
let obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
let obj2 = _.cloneDeep(obj1);
const obj1 = {
name: 'johe',
sizes: {
width: 100,
height: 200,
}
}
const obj2 = cloneDeep(obj1);
https://www.jsdelivr.com/package/npm/lodash
| 【示例】_.cloneDeep()源码分析 |
|---|
规避原型链属性上的拷贝
obj.hasOwnProperty(key)
一、最常用的方式:
for (let key in targetObj) {
if (targetObj.hasOwnProperty(key)) {
// 相关操作
}
}
Object.keys(obj)
二、只会返回参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名所组成的数组。
const keys = Object.keys(targetObj);
keys.map((key)=>{
// 相关操作
});
Object.create
const obj = Object.create(null);
target.__proto__ = Object.create(null);
for (let key in target) {
// 相关操作
}
