JS知识点回顾——数组

223 阅读7分钟

创建数组的方式

数组对象字面量

const arr = [1,2,3];

new Array

如果参数是数字且是在合理范围内的数字;生成该参数长度的数组,里面内容是empty,empty在一些遍历中是不参与运算的;如果不是数字将参数作为数组元素

new Array('a') // ['a']

Array.from

可以传入一个类数组,或者一个可迭代的对象,比如set、map、已经实现了迭代器的对象等等

image.png

Array.of

Array.of() 和 Array() 构造函数之间的区别在于对单个参数的处理:Array.of(7) 创建一个具有单个元素 7 的数组,而new Array(7) 创建一个 length 为 7 的空数组

数组的一些特点

数组长度

一个逗号不代表成员,代表后面还能输入;逗号和逗号之间没有数据代表空元素

console.log([1,].length) // 1
console.log([1,,].length) // 2
console.log(new Array('10').length) // 1
console.log(new Array(10).length) // 10

清空数组的方法 Array.prototype.splice(0) 和 Array.prototype.length = 0

空元素

empty:数组的空位,指数组的某一个位置没有任何值;有空元素的数组的是稀疏数组

console.log([1,,])

image.png

V8引擎在存储稀疏数组的时候不是连续的空间,而是散列模式,在访问的时候会去计算hash相对慢一点,但是在存储空间上又会相对节约一些;而连续的空间可以使用索引访问,相对快

避免创建稀疏数组

Array.apply(null,Array(3));
[...new Array(3)]; // [undefined, undefined, undefined]
Array.from(Array(3))

数组不会自动添加分号

