js基础之数组

现象

今天工作时碰到一个需求,有两个数组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); // {a: 1, b: 10, c: 5}

// 数组。请注意,这是赋值,不是浅拷贝
const o1 = [1,2,3];
const o2 = o1;

console.log(o1 === o2); // => true
o2.push(4);
console.log(o1); // => [1, 2, 3, 4]

数组的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);

// Array ["ant", "bison", "camel", Array [65, 2, 3, Array [2, 3, 4]], "duck", "elephant"]
// Array ["camel", Array [65, 2, 3, Array [2, 3, 4]], "duck", "elephant"]

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)); // { a: 0, b: { c: 0}}

obj1.a = 1;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 0}}
console.log(JSON.stringify(obj2)); // { a: 0, b: { c: 0}}

obj2.b.c = 3;
console.log(JSON.stringify(obj1)); // { a: 1, b: { c: 3}}
console.log(JSON.stringify(obj2)); // { a: 2, b: { c: 3}}

此例不仅证明了赋值与浅拷贝的区别,还向我们展示了浅拷贝的含义,其中obj1内部的对象{c:0}被obj2改变后,obj1也跟着改变了,即证明浅拷贝只是复制其引用

深拷贝

假如要完全新建一个原对象的实例,一般用JSON.parse(JSON.stringify())来处理。

1
2
3
4
5
6
// Deep Clone 
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)); // { a: 0, b: { c: 0}}

再者,使用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. 添加元素
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...) // 从start开始,到end结束,删除中间元素,并将后面的item添加进去
// 示例
var months = ['Jan', 'March', 'April', 'June'];
months.splice(1, 0, 'Feb');
console.log(months);
// expected output: Array ['Jan', 'Feb', 'March', 'April', 'June']

months.splice(4, 1, 'May');
console.log(months);
// expected output: Array ['Jan', 'Feb', 'March', 'April', 'May']
  1. 删除元素
1
2
3
Array.prototype.pop() // 删除最后一个元素,并返回该元素的值
Array.prototype.shift() // 删除开头一个元素,并返回该元素的值
Array.prototype.splice(start, end) // 从start开始,到end结束,删除中间元素
  1. 遍历
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) // 对数组的每个元素都进行callback判断,如果全部都符合,返回true,否则返回false
Array.prototype.some(callback(item, index, array), this) // 对数组的元素进行callback运算,找到一个符合的立即返回true
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;
// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10

const reducer1 = (accumulator, currentValue) => accumulator + currentValue;
// 3 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer1, 3));
// expected output: 13
  1. 会改变原数组的方法
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...) // 在数组中删除或添加n个元素,返回被删除的元素,添加的元素不会被返回
Array.prototype.reverse() // 数组元素反转
Array.prototype.sort() // 对数组元素进行排序
  1. 不会改变原数组的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Array.prototype.concat() // 连接数组,返回新数组
Array.prototype.slice(begin, end) // 返回一个新的数组对象,由begin和end决定的原数组的浅拷贝
Array.prototype.find(function(){}) // 查找一个符合条件的元素
Array.prototype.every() // 全部元素符合方法返回true
Array.prototype.some() // 有一个符合就返回true
Array.prototype.indexOf() // 查找某元素是否存在于该数组,有则返回第一个的index,无则返回-1
Array.prototype.lastIndexOf() // 查找某元素是否存在于该数组,有则返回倒数第一个的index,无则返回-1
Array.prototype.map() // 对数组内每个元素都执行一个方法,返回新数组
Array.prototype.toString() // 将数组元素转换为字符串
// let array = ["Jan", "March", "April", "June"]
// array.toString()
// "Jan,March,April,June"
Array.prototype.join() // 数组元素用指定字符串连接的字符串
Array.prototype.flat(depth) // 按照指定的深度递归遍历数组,扁平化数组元素
Array.prototype.reduce(callback(accumulator, currentValue, index, array), initialValue) // 将数组元素按照顺序从initialValue开始执行callback,返回最终的值
  1. 数组查重
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); // res { '1': 2, '2': 2, '3': 1, '5': 3, '55': 1 }
  1. 数组去重
1
[...new Set(Array)]
  1. 求数组交集与并集
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. 不用遍历,生成指定长度数组:
1
[...Array(length)].map(() => { item => 0 });
1
''.padEnd(length - 1, ',').split(',');
1
Array.from({length:10}).map(function(item,index){return index});
  1. 同一个数组内的元素组合
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]]);
}
}
};