本文将向读者介绍在js中数据的拷贝,旨在让读者能在工作中或者面试中遇到相似的问题能够应用起来
首先声明一下,这批文章仅代表我自己对js的一些理解。由于笔者水平有限,如果有出错之出,还望多多谅解,指正。谢谢。当然如果你看的不爽...那么---------你顺着网线过来打我呀O(∩_∩)O哈哈~
正文开始------------------
数据类型
说到js的数据拷贝就不得不提js的数据类型。我们知道的7中数据类型:
Number、String、Boolean、Undefined、Null、Object 和es6新增的Symbol
(null undefined number boolean )都是基本类型,存栈里的数据
Null: 不存在的,没有的数据对象
Undefined: 变量声明了却没有初始化的数据。表示"缺少值",就是此处应该有一个值,但是还没有定义。
number: 顾名思义就是一个数字类型,number是存放在栈里的数据
string:string比较特殊,它其实是存在堆里的,我们拿到的只是一个地址引用。如果对js比较了解的话,那么会知道
js中string是不可变的,我们没有任何一个方法可以改变一个字符串。可以认为string是
行为与基本类型相似的不可变引用类型
Object 是引用类型,存堆里的地址
Objecty下面又有三员大将,它们都是 Object.prototype 下的熟悉
Array: 一个存放数据的集合,js中 数组可以存放任何数据。
Function: 函数,其实也是数据。我们可以把一个函数赋值给另一个变量
object: 对象,没啥好说的。
Symbol的话是一个es6新增对象,用Symbol可以创建唯一的变量名。
这个array有点意思,在有的语言数组就是数组,但是js里它确实对象,数组的key就是它的下标
// instanceof 是判断xxx 是否xxx的实例
Object instanceof Object // true
Array instanceof Object // true
Function instanceof Object // true
接下来重点来了,请大家看一段代码:
Number.constructor // ƒ Function() { [native code] }
String.constructor // ƒ Function() { [native code] }
Boolean.constructor // ƒ Function() { [native code] }
Object.constructor // ƒ Function() { [native code] }
Symbol.constructor //ƒ Function() { [native code] }
大家看到这个有陷入深深的沉思吗?为啥他们都有 constructor,为啥他们都有方法可以去调用。而且如果用 刚刚这个 instanceof 去判断会发现,string、boolean、number...都是object 的实例。
那么你知道是为什么吗?
你没猜错。在js中所以数据都是对象。all in of object。这个英文怕不怕。
接下来再看下面的代码:
Number.prototype.six = () => { console.log(666) }
const num = 123
num.six() // 666
这里能给number原型添加一个方法,并且定义的一个number 可以去掉用这个方法。已经说明了js中number 其实是基于对象。
string、Boolean 等都是如此
顺便解释一下js堆和栈的一点概念
栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。
拷贝
js的拷贝是有点复杂的,涉及到引用类型的话
let a = 1
let b = '2'
a = b
a = 3
console.log(a, b) // 3 2
这是js中基本类型的复制,没有任何问题,我们就不多扯了。就是覆盖就好。
我们来看看js中引用类型的拷贝
let obj = {
a: 1,
b: {
b: 1
}
}
let arr = [1, 2, 3]
let arr2 = arr
let obj2 = obj
arr[1] = '666'
obj.a = '777'
console.log(obj.a) // 666
console.log(obj2.a) // 666
console.log(arr[1]) // 777
console.log(arr2[1]) // 777
这就是引用类型的尿性,有的时候很烦,有的时候却很有用。因为js中引用类型给你访问的是一个地址。这个地址对应着数据的位置,我们像复制普通类型一样复制引用类型,就等于直接把地址给了别人。改动的时候大家改的其实是同一个数据。
那么问题来了,我就是复制一个引用类型的所以数据,但是我不想被我复制的对象收到影响怎么办。
浅拷贝
let obj = {
a: 1,
b: {
b: 1
}
}
let arr = [1, 2, 3]
let obj2 = { ...obj }
let arr2 = [ ...arr ]
obj2.a = '666'
arr2[1] = '777'
console.log(obj.a) // 1
console.log(obj2.a) // 666
console.log(arr[1]) // 1
console.log(arr2[1]) // 777
好像没问题了,似乎很简单的样子啊。
我们接着上面的代码
...
obj2.b.b = '888'
console.log(obj.b.b) // { b: '888' }
console.log(obj2.b.b) // { b: '888' }
震惊!似乎又出现了刚刚的问题。没错这就是因为 ...是es6的扩展运算符。他只是拷贝了第一层变量。后面的依然还是直接复制地址。
深拷贝
先来一个简单的,百度一搜索一大堆的看看
function clone(params) {
var obj = {};
for(var i in params) {
if (params.hasOwnProperty(i)) {
if (typeof params[i] === 'object') {
obj[i] = clone(params[i]); // 通过判断是否对象而进行递归
} else {
obj[i] = params[i];
}
}
}
return obj;
}
let obj = {
a: 1,
b: {
b: 1
}
}
let obj2 = clone(obj)
obj2.b.b = '888'
console.log(obj.b.b, obj2.b.b) // 1 888
这就实现了一个简单的深拷贝,但是它有一些问题。主要就是考虑的不够严谨,比如一些数据没有做到兼容。比如set、 map、weakset、weakmap、array... 是不是感觉很麻烦。当然其实我们有个简单的方法。
function clone2(params) {
return JSON.parse(JSON.stringify(params));
}
平时我一般工作中拷贝一些json类型的数据就用这个....简单粗暴。
这个方法其实也有一些问题。就是没法克隆 函数和正则匹配等.当然如果只是简单的数据还是可以的。
这个方法是我在前端早读课中的一篇文章看到的一个方法。这里就厚颜无耻的clone了下来,如果大家有兴趣的话可以去关注前端早读课。看那篇关于js对象拷贝的文章。
function cloneForce(x) {
// =============
const uniqueList = []; // 用来去重
// =============
let root = {};
// 循环数组
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while(loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const data = node.data;
// 当第一次循环的时候key是undefined,所以靠谱到第一级,后面的都是拷贝到子级
let res = parent;
if (typeof key !== 'undefined') {
res = parent[key] = {};
}
// =============
let uniqueData = find(uniqueList, data);
if (uniqueData) {
parent[key] = uniqueData.target;
continue; // 判断数据是否存在,如果存在就不继续这次循环了
}
// 数据不存在
// 保存源数据,在拷贝数据中对应的引用
uniqueList.push({
source: data,
target: res,
});
// =============
for(let k in data) {
if (data.hasOwnProperty(k)) {
if (typeof data[k] === 'object') {
// 如果有object 就push禁 loopList,进行下一次转换
loopList.push({
parent: res,
key: k,
data: data[k],
});
} else {
res[k] = data[k];
}
}
}
}
return root;
}
function find(arr, item) {
for(let i = 0; i < arr.length; i++) {
if (arr[i].source === item) {
return arr[i];
}
}
return null;
}
最后在向大家介绍一种神奇的方法。
let obj = {a:1, b: {b:1}}
var channel = new MessageChannel();
var port1 = channel.port1;
var port2 = channel.port2;
port1.onmessage = function(event) {
let obj2 = event.data
obj2.b.b = '666'
console.log(obj.b.b,obj2.b.b) // 1 666
}
port2.onmessage = function(event) {
console.log("port2收到来自port1的数据:" + event.data);
}
port2.postMessage(obj);
是不是很神奇。不过这个也无法解决对象循环引用的问题。并且它是异步的。