写点什么

重学 JS | 数组知识点大全,必收藏!

用户头像
梁龙先森
关注
发布于: 2021 年 01 月 04 日
重学JS | 数组知识点大全,必收藏!

数组在日常开发中,无处不见,本篇文章整理了数组的日常用法、疑难点解析,以及数组常用算法类型。并通过编写 polyfill,理解数组常用方法的内部实现。首先从了解数组的底层实现开始,看看 chrome V8 引擎是如何实现。

本文大纲如下:

  1. 数组的底层实现

  2. 数组的类型判断

  3. 数组静态属性

  4. 数组静态方法

  5. 数组常见算法

1. 数组的底层实现

数组是一个特殊的对象,底层实现是JSArray extends JSObject,内部也是通过key-value的形式存储数据,这也就可以理解 JS 的数组可以存放不同的数据结构。


JS 数组存在两种表现形式:快数组和慢数组

1.1 快数组(FAST ELEMENTS)

快数组是一种线性的存储结构。新创建的空数组,默认存储方式是快数组,快数组长度是可变的,可以根据元素的增加和删除来动态调整存储空间的大小,内部是通过扩容和收缩机制实现。


扩容

扩容算法如下,扩容后会将数组拷贝到新的内存空间。

new_capacity = old_capicity/2+old_capicity + 16
复制代码

收缩

收缩条件:(容量) >= length*2 + 16,则收缩容量调整,否则用holes对象填充空位置。

收缩算法:

elements_to_trim = length+1 == old_length?(capacity-length)/ 2 : capacity-length
复制代码

hodes:空洞,对象指的是数组中分配了空间,但是没有存储元素的位置。

转为慢数组

  1. 新容量 >= 3*扩容后的容量*2

  2. 加入的 index-当前 capacity >= 1024,即存在超过 1024 个空洞


快数组就是以空间换时间的方式,申请了大块连续内存,提高效率。

1.2 慢数组(DICTIONARY ELEMENTS)

慢数组是一种字典的内存形式。不用开辟大块连续的存储空间,节省了内存,但是由于需要维护这样一个 HashTable,其效率会比快数组低。

转为快数组:

处于哈希表实现的数组,在每次空间增长时, V8 的启发式算法会检查其空间占用量。当慢数组的元素可存放在快数组中且长度在 smi 之间且仅节省了 50%的空间,则会转变为快数组


慢数组以时间换空间,不必申请连续的空间,节省了内存,但需要付出效率变差的代价。

2. 数组类型判断

2.1 typeof 为啥判断不了?

谈到数据类型判断,通常会想到typeof,但实际上这运算符只在判断基本数据类型有用,对于引用类型乏力。

typeof {} // objecttypeof [] // object
复制代码

为什么呢?这里就要看看,js 变量在底层中,它的数据类型是怎么实现的了。

重点:JS 底层处理变量时,会在变量机器码的低位 1-3 位存储其类型信息。

000 : Object001 : Int010 : Double100 : String110 : Boolean
特殊点:null:代表空指针,大多数平台中值为0x00,因此null的类型标签就成了0,所以typeof null返回'object'undefined:用-2^30 整数来表示
复制代码
2. 2 instanceof 运算符

原理:通过查找原型链来检测变量是否为某个类型的实例。

[] instanceof Array   // true[] instanceof Object  // true{} instanceof Array   // false
// []的原型链如下,因此为Array / Object 的实例[].__proto__ = Array.prototypeArray.prototype.__proto__ === Object.prototype
复制代码

可以通过优先判断类型是否为 Array 的实例的来判断类型,封装如下:

function isArray(obj){	if(obj instanceof Array){  	return true  }else if(obj instanceof Object){  	return false  }else{  	return '参数非对象类型'  }}
复制代码
2.3 判断构造函数

判断变量是否为数组还是对象,另一角度讲,即是判断变量的构造函数是 Array 类型还是 Object 类型。因为对象的实例都是通过构造函数生成的。


为什么变量会有 constructor 属性呢?

每个变量都会有个__proto__属性,表示的是隐式原型。一个对象的隐式原型指向的是构造该对象的构造函数的原型。

[].constructor === Array  // true[].constructor === Object // false[].__proto__ === Array.prototype  // true[].__proto__ === [].constructor.prototype // true
[].__proto__.constructor == Array // true
复制代码

通过如上对原型链的分析,封装判断方法:

