现象 今天工作时碰到一个需求,有两个数组arrayChild, arrayFather, 要求: 1、往数组arrayChild中放入一个元素; 2、将当前的数组arrayChild放入arrayFather中; 3、清空数组arrayChild,将一个新元素放进去; 4、将放了新元素的arrayChild放入数组arrayFather中。 刚开始是这么写的:
1 2 3 4 5 6 7 8 9 10 11 12 const arrayChild = [];const arrayFather = [];arrayChild.push (0 , 1 ); arrayFather.push (arrayChild); arrayChild.splice (0 ); arrayChild.push (3 , 4 ); arrayFather.push (arrayChild); console .log (`arrayFather = ${arrayFather} ` );
1 2 3 4 预想结果是: arrayFather = [[0 , 1 ], [3 , 4 ]]; 实际结果: arrayFather = [[3 , 4 ], [3 , 4 ]];
为什么呢?向公司老司机请教,才知道原来创建一个数组时,会在内存中开辟一块堆内存A,我的arrayChild是在另一块栈内存中存入了指向 堆内存A的地址,所以使用const声明的数组,还可以继续向数组内添加东西。在第一步,arrayFather.push(arrayChild),也是将arrayFather指向了arrayChild指向的堆内存A,然后splice 是清除arrayChild中的数据,就是将堆内存A中的数据全部清除,所以这时arrayFather和arrayChild都是空的。这时再往arrayChild中添加新数据,那么arrayFather = arrayChild = [3, 4], 然后arrayFather又push了一次arrayChild,所以最后arrayFather = [[3, 4], [3, 4]]
那想要实现需求怎么办呢?可以用这种方法:
1 2 3 4 5 6 7 8 9 10 11 12 let arrayChild = [];const arrayFather = [];arrayChild.push (0 , 1 ); arrayFather.push (arrayChild); arrayChild = []; arrayChild.push (3 , 4 ); arrayFather.push (arrayChild); console .log (`arrayFather = ${arrayFather} ` );
1 2 3 4 预想结果是: arrayFather = [[0 , 1 ], [3 , 4 ]]; 实际结果: arrayFather = [[0 , 1 ], [3 , 4 ]];
这里的arrayChild = []就是重新开辟一片内存了,所以原来的值还会存在,相当于: 1、首先分配了一块内存(数组的值存放在堆中,索引存放在栈中),存了个数组[0, 1],索引是arrayChild 2、将arrayFather(前两个地址指针)指向这块堆内存 3、另外分配一块新内存,存了数组[3, 4],把索引arrayChild重新指向这里 4、将新内存的地址存入arrayFather(的arrayFather[2]和arrayFather[3])中,因为原先的arrayChild的值还在被arrayFather引用,所以这块内存不会被回收,所以最终的目的达成。 综上所述,问题的根源在于对数组的本质 不了解。新建数组,就是新分配一块堆内存存放数组的值。堆内存的地址存放在一块栈内存中,组成数组的索引 。
本质 回顾过去,总能发现一些需要提高的部分。这个问题其实是JS浅拷贝(shallow copy)与深拷贝(deep copy)的问题。 深拷贝与浅拷贝只针对引用类型,它们有类似于C语言的指针机制,就是引用型。深拷贝会新创建一个新的引用类型,浅拷贝只是新建指针,两个指针指向的内容还是同一个。
浅拷贝 浅拷贝的目标如果只是基本数据类型,那么会直接复制一份;如果是引用类型,浅拷贝只是将引用类型的引用指针复制了一层,两个指向的还是同一块堆内存,同一个内容。 不管是什么类型,都是重新创建了一块内存,但是其中的引用类型的数据的引用指向的还是同一块堆内存。 浅拷贝与赋值的区别:如果是引用型数据,赋值只是将引用(指针)复制了一次。而浅拷贝是重新开辟了一块地址,将第一层数据复制了一层,内部的引用型则只是复制了其引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const obj1 = {a : 1 , b : 3 , c : 5 };const obj2 = obj1;obj2.b = 10 ; console .log ( obj1 === obj2 )console .log (obj1); const o1 = [1 ,2 ,3 ];const o2 = o1;console .log (o1 === o2); o2.push (4 ); console .log (o1);
数组的Array.prototype.slice()和对象的Object.assign()都是浅拷贝
1 2 3 4 5 6 7 8 9 10 11 let obj = [1 ,2 , 3 , [2 , 3 ,4 ]]let animals = ['ant' , 'bison' , 'camel' ,obj, 'duck' , 'elephant' ];let copyArr = animals.slice (2 );animals[3 ][0 ] =65 ; console .log (animals);console .log (copyArr);
copyArr拷贝的animals里的obj对象跟原数组里的都指向同一个obj,所以复制以后再改变obj,copyArr也跟着改变
1 2 3 4 5 6 7 8 9 10 11 let obj1 = { a : 0 , b : { c : 0 }};let obj2 = Object .assign ({}, obj1);console .log (JSON .stringify (obj2)); obj1.a = 1 ; console .log (JSON .stringify (obj1)); console .log (JSON .stringify (obj2)); obj2.b .c = 3 ; console .log (JSON .stringify (obj1)); console .log (JSON .stringify (obj2));
此例不仅证明了赋值与浅拷贝的区别,还向我们展示了浅拷贝的含义,其中obj1内部的对象{c:0}被obj2改变后,obj1也跟着改变了,即证明浅拷贝只是复制其引用
深拷贝 假如要完全新建一个原对象的实例,一般用JSON.parse(JSON.stringify())来处理。
1 2 3 4 5 6 obj1 = { a : 0 , b : { c : 0 }}; let obj3 = JSON .parse (JSON .stringify (obj1)); obj1.a = 4 ; obj1.b .c = 4 ; console .log (JSON .stringify (obj3));
再者,使用for…in遍历进行拷贝。循环判断对象的成员是否还是对象,还是的话继续调用deepCopy
1 2 3 4 5 6 7 8 9 10 function isObj (obj ) { return (typeof obj === 'object' || typeof obj === 'function' ) && obj !== null } function deepCopy (obj ) { let tempObj = Array .isArray (obj) ? [] : {} for (let key in obj) { tempObj[key] = isObj (obj[key]) ? deepCopy (obj[key]) : obj[key] } return tempObj }
但是,JSON序列化只能处理源对象是对象、数组、基本类型格式的。在序列化JavaScript对象时,所有函数和原型成员会被有意忽略。所以带函数的就没办法拷贝了。这些特殊情况需要针对具体情况进行处理。
数组常用方法
添加元素
1 2 3 4 5 6 7 8 9 10 11 12 13 Array .prototype .push () Array .prototype .unshift () Array .prototype .concat () Array .prototype .splice (start, end, item1, item2...) var months = ['Jan' , 'March' , 'April' , 'June' ];months.splice (1 , 0 , 'Feb' ); console .log (months);months.splice (4 , 1 , 'May' ); console .log (months);
删除元素
1 2 3 Array .prototype .pop () Array .prototype .shift () Array .prototype .splice (start, end)
遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Array .prototype .forEach (callback (item, index, array), this ) Array .prototype .map (callback (item, index, array), this ) Array .prototype .every (callback (item, index, array), this ) Array .prototype .some (callback (item, index, array), this ) Array .prototype .keys () Array .prototype .values () Array .prototype .reduce (callback (accumulator, currentValue, currentIndex, array), initiatiValue) const array1 = [1 , 2 , 3 , 4 ];const reducer = (accumulator, currentValue ) => accumulator + currentValue;console .log (array1.reduce (reducer));const reducer1 = (accumulator, currentValue ) => accumulator + currentValue;console .log (array1.reduce (reducer1, 3 ));
会改变原数组的方法
1 2 3 4 5 6 7 Array .prototype .push () Array .prototype .unshift () Array .prototype .pop () Array .prototype .shift () Array .prototype .splice (start, n, item1, item2...) Array .prototype .reverse () Array .prototype .sort ()
不会改变原数组的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Array .prototype .concat () Array .prototype .slice (begin, end) Array .prototype .find (function ( ){}) Array .prototype .every () Array .prototype .some () Array .prototype .indexOf () Array .prototype .lastIndexOf () Array .prototype .map () Array .prototype .toString () Array .prototype .join () Array .prototype .flat (depth) Array .prototype .reduce (callback (accumulator, currentValue, index, array), initialValue)
数组查重
1 2 3 4 5 6 7 8 9 10 const myArray = [1 , 2 , 3 , 1 , 2 , 5 , 5 , 5 , 55 ];const res = myArray.reduce ((pre, cur ) => { if (pre[cur] === undefined ) { pre[cur] = 1 ; } else { pre[cur] += 1 ; } return pre; }, {}); console .log ('res' , res);
数组去重
求数组交集与并集
1 2 3 4 5 let names = ['Alice' , 'Tom' , 'Jhon' ];let nameList = ['Alice' , 'Tom' , 'Joe' , 'Hank' ];let res = nameList.filter (item => names.includes (item));console .log (res);
1 2 3 4 5 6 let names = ['Alice' , 'Tom' , 'Jhon' ];let nameList = ['Alice' , 'Tom' , 'Joe' , 'Hank' ];let res = names.concat (nameList);res = [...new Set (res)]; console .log (res);
不用遍历,生成指定长度数组:
1 [...Array (length)].map (() => { item => 0 });
1 '' .padEnd (length - 1 , ',' ).split (',' );
1 Array .from ({length :10 }).map (function (item,index ){return index});
同一个数组内的元素组合
1 2 3 4 5 6 7 8 9 10 11 12 13 const nums = [2 , 7 , 11 , 15 ];function permutation (nums ) { const newNums = [].concat (nums); const permutation = [] for (let j = 0 ; j < newNums.length ; j ++) { for (let k = j + 1 ; k < newNums.length ; k ++) { permutation.push ([newNums[j], newNums[k]]); } } };