一、拷贝对象或将其作为函数参数传递时,所拷贝的是引用,而不是对象本身。
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);
// 第一个参数作为target
var 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; // false
result.key === target.key; // false
result.num === target.num; // false
result.bool === target.bool;// false
// 比较引用数据类型的属性值
result.arr === target.arr; // true
result.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 的初始值为1
target = arguments[i] || {};
// i 自增1
i++;
}
// 判断 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 本身赋值给 target
target = this;
// i 自减1,可能的值为 0 或 1
i--;
}
for (; i < length; i++) {
// 以下拷贝操作,只针对非 null 或 undefined 的 arguments[i] 进行
if ((options = arguments[i]) != null) {
// Extend the base object
for (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或纯array
if (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 object
return 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```javascript
var a = [[1], [2], [3]]
var b = [...a];
b.shift().shift(); // 1
console.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) {
// 相关操作
}