function isArray(obj){  // 获取构造函数  let constructor = obj.__proto__.constructor || obj.constructor  if(constructor === Array){  	return true  }else if(constructor === Object){  	return false  }else{  	return '参数非对象类型'  }}
复制代码
2.4 toString()函数

每种引用类型都会直接或间接继承 Object 的属性,因此都存在toString方法,并且不同数据类型toString返回的值不一样,因此可用于判断是数组还是对象。

Object.prototype.toString.call([])  // "[object Array]"Object.prototype.toString.call({})  // "[object Object]"Object.prototype.toString.call(1)   // "[object Number]"
复制代码

封装方法如下:

function isArray(obj){	let type = Object.prototype.toString.call(obj)  if(type === "[object Array]"){  	return true  }else if(type === "[object Object]"){  	return false  }else{  	return '参数非对象类型'  }}
复制代码
2.5 Array.isArray()函数

只能判断是否为数组,不能判断是否为对象。

Array.isArray([])  // trueArray.isArray({})  // falseArray.isArray(0)   // false
复制代码

3. 数组静态属性

3.1 Array.from()

从类似数组或可迭代的对象中创建一个新的,浅表复制的 Array 实例。

什么是类数组对象?

一个对象并不是由 Array 构造函数所创建的,它依然呈现出数组的行为。在 ECMAScript 5 标准中,字符串 string 就是一个只读的类数组对象。

/* * 语法: * mapFn: 映射函数可调用数组的每个元素 */ Array.from(arrayLike [, mapFn [, thisArg]])
// ============ 用法 ==================== // 字符串Array.from('ab') // ['a','b']// Set Array.from(new Set([1,2,1,2])) // [1,2]// Map const map = new Map([[1, 2]])Array.from(map) // [[1,3]]Array.from(map.values()) // [2]Array.from(map.keys()) // [1]// NodeListconst imgs = document.getElementsByTagName('img')const sources = Array.from(imgs,img=>img.src) // 返回img标签的src属性值// argumentsfunction fun(){ return Array.from(arguments) // [1,2]}fun(1,2)// objectArray.from({a:1,b:2}) // []Array.from({a:1,length:2}) // [undefined,undefined]Array.from({0:1,1:2,length:2}) // [1,2]
// 映射函数Array.from([1,2,3],i=>i*2) // [2,4,6]
复制代码
3.2 Array.isArray()

判断参数是否为数组,无法判断是否是对象。

Array.isArray([])  // trueArray.isArray({})  // falseArray.isArray(1)  // false
复制代码
3.3 Array.of()

创建新的具有可变数量参数的 Array 实例,而不考虑参数的数量或类型

Array.of(1,2)  // [1,2]Array(2)   // 创建指定长度的空槽数组[empty,empty]  
// polyfillArray.of = function(){ return Array.prototype.slice.call(arguments);}
复制代码

4. 数组静态方法

4.1 concat()

合并两个或多个数组。此方法不更改现有数组,而是返回一个新数组。

[].concat(1,[2,3],[4]) // [1,2,3,4]
复制代码
4.2 entries()

返回一个新的 Array Iterator 对象,该对象包含数组中每个索引的键/值对。

const arr = [1,2,3]const itea = arr.entries() // 迭代器const [index,value] = itea.next().valuem
// 循环const arr = [1,2,3]for(const [index,value] of arr.entries()){ console.log(index,value)}
复制代码
4.3 fill()

将数组中的所有元素更改为静态值,从开始索引(默认为 0)到结束索引(默认为 array.length)。它返回修改后的数组。

let array = [1,2,3,4]array.fill(0,2,4)  // [1,2,0,0]--> arrayarray.fill(5,1)   // [1,5,5,5]
复制代码
4.4 findIndex()

返回满足条件的第一个元素的索引,否则返回-1,表示没有满足的元素。

const index = [1,2,3,4,5].findIndex(function(ele,idx,array){  return ele>3})  // 3
复制代码
4.5 flat()

创建一个新数组,其中所有子数组元素都以递归方式连接到该数组中,直到达到指定的深度。

const arr2 = [0, 1, 2, [[[3, 4]]]];arr2.flat(2)  // 2级深度: [0, 1, 2, [3, 4]]
复制代码
4.6 includes()

确定数组的条目中是否包含某个值,并根据需要返回 true 或 false。

[1,2,3].includes(2)  // true[1,2,3].includes(4)  // false
复制代码
4.7 indexOf()

返回可以在数组中找到给定元素的第一个索引;如果不存在,则返回-1。

[1,2,3,4].indexOf(3)   // 2[1,2,3,4].indexOf(3,2) // 从索引2,开始往后搜索3 --> 2(从头往后的索引值)[1,2,3,4].indexOf(3,3) // 从索引3,开始往后搜索3 --> -1
复制代码
4.8 数组其他用法
// 通过连接数组(或类似数组的对象)中的所有元素来创建并返回新字符串。[1,2,3].join()  // 1,2,3
// 返回一个新的Array Iterator对象,该对象包含数组中每个索引的键。let itea = [1,2,3].keys() // 迭代器itea.next() // {value:0,done:false}
// 返回一个新的Array Iterator对象,该对象包含数组中每个索引的值。1let itea = [1,2,3].values()itea.next() // {value:1,done:false}
// 从数组中删除最后一个元素,然后返回该元素。此方法更改数组的长度[1,2,3].pop() // 3,返回未删除前的数组长度
// 将一个或多个元素添加到数组的末尾,并返回该数组的新长度。[1,2,3].push(4) // 4
// 数组翻转[1,2,3,4].reverse() // [4, 3, 2, 1]
// 从数组中删除第一个元素,然后返回删除的元素。此方法更改数组的长度[1,2,3,4].shift() // 1
// 将一个或多个元素添加到数组的开头,并返回数组的新长度。[1,2].unshift(3,4) // 4
// 从start到end切割数组,不包含end,返回新数组[1,2,3,4,5,6].slice(2,4) // [3,4]
// 数组排序let numbers = [4, 2, 5, 1, 3];numbers.sort(function(a, b) { return a - b;}); // [1,2,3,4,5]
// 通过删除或替换现有元素和/或在适当位置添加新元素来更改数组的内容let arr = [1,2]arr.splice(1,0,'a') // arr -> [1,'a',2]let arr1 = [1,2,3,4]let removed = arr1.splice(2,1) // removed:[3] arr1:[1,2,3]
复制代码
4.9 数组遍历 7 种方法

for

forEach

map

filter

some

every

reduce

find

以上数组遍历的 7 种方法详解,以及polyfill,详看文章:

数组遍历的 7 种方法及兼容性处理 (polyfill)

5. 数组常见算法

1. 求数组的最大值和最小值
  1. 通过给 prototype 属性扩展 min()和 max()函数。

Array.prototype.min = function(){	let min = this[0]  for(let i=0,l = this.length;i<l;i++){  	if(this[i]<min){    	min = this[i]    }  }  return min}
Array.prototype.max = function(){ let max = this[0] for(let i=0,l = this.length;i<l;i++){ if(this[i]>max){ max = this[i] } } return max}
[2,4,1,4].min() // 1[2,4,1,4].max() // 4
复制代码
  1. 借助 Math 对象的 min()函数和 max()函数

// 静态方式Array.min = function(array){  return Math.min.apply(Math,array)}Array.max = function(array){  return Math.max.apply(Math,array)}
// prototype属性扩展Array.prototype.min = function(){ return Math.min.apply(null,this)}
Array.prototype.max = function(){ return Math.max.apply(null,this)}
复制代码
  1. 借助 reduce()函数

Array.prototype.min = function(){	return this.reduce(function(preVal,curVal){  	return preVal>curVal?curVal:preVal  })}
// 最大值同理
复制代码

4. 通过 sort()函数

let arr = [2,1,3,4]let sortArr = arr.sort(function(a,b){	return a-b})sortArr[0]  // 最小值
复制代码

5. 借助 es6 的扩展运算符

let arr = [2,1,4,3]Math.min(...arr)  // 最小值Math.max(...arr)  // 最大值
复制代码
2. 数组去重的 7 种算法

数组去重的 7 种算法

3. 找出数组中出现最多元素的 4 种算法

找出数组中出现次数最多元素的 4 种算法

8. 总结

至此我们总结了数组的知识点,值得收藏,工作面试必备!

发布于: 2021 年 01 月 04 日阅读数: 61
用户头像

梁龙先森

关注

无情的写作机器 2018.03.17 加入

vite原理/微前端/性能监控方案...,正在来的路上... 最近太忙...

评论

发布
暂无评论
重学JS | 数组知识点大全,必收藏!