js 中的深拷贝,Dom 元素的拷贝

js 在原生中并不具备深拷贝的功能,甚至不具备拷贝功能,大多是使用其他函数模拟效果,所以这里进行深入探讨。

现状

js 中有

简单类型:1. 字符串 2. 数值 3. 布尔值 4. null 5. undefined

引用类型:对象(object)

同时,引用类型又分为几个特殊型:1. Object 2. Array 3. RegExp 4. Date 5. Function
以及单体内置对象:1. Global 2. Math

简单类型的拷贝很简单,因为数据储存在栈内存,拷贝实际就是内存的复制,并用新变量承载;

1
2
3
4
5
6
7
8
9
10
11
var s1 = 'string';
var s2 = s1;
s2; // 'string'

var n1 = 1;
var n2 = n1;
n2; // 1

var b1 = false;
var b2 = b1;
b2; // false;

引用类型则要复杂一些,因为 js 的内存分为 栈内存堆内存 ,引用类型实际内容是储存在堆内存中,声明一个新变量使用等号仅仅是指向了同一片堆内存,所以会发生:

1
2
3
4
5
var o1 = {a: 1}
var o2 = o1;
o1.a = 2; // 改变对象的值
// o2 数值也变了
o2; // {a: 2}

看上去,改变 o1 的值,o2 的值也跟着变,但实际上,o1 和 o2 都指向同一片堆内存,就和我叫我的弟弟 傻康,但我妈叫他 亲宝贝,实际两个名字代表的是同一个人。

那要如何按预想中复制变量呢?

深拷贝

事实上引用类型的拷贝有深浅两种,浅拷贝指的是只拷贝引用类型的第一层,也就是,如果引用类型的内部变量指向了另一个引用类型,那内部变量指向的引用没有被拷贝,就称为浅拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 数组
var arr1 = [1,2,{a: 1}];
var arr2 = arr1.slice(0, arr1.length); // 切片切整个,并原数组不变

// var arr2 = arr1.concat(); // 数组同样可以使用 concat 方法,数组也不变

// 数组浅拷贝另一种方法就是 es6 中的 扩展运算符(...)
var arr2 = [...arr1];

arr2; // [1,2,{a: 1}]
arr1[2].a = 2;
arr1; // [1,2,{a: 2}]
arr2; // [1,2,{a: 2}] // 数组内部引用类型跟着改了 ,所以是浅拷贝
  1. 使用 slice 方法切片切整个达到复制目的,并原数组不变;

  2. 数组同样可以使用 concat 方法,concat 方法连接接数组,不传参数时,就只返回自己,原数组也不变,达到复制目的

1
2
3
4
5
6
// 对象
var obj1 = {a: 1, b: 2}
var obj2 = Object.assign(obj1);
obj2; // {a: 1, b: 2}
obj1.a = 3;
obj2; // {a: 3, b: 2}
  1. 显然对象使用 Object.assign 方法不能开辟新内存;

  2. 想要无脑浅拷贝,可以使用 Json 对象的转换方法

1
2
3
4
5
6
var obj1 = {a: 1, b: 2}
var obj2 = JSON.parse(JSON.stringify(obj1))
obj2; // {a: 1, b: 2}
obj1.a = 3;
obj1; // {a: 3, b: 2}
obj2; // {a: 1, b: 2}

引用类型的深拷贝

那想要内部的引用类型也拷贝怎么办,递归遍历他

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var a = [1,2,{c: 1}]

var b = deepCopy(a);

console.log(a); // [1,2,{c: 1}]
console.log(b); // [1,2,{c: 1}]
a[2].c = 2;
console.log(a); // [1,2,{c: 2}]
console.log(b); // [1,2,{c: 1}]

function deepCopy (obj) {
var newObj;
if (typeof obj === 'object') {
if (Object.prototype.toString.call(obj) === '[object Object]') {
newObj = {}
} else if (Object.prototype.toString.call(obj) === '[object Array]'){
newObj = []
}

for(var i in obj) {
if (obj.hasOwnProperty(key)) {
if (typeof obj === 'object') {
newObj[i] = deepCopy(obj[i]);
}
else {
newObj[i] = obj[i];
}
}
}
} else {
newObj = obj;
}
return newObj;
}

显然就可以了。

另外在 jquery 中,框架已经帮我们实现了相关函数:extend

1
2
3
4
5
var a = [1,2,{c: 1}]
var b = $.extend(true, {}, a); // 第一参数就是是否开启深拷贝
a[2].c = 3;
console.log(a); // [1,2,{c: 2}]
console.log(b); // [1,2,{c: 1}]

dom 元素的拷贝

dom 元素的复制比想象中还要简单,早在 DOM 2.0 就加入了 cloneNode 方法,第一个参数设置为 true,就可以拷贝元素的子元素。

1
2
3
4
5
var _body = document.body.cloneNode();
_body; // <body></body>

var _body = document.body.cloneNode(true);
_body; // <body>...</body>

但并没有复制元素“已经绑定的事件”,万能的 jq 同样封装了 clone 方法,第一个参数传入 布尔值,就可以复制事件了。

1
2
3
4
5
var _body = $(body).clone();
_body[0]; // _body; // <body>...</body>

var _body = $(body).clone(true);
_body.onclick(); // 执行点击事件