( { + - /作为一行代码的开头,不会和前面的代码做分割

const a = [1,2,3];
console.log(a);
[1,2,3].map((v)=>{
    console.log(v*v)
})

image.png

const a = [1,2,3];
console.log(a) // 这里去掉分号了
[1,2,3].map((v)=>{
    console.log(v*v)
})

image.png

console.log返回值是undefined,不能被遍历

includes和indexOf

const arr1 = [NaN];
console.log("includes NaN:",arr1.includes(NaN));
console.log("indexOf NaN:",arr1.indexOf(NaN) > -1);
const arr2 = [1,,];
console.log("includes empty:",arr2.includes(undefined));
console.log("indexOf empty:",arr2.indexOf(undefined)>-1);
const arr3 = [undefined];
console.log("includes undefined:",arr3.includes(undefined));
console.log("indexOf undefined:",arr3.indexOf(undefined)>-1);

image.png

image.png

数组可变长度问题

  1. length代表数组中的元素个数,数组额外附加属性不计算在内,比如arr.a,因为数组本身也是对象
  2. length可写,可以通过修改length改变数组长度,比如设置arr.length = 0
  3. 数组操作不存在越界,找不到下标返回undefined
const array = [1,2,3,4,5,6];
array[10] = 10;
console.log("length:",array.length);
array['test'] = 11;
console.log("length:",array.length);
array.length = 3;
console.log("length:",array.length);
console.log("value:",array[Number.MAX_VALUE + 1000]);

image.png

数组中可以改变自身的方法

1、删减操作:pop、shift、splice

2、unshift、push、sort、reverse

3、copyWithin、fill

特别的sort

const array = [1,2,3,4,5,6,7,8,9,10];
array.sort();
console.log(array) // [1, 10, 2, 3, 4, 5, 6, 7, 8, 9]

console.log("10:".charCodeAt()) // 49
console.log("1:".charCodeAt()) // 49

非线性存储问题

默认情况下是线性存储的,下标有序存取;我们尽量有序递增,否则将改变存储规则影响性能

delete删除数组

不会改变数组长度,只是删除该下标的引用

concat性能问题

复制大量数据的时候,push性能比concat好很多

const count = 10000;
const arr = [1, 2, 3, 4, 5];
let newArr = [];
console.time("push");
for(let i=0;i<count;i++){
    newArr.push(arr[0],arr[1],arr[2],arr[3],arr[4]);
}
console.timeEnd("push")
// 大概在1秒左右

const count = 10000;
const arr = [1, 2, 3, 4, 5];
let newArr = [];
console.time("concat");
for(let i=0;i<count;i++){
    newArr.concat(arr[0],arr[1],arr[2],arr[3],arr[4]);
}
console.timeEnd("concat")
// 大概在3秒左右

批量制造数组

for循环
function creatData() {
  const res = [];
  for(let i=0;i<1000;i++){
      res.push({
        name:`name${i+1}`
      })
  }
  return res;
}
console.log(creatData())

image.png

特别注意使用map

创建出来的empty不参与运算,如map、filter;详情看juejin.cn/post/712722…

function creatData() {
  return new Array(1000).map((v,i)=>{
      return {name:`name${i+1}`}
  })
}
console.log(creatData())

image.png

修改:先填充null再修改

function creatData() {
  return new Array(1000).fill(null).map((v,i)=>{
      return {name:`name${i+1}`}
  })
}
console.log(creatData())

数据量大必然会导致性能问题

Array.from

从[可迭代]或[类数组]对象创建一个新的浅拷贝的数组实例;接收的第二个参数,可以对数组中的每个元素做处理

function creatData() {
  return Array.from({length:1000},(v,i)=>{
      return {name:`name${i+1}`}
  })
}
console.log(creatData())

数组去重

set

const arr1 = [1,2,3];
const arr2 = [3,4,5];
console.log(new Set([...arr1,...arr2]))
// Set(5) {1, 2, 3, 4, 5}

使用for循环

for+indexOf:indexOf不能对NaN去重,可以换成includes,数据量大也会造成性能问题

function mergeArray(arr1,arr2) {
  // 复制数组
  const arr = arr1.slice(0);
  let v;
  for(let i=0;i<arr2.length;i++){
      v=arr2[i];
      if(~arr.indexOf(v)){
        continue;
      }
      arr.push(v)
  }
  return arr;
}

数组对象去重

function uniqueArray(arr = [],key) {
  const keyValues = new Set();
  let val;
  return arr.filter(obj => {
    val = obj[key];
    if(keyValues.has(val)){
      return false;
    }
    keyValues.add(val);
    return true;
  })
}
类数组

是一个具有length属性和从零开始索引的属性,但是没有Array的内置方法,比如forEach、map等的一种特殊对象

特点:

  • 是一个普通对象
  • 必须有length属性,可以有非负整数索引
  • 本身不具备数组的方法

常见的类数组:

  • arguments
  • DOM,如NodeList、HTMLCollection、DOMTokenList

image.png

特别的字符串

具备类数组的所有特征,但类数组一般指对象

image.png

数组和类数组的区别

image.png

代码判断是否是类数组

function isArrayLikeObject(arr) {
  if(arr == null || typeof arr !=="object"){
    return false;
  }
  const lengthMaxValue = Math.pow(2,53) - 1;
  // 需要自身带有length属性
  if(!Object.prototype.hasOwnProperty.call(arr,"length")) return false;
  // 并且length属性的值可以转换成数字
  if(typeof arr.length != "number") return false;
  // 是否是有限的length值
  if(!isFinite(arr.length)) return false;
  if(Array === arr.constructor) return false;
  if(arr.length >=0 && arr.length < lengthMaxValue){
    return true;
  }else {
    return false;
  }
}

类数组转换成数组

slice和concat

const arrayLikeObj = {
      0:1,
      1:2,
      length:2,
}
console.log(Array.prototype.slice.call(arrayLikeObj)); //  [1, 2]
console.log(Array.prototype.concat.apply([],arrayLikeObj));//  [1, 2]
console.log(Array.prototype.slice.call("arrayLikeObj")); 
// ['a', 'r', 'r', 'a', 'y', 'L', 'i', 'k', 'e', 'O', 'b', 'j']

Array.from

console.log(Array.from(arrayLikeObj)) // [1,2]

Array.apply

console.log(Array.apply(null,arrayLikeObj)) // [1,2]

万能数据生成器

const createValues = (creator,length = 10) => Array.from({length},creator)
随机数生成器
const createRandomValues = (len) => createValues(Math.random,len);
序列生成器
const createRange = (start,end,step) => createValues((_,i) => start + (i*step), (end-start) / step + 1);
数据生成器
const createUser = (_,i) => {
  return {
    name:`user-${i}`,
    age:Math.random()*100 >> 0 // 取整
  }
}
console.log(createValues(createUser),5)

image.png

求数组交集

Array.prototype.filter + includes;不能用于引用数据类型;includes性能消化较大;单次使用includes没有问题,在遍历中多次使用性能很差

function intersectSet(arr1,arr2) {
      return [...new Set(arr1)].filter(item => arr2.includes(item))
};

引用类型:Array.prototype.forEach + Map + Key + filter唯一

function intertsect(arr1,arr2,key) {
  const map = new Map();
  arr1.forEach((val)=> map.set(val[key]));
  return arr2.filter(val => map.has(val[key]))
};

求差集

function intertsect(arr1,arr2,key) {
  const map = new Map();
  arr1.forEach((val)=> map.set(val[key]));
  return arr2.filter(val => !map.has(val[key]))
};

数组中去除虚值

虚值是指转为布尔为false的值

Array.prototype.filter(Boolean)
const arr = [false,0,undefined,,"",NaN,9,true,null,false,"test"];
console.log(arr.filter(Boolean)) // [9,true,'test']

获取数组中的最大值和最小值

Math.max.apply(Math,arr)
Math.min.apply(Math,arr)

reduce

获取queryString

queryString是用于页面传递参数;是请求地址问号后拼接的键值对

现代浏览器已有的API:URLSearchParams 和 URL

const urlSP = new URLSearchParams(location.search);
function getQueryString(key) {
      return urlSP.get(key);
}
console.log("words:",getQueryString('words'))


const urlObj = new URL(location.href);
function getQueryString(key) {
      return urlObj.searchParams.get(key);
}
console.log("words:",getQueryString('words'))

// urlObj.searchParams instanceof URLSearchParams  ==> true 其实他们来自一个东西

reduce实现

image.png

将location.search取问号后面的参数按照&分割,结果再按照=分割即可得到queryString的键值对

const urlObj = location.search.slice(1).split("&").filter(Boolean)
 .reduce((obj,cur)=>{
     const arr = cur.split("=");
     if(arr.length !=2){
               return obj;
     }
     // 处理可能出现的中文
     obj[decodeURIComponent(arr[0])] = decodeURIComponent(arr[1]);
    return obj;
},{});
function getQueryString(key) {
  return urlObj.searchParams.get(key);
}

折上折

草民版:

function discount(x) {
      return x*0.9;
}
function reduce(x) {
      return x>200 ? x-50:x;
}
console.log(reduce(discount(100)));  // 90
console.log(reduce(discount(230))); // 157

黄金版:无法适应变化;需要优化getPriceMethods方法实现传入函数的返回值刚好是下一个函数的入参

function discount(x) {
      return x*0.9;
}
function reduce(x) {
      return x>200 ? x-50:x;
}
function getPriceMethods(discount,reduce) {
  return function getPrice(x) {
    return reduce(discount(x))
  }
}
const method = getPriceMethods(discount,reduce)
console.log(method(100));
console.log(method(230));

王者:

function discount(x) {
      return x*0.9;
}
function reduce(x) {
      return x>200 ? x-50:x;
}
function discountPlus(x) {
      return x*0.95
}
function compose(...funcs) {
      if(funcs.length==0) return arg => arg;
      // 注意执行顺序
      return funcs.reduce((a,b) => (...args) => a(b(...args)))
}
const method = compose(discountPlus,reduce,discount)
console.log(method(100));
console.log(method(230));

例子3

假设需要先登录,再获取用户信息,再获取订单;刚好前者的返回值是下一个函数的入参;伪代码如下

function login(...args): Promise<any> {};
function getUesrInfo(...args): Promise<any> {};
function getorders(...args): Promise<any> {};
const order = async (loginParams) {
      const loginRes = await login(loginParams);
      const userRes = await getUesrInfo(loginRes);
      const orderRes = await login(userRes);
      console.log(orderRes)
}

升级:

function runPromise(promiseCreators,initData) {
  return promiseCreators.reduce((promise,next)=>{
    return promise.then((data)=>next(data))
  },Promise.resolve(initData))
}
runPromise([login,,getUesrInfo,getorders],loginParams).then((data)=>{
    console.log(data);
})

数组分组

const hasOwn = Object.prototype.hasOwnProperty;
function group(arr,fn) {
  if(!Array.isArray(arr)){
    return arr;
  }
  if(typeof fn !== "function"){
    throw new TypeError("fn必须是一个函数");
  }
  let v;
  return arr.reduce((obj,cur,index)=>{
    v=fn(cur,index);
    if(!hasOwn.call(obj,v)){
      obj[v] = [];
    };
    obj[v].push(cur);
    return obj;
  },{})
}
console.log(group(['apple','pear','orange','peach'],v=>v.length))

image.png

手写

Array.isArray

作用:确定传递的值是否是Array

const arr = ['1'];
console.log(Array.isArray(arr))  // true

特别的proxy对象代理:如果是个代理对象,看代理对象的target属性是不是Array;proxy虽然使用new,但是他不是一个构造函数,只是将传入内容做了代理

const arr = ['1'];
const proxy = new Proxy(arr,{});
console.log(proxy)
console.log(Array.isArray(proxy))

image.png

以下几种方式都是可以的

function myIsArray(arr) {
  return Object.prototype.toString.call(arr) === '[object Array]';
}

function myIsArray(arr) {
      if(typeof arr !== 'object' || arr === null) return false;
      return arr instanceof Array;
}

function myIsArray(arr) {
      return Array.prototype.isPrototypeOf(proxy);
}

Array.prototype.entries

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

const arr = [1,undefined,null,{}];
const iter = arr.entries();
console.log(iter.next());
for(let [k,v] of iter) {
      console.log(k,v)
}

image.png

实现next

Array.prototype.myEntries = function () {
  const obj = Object(this);
  let index = 0;
  const len = obj.length;
  return {
    next() {
      if(index < len){
        return { value:[index,obj[index++]],done:false};
      }
      return {value:undefined,done:true};
    }
  }
}
const arr = [1,2,3];
const iter = arr.myEntries();
console.log(iter.next())

实现迭代器

对象obj是可遍历的(iterable),因为具有Symbol.iterator属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性。

const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
};

完整实现

Array.prototype.myEntries = function () {
  const obj = Object(this);
  let index = 0;
  const len = obj.length;
  function next() {
    if(index < len){
        return { value:[index,obj[index++]],done:false};
      }
      return {value:undefined,done:true};
  }
  return {
    next,
    [Symbol.iterator](){
      return {next};
    }
  }
}
const arr = [1,2,3];
const iter = arr.myEntries();
for (const [k,v] of iter) {
      console.log(k,v)
}

其实数组上很多方法都有迭代器,比如arr.keys等,所以需要将迭代器实现到数组的原型

Array.prototype[Symbol.iterator] = function () {
  const obj = Object(this);
  let index = 0;
  const len = obj.length;
  function next() {
    if(index < len){
        return { value:[index,obj[index++]],done:false};
      }
      return {value:undefined,done:true};
  }
  return {
    next
  }
}

再实现Array.prototype.entries:

Array.prototype.myEntries = function () {
  const obj = Object(this);
  const len = obj.length;
  const entries = [];
  for(let i=0;i<len;i++){
    entries.push([i,obj[i]])
  }
  const itr = this[Symbol.iterator].bind(entries)();
  return {
    next:itr.next,
    [Symbol.iterator]() {return itr}
  }
}
const arr = [1,2,3];
const iter = arr.myEntries();
for (const [k,v] of iter) {
      console.log(k,v)
}

Array.prototype.includes

特点是能识别NaN

const arr = [1,2,3,null,undefined,NaN,""];
console.log(arr.includes(null)) // true
console.log(arr.includes(undefined)) // true
console.log(arr.includes(NaN)) // true

第二个参数position的特点:

1、如果不是数字变成数字

2、如果是NaN变成0

3、如果是正无穷返回false,负无穷返回0

4、返回与number符号相同且大小为floor(abs(number))的数值

5、如果是undefined变成0

6、设start为min(max(pos, 0), len)

image.png image.png

Number.isNaN = function (param) {
  if(typeof param === "number"){
    return isNaN(param);
  }
  return false;
}
function toIntegerOrInfinity(params) {
  const num = Number(params);
  if(Number.isNaN(num) || num == 0){
    return 0;
  }
  if(num === Infinity || num == -Infinity){
    return num;
  }
  const inter = Math.floor(Math.abs(num));
  return num < 0 ? -inter : inter;
}
Array.prototype.myIncludes = function(item,fromIndex) {
  if(this == null){
    throw new TypeError('无效的this')
  }
  const obj = Object(this);
  const len = obj.length;
  if(len <=0){
    return false;
  }
  const n = toIntegerOrInfinity(fromIndex);
  if(fromIndex === undefined){
    return 0;
  }
  if(n === Infinity){
    return false;
  }
  if(n === -Infinity){
    return 0;
  }
  let k = n > 0 ? n : len+n;
  if(k<0){
    k=0;
  }
  const isNaN = Number.isNaN(item);
  for(let i=k;i<len;i++){
      if(obj[i] === item){
        return true;
      }else if(isNaN && Number.isNaN(obj[i])){
        return true
      }
  }
  return false;
}

Array.from

从可迭代或类数组对象创建一个新的浅拷贝的数组实例,接收三个参数Array.from(arrayLike, mapFn, thisArg)

arrayLike:必填,想要转换成数组的类数组或可迭代对象,也可以是字符串

mapFn:可选,调用数组每个元素的函数。如果提供,每个将要添加到数组中的值首先会传递给该函数,然后将 mapFn 的返回值增加到数组中

thisArg:可选,执行 mapFn 时用作 this 的值

等同于Array.from(obj).map(mapFn,thisArg)

console.log(Array.from({}))
console.log(Array.from(""))
console.log(Array.from({a:1,length:'10'}))
console.log(Array.from({a:1,length:'s'})) // length不能转换成数字
const max = Math.pow(2,32);
console.log(Array.from({0:1,length:max - 1})) 
// 浏览器不会报错,但是会崩溃
console.log(Array.from({0:1,length:max}))
// 浏览器会报错

image.png

const toIntegerOrInfinity = function (value) {
  const number = Number(value);
  if(isNaN(number)) return 0;
  // 判断是否是无穷
  if(number == 0 || !isFinite(number)) return number;
  return (number > 0 ? 1 : -1)*Math.floor(Math.abs(number));
}
const ToLength = function (value) {
  const len = toIntegerOrInfinity(value);
  return Math.min(Math.max(len,0),Number.MAX_SAFE_INTEGER)
}
const isCallable = function (fn){
  return typeof fn === "function" || Object.prototype.toString.call(fn) === "[object Function]";
}
Array.myForm = function(arrayLike,mapFn,thisArg){
  const C = this;
  if(arrayLike == null){
    throw new TypeError('需要传入一个对象')
  }
  if(typeof mapFn !== 'undefined' && typeof mapFn != "function"){
    throw new TypeError('如果传入mapFn必须是个方法')
  }
  const items = Object(arrayLike);
  const len = ToLength(items.length);
  if(len <= 0){
    return [];
  }
  // 判断c是否可调用
  const A = isCallable(C) ? Object(new C(len)) : new Array(len);
  for(let i=0;i<len;i++){
    const value = items[i];
    if(mapFn){
      A[i] = typeof thisArg === 'undefined' ? mapFn(value,i) : mapFn.call(thisArg,value,i);
    }else {
      A[i] = value;
    }
  }
  return A;
}

function MyArray(len) {
    return new Array(len * 2);
}
console.log(Array.myForm.call(MyArray,{length:5}))

image.png