本文将向读者介绍在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);复制代码
是不是很神奇。不过这个也无法解决对象循环引用的问题。并且它是异步的。