前端JS数组进阶教程
云牧 人气:01.批量制造数据
一、创建新数组使用 for 循环批量 push 数据
function createData() { const data = []; for (let i = 0; i < 1000; i++) { data.push({ name: `name${i + 1}`, }); } return data; } const data = createData(); console.log(data);
二、创建空数组,填充full,然后map
function createData() { // 如果不 fill 循环默认会跳过空值 return new Array(1000).fill(null).map((v, i) => ({ name: `name${i + 1}` })); } const data = createData(); console.log(data);
三、Array.from 第二个初始化函数返回数据
function createData() { return Array.from({ length: 1000 }, (v, i) => ({ name: `name${i + 1}` })); } const data = createData(); console.log(data);
2.数组合并去重
一、Set去重
const arr1 = [1, 2, 3]; const arr2 = [3, 4, 5]; console.log(new Set([...arr1, ...arr2]));
二、for循环,indexOf判断是否存在
const arr1 = [1, 2, 3]; const arr2 = [3, 4, 5]; function mergeArray(arr1, arr2) { // 克隆 const cloneArr1 = arr1.slice(0); let v; for (let i = 0; i < arr2.length; i++) { v = arr2[i]; // 按位非,反转操作数的位,表象是对后面数字取负减一 // 当数组中不存在此项 indexOf 返回 -1 按位非得 0 不走 if 逻辑 // 如果两个数组都包含NaN,想要去重可使用includes if (~cloneArr1.indexOf(v)) { continue; } cloneArr1.push(v); } return cloneArr1; } console.log(mergeArray(arr1, arr2));
去重对象?
const arr1 = [{ id: 1 }, { id: 2 }, { id: 3 }]; const arr2 = [{ id: 3 }, { id: 4 }, { id: 5 }]; console.log(Array.from(new Set([...arr1, ...arr2]))); // [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 3 }, { id: 4 }, { id: 5 } ] // 这样对象都是独立的引用,肯定无法去除属性相同的数据啦
如果是相同引用呢?
const obj3 = { id: 3 }; const arr1 = [{ id: 1 }, { id: 2 }, obj3]; const arr2 = [obj3, { id: 4 }, { id: 5 }]; console.log(Array.from(new Set([...arr1, ...arr2]))); // 确实可以,但是你开发这样做?
我们可以这样做
const arr1 = [{ id: 1 }, { id: 2 }, { id: 3 }]; const arr2 = [{ id: 3 }, { id: 4 }, { id: 5 }]; function mergeArray(arr1, arr2) { // 克隆 const cloneArr1 = arr1.slice(0); let v; for (let i = 0; i < arr2.length; i++) { v = arr2[i]; // 能找到相同 id 属性值的数据则进入判断 if (~cloneArr1.findIndex((el) => el.id === v.id)) { continue; } cloneArr1.push(v); } return cloneArr1; } console.log(mergeArray(arr1, arr2)); // [ { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 } ]
3.创建数组的几种方式
- 字面量
// 字面量 const arr1 = [1, 2, 3, ...[4, 5, 6]]; // 1,2,3,4,5,6 const arr2 = [, , , , ,]; // [empty × 5]
- new Array(当参数只有一个且是数字时,new Array()表示数组的长度,其余参数则是数组的内容)
const arr3 = new Array(5); // [empty × 5] const arr4 = new Array(1, 2, 3); // 1,2,3 const arr5 = new Array("a"); // ["a"]
- Array.of(参数只用来作为数组中的内容)
const arr6 = Array.of(5); // [5] const arr7 = Array.of(1, 'abc', true); // [1, "abc", true]
- Array.from 可传入类数组和可遍历对象转换为真数组
- (第一个参数传入对应类数组和可遍历对象,第二个函数参数则相当于对生成的数组做一次map)
- 可遍历和类数组 ==> 数组、字符串、Set、Map、NodeList、HTMLCollection、arguments以及拥有 length 属性的任意对象
const arr8 = Array.from([1, 2, 3]); // [1,2,3] const arr9 = Array.from({ length: 3 }, (value, index) => { return index + 1; }); // [1,2,3] const arr10 = Array.from({ 0: "a", 1: "b", 2: "c", length: 3 }); // ["a", "b", "c"]
- 其他的很多可以返回数组的方法都算
// Array.prototype.slice const arr11 = Array.prototype.slice.call(document.querySelectorAll("div")); // [div, div, div....] // Array.prototype.concat const arr12 = Array.prototype.concat.call([], [1, 2, 3]); // [1, 2, 3]
4.类数组
- 是一个普通对象,不具备数组自带丰富的内建方法
- key是以数字或者字符串数字组成
- 必须有length属性
const arrayLike = { 0: "a", 1: "b", 2: "c", name: "test", length: 3, push: Array.prototype.push, //自己实现 splice: Array.prototype.splice, }; //由于类数组对象length属性声明了对象有多少个属性,所以可以使用for遍历对象属性: for (let i = 0; i < arrayLike.length; i++) { console.log(i + ":" + arrayLike[i]); }
常见的类数组
- arguments
function person(name, age, sex) { console.log("person arguments:", arguments); console.log("person type:", Object.prototype.toString.call(arguments)); } person("name", "age", "sex");
打印结果如下:
- NodeList、HTMLCollection、DOMTokenList等
const nodeList = document.querySelectorAll("box"); console.log("querySelectorAll type:", Object.prototype.toString.call(nodeList)); const htmlCollection = document.getElementsByTagName("div"); console.log("getElementsByTagName type:", Object.prototype.toString.call(htmlCollection)); const DOMTokenList = document.querySelector("div").classList; console.log("classList:", DOMTokenList);
- 奇特:字符串(具备类数组的特性,但一般类数组指对象)
const str = "abc"; console.log(Object.keys(str)); // ['0', '1', '2'] console.log(Array.from(str)); // ['a', 'b', 'c']
判断是否是类数组
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 属性是否是number类型 if (typeof arr.length != "number") return false; //使用 isFinite() 判断是否在正常数字范围 if (!isFinite(arr.length)) return false; // 构造函数等于Array if (Array === arr.constructor) return false; // 长度有效值 if (arr.length >= 0 && arr.length < lengthMaxValue) { return true; } else { return false; } } console.log(isArrayLikeObject(null)); // false console.log(isArrayLikeObject({ 0: "a", 1: "b", length: 2 })); // true console.log(isArrayLikeObject({ 0: 1, 2: 3, length: "" })); // false console.log(isArrayLikeObject({ 0: 1, 2: 3 })); // false console.log(isArrayLikeObject([1, 2])); // false
类数组如何转换为数组
- 复制遍历
const arr = []; const arrayLike = { 0: 1, 1: 2, length: 2, }; for (let i = 0; i < arrayLike.length; i++) { arr[i] = arrayLike[i]; } console.log(arr); // [1, 2]
- slice, concat等
const arrayLike = { 0: 1, 1: 2, length: 2, }; const array1 = Array.prototype.slice.call(arrayLike); console.log(array1); // [ 1, 2 ] const array2 = Array.prototype.concat.apply([], arrayLike); console.log(array2); // [ 1, 2 ]
- Array.from
const arrayLike = { 0: 1, 1: 2, length: 2, }; console.log(Array.from(arrayLike)); // [ 1, 2 ]
- Array.apply
const arrayLike = { 0: 1, 1: 2, length: 2, }; console.log(Array.apply(null, arrayLike)); // [ 1, 2 ]
- 扩展运算符
console.log([...document.body.childNodes]); // [div, script, script...] // arguments function argumentsTest() { console.log([...arguments]); // [ 1, 2, 3 ] } argumentsTest(1, 2, 3);
如何让类数组使用上数组丰富的内建方法
- 在类数组对象上直接定义数组原型的方法
- 运用call或者apply显示绑定this的指向
例如我想通过 filter 方法过滤出类数组中元素包含 "i" 这个字符的所有元素。
const arrayLike = { 0: "i love", 1: "you", length: 1, }; console.log([].filter.call(arrayLike, (item) => item.includes("i"))); // [ 'i love' ]
为什么会这样?其实可以想想 filter 是如何实现的。
[].__proto__.myfilter = function (callback) { let newArr = []; for (let i = 0; i < this.length; i++) { if (callback(this[i])) { newArr.push(this[i]); } } return newArr; };
可以看出因为 filter 实现是通过 this 进行绑定的,哪个数组调用了这个filter,filter中的 this 就指向哪个数组
类数组和数组的区别
方法/特征 | 数组 | 类数组 |
---|---|---|
自带方法 | 多个方法 | 无 |
length属性 | 有 | 有 |
toString返回 | [object Array] | [object Object] |
instanceof | Array | Object |
constructor | [Function: Array] | [Function: Object] |
Array.isArray | true | false |
5.数组方法的使用注意事项
数组的长度
const arr1 = [1]; const arr2 = [1, ,]; const arr3 = new Array("10"); const arr4 = new Array(10); console.log("arr1 length: " + arr1.length); // arr1 length: 1 console.log("arr2 length: " + arr2.length); // arr2 length: 2 console.log("arr3 length: " + arr3.length); // arr3 length: 1 console.log("arr4 length: " + arr4.length); // arr4 length: 10
数组的空元素 empty
empty:数组的空位,指数组的某一位置没有任何值,有空位的数组也叫稀疏数组
稀疏数组性能会较差,可以避免创建
Array.apply(null,Array(3))
[...new Array(3)]
Array.from(Array(3))
一般遍历如forEach、map、reduce 会自动跳过空位
const arr = [1, ,]; arr.forEach((item) => console.log(item)); // 1 console.log("arr", arr);// arr [ 1, <1 empty item> ]
基于值进行运算,空位的值作为undefined
- find,findIndex,includes等, indexOf除外
- 当被作为迭代的时候,参与Object.entries、扩展运算符、for of 等
join和toString,空位怎么处理
- 视为空字符串
- toString 内部其实会调用 join 方法
数组不会自动添加分号
- (,[, + , -,/,其作为一行代码的开头,很可能产生意外的情况,所以,没事代码最后写个分号,保准没错
const objA = { a: 1 } ["a"]; console.log(objA); // 1 const objB = ["a"] ["a"]; console.log(objB); // undefined const a = [[1, 2], 2, 3]; console.log(a) [0, 2, 3].map((v) => console.log(v * v)); // 报错 console.log(a);
indexOf与includes
方法 | 返回值 | 是否能查找NaN | [, ,]空位 | undefined |
---|---|---|---|---|
indexOf | number | × | × | √ |
includes | boolean | √ | √ | √ |
const array1 = [NaN]; console.log("array.includes NaN:", array1.includes(NaN)); // true console.log("array.indexOf NaN:", array1.indexOf(NaN) > -1); // false const array2 = [1, ,]; console.log("array.includes ,,:", array2.includes(undefined)); // true console.log("array.indexOf ,,:", array2.indexOf(undefined) > -1); // false const array3 = [undefined]; console.log("array.includes undefined:", array3.includes(undefined)); // true console.log("array.indexOf undefined:", array3.indexOf(undefined) > -1); // true console.log(Object.prototype.hasOwnProperty.call(array2, 1)); // 区分空位和undefined,判断此位上是否有值
数组可变长度问题
- length 代表数组中元素个数,数组额外附加属性不计算在内
- length 可写,可以通过修改length改变数组的长度
- 数组操作不存在越界,找不到下标,返回undefined
const array = [1, 2, 3, 4, 5, 6]; array[10] = 10; // 尽量不要这样破坏数组默认线性存储的结构 console.log("array.length:", array.length); // 11 array["test"] = "test"; console.log("array.length:", array.length); // 11 array.length = 3; console.log("array.length:", array.length); // 3 console.log("array value:", array[Number.MAX_VALUE + 1000]); // undefined
数组查找和过滤
方法 | 返回结果类型 | 是否能短路操作 | 是否需要全部满足条件 | 遍历空元素 |
---|---|---|---|---|
some | boolean | √ | × | × |
find | undefined | object | √ | × | √ |
findelndex | number | √ | × | √ |
every | boolean | √ | √ | × |
filter | array | × | × | × |
改变自身的方法
push、pop、unshift、shift
sort、splice、reverse
ES6: copyWithin、fill
let array = [1, 2, 3, 4, 5, 6, 7]; array.push("push"); console.log("array push:", array); array.pop(); console.log("array pop:", array); array.unshift("unshift"); console.log("array unshift:", array); array.shift(); console.log("array shift:", array); array.reverse(); console.log("array reverse:", array); array.sort(); console.log("array sort:", array); array.splice(2, 1); console.log("array splice:", array); array.copyWithin(2, 0); console.log("array copyWithin:", array); array.fill("fill", 3); console.log("array fill:", array);
delete误区
- delete删除数组元素,后面元素不会补齐,delete删除引用
const array = [1, 2, 3, 4, 5]; delete array[2]; console.log("delete array:", array); // delete array: [ 1, 2, <1 empty item>, 4, 5 ]
push vs concat
- 大量数据操作的时候 push 性能会比 concat 性能高很多
const count = 10000; const array1 = [1, 2, 4, 5, 6]; let newArray = []; console.time("push"); for (let i = 0; i < count; i++) { newArray.push(array1[0], array1[1], array1[2], array1[3], array1[4]); } console.timeEnd("push"); console.time("concat"); for (let i = 0; i < count; i++) { newArray = newArray.concat(array1[0], array1[1], array1[2], array1[3], array1[4]); } console.timeEnd("concat");
6.数组的高级用法
1.万能数据生成器
const createValues = (creator, length = 10) => Array.from({ length }, creator); // 第一个参数控制随机数生成,第二个控制其数组长度 const createRandomValues = (len) => createValues(Math.random, len); const values = createRandomValues(); console.log("values:", values.length, values);
2.序列生成器
const createValues = (creator, length = 10) => Array.from({ length }, creator); const createRange = (start, stop, step) => createValues((_, i) => start + i * step, (stop - start) / step + 1); // 生成数组,里面元素是 1 ~ 100 以内每次从 1 开始每次递增 3 的数字 const values = createRange(1, 100, 3); console.log(values);
3.数据生成器
const createValues = (creator, length = 10) => Array.from({ length }, creator); function createUser(v, index) { return { name: `user-${index}`, age: (Math.random() * 100) >> 0, // 取整 }; } const users = createValues(createUser, 100); console.log("users:", users);
4.清空数组
const arr = [1, 2, 3]; arr.splice(0); console.log("splice:", arr); // [] const arr1 = [1, 2, 3]; arr1.length = 0; console.log("length:", arr1); // []
5.数组去重
const arr = [ "apple", "banana", 1, 1, 3, 3, undefined, undefined, , , NaN, NaN, null, null, "true", true, { a: 1 }, ]; const arr1 = Array.from(new Set(arr)); // 正常去重 console.log("set:", arr1);
对于数组里面对象去重
function uniqueArray(arr) { return Array.from(new Set(arr)); } const arr = [{ a: 1 }, { a: 1 }]; console.log("set 不同引用:", uniqueArray(arr)); const obj1 = { a: 1 }; const arr2 = [obj1, obj1]; console.log("set 同一引用:", uniqueArray(arr2));
如果我们想认为两个对象里面的 a 属性的值相同就认为是同一数组的话,可以使用 filter
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; }); } const arr = [{ a: 1 }, { a: 1 }, { a: 2 }]; console.log("filter 去重:", uniqueArray(arr, "a")); // filter 去重: [ { a: 1 }, { a: 2 } ]
6.数组交集
- Array.prototype.filter + includes判断
- 但是会存在性能和引用类型相同的判断的问题
const arr1 = [0, 1, 2]; const arr2 = [3, 2, 0]; function intersectSet(arr1, arr2) { return [...new Set(arr1)].filter((item) => arr2.includes(item)); } const values = intersectSet(arr1, arr2); console.log(values); // [ 0, 2 ]
我们可以这样做:
// 引用类型 function intersect(arr1, arr2, key) { const map = new Map(); arr1.forEach((val) => map.set(val[key])); return arr2.filter((val) => map.has(val[key])); } // 原始数据类型 function intersectBase(arr1, arr2) { const map = new Map(); arr1.forEach((val) => map.set(val)); return arr2.filter((val) => map.has(val)); } const arr1 = [{ p: 0 }, { p: 1 }, { p: 2 }]; const arr2 = [{ p: 3 }, { p: 2 }, { p: 1 }]; const result = intersect(arr1, arr2, "p"); console.log("result:", result); // result: [ { p: 2 }, { p: 1 } ] const arr3 = [0, 1, 2]; const arr4 = [3, 2, 0]; const result1 = intersectBase(arr3, arr4); console.log("result1:", result1); // result1: [ 2, 0 ]
性能比对:
function createData(length) { return Array.from({ length }, (val, i) => { return ~~(Math.random() * length); }); } function intersectSet(arr1, arr2) { return [...new Set(arr1)].filter((item) => arr2.includes(item)); } // 原始数据类型 function intersectMap(arr1, arr2) { const map = new Map(); arr1.forEach((val) => map.set(val)); return arr2.filter((val) => { return map.has(val); }); } console.time("createData"); const data1 = createData(100000); const data2 = createData(100000); console.timeEnd("createData"); console.time("intersectMap"); intersectMap(data1, data2); console.timeEnd("intersectMap"); console.time("intersectSet"); intersectSet(data1, data2); console.timeEnd("intersectSet");
7.数组差集
// 引用类型 function difference(arr1, arr2, key) { const map = new Map(); arr1.forEach((val) => map.set(val[key])); return arr2.filter((val) => !map.has(val[key])); } // 原始数据类型 function differenceBase(arr1, arr2) { const map = new Map(); arr1.forEach((val) => map.set(val)); return arr2.filter((val) => !map.has(val)); } const arr1 = [{ p: 0 }, { p: 1 }, { p: 2 }]; const arr2 = [{ p: 3 }, { p: 2 }, { p: 1 }]; const result = difference(arr1, arr2, "p"); console.log("result:", result); // result: [ { p: 3 } ] const arr3 = [0, 1, 2]; const arr4 = [3, 2, 0]; const result1 = differenceBase(arr3, arr4); console.log("result1:", result1); // result1: [ 3 ]
8.数组删除虚(假)值
const array = [false, 0, undefined, , "", NaN, 9, true, undefined, null, "test"]; const newArray = array.filter(Boolean); console.log(newArray); // [ 9, true, 'test' ]
9.获取数组中最大值和最小值
const numArray = [1, 3, 8, 666, 22, 9982, 11, 0]; const max = Math.max.apply(Math, numArray); const min = Math.min.apply(Math, numArray); console.log("max:", max + ",min:" + min); // max: 9982,min:0 console.log(Math.max(...numArray)); // 9982 console.log(Math.min(...numArray)); // 0
来看一个实际的例子,我们去获取用户对象中最大和最小的年龄:
const createValues = (creator, length = 10) => Array.from({ length }, creator); function createUser(v, index) { return { name: `user-${index}`, age: (Math.random() * 100) >> 0, }; } const users = createValues(createUser, 10); const ages = users.map((u) => u.age); const max = Math.max.apply(Math, ages); const min = Math.min.apply(Math, ages); console.log(ages); console.log("max:", max + ",min:" + min);
10.reduce高级用法
querystring
- 作用∶页面传递参数
- 规律∶地址url问号(?)拼接的键值对
URLSearchParams:
const urlSP = new URLSearchParams(location.search); function getQueryString(key) { return urlSP.get(key); } // 获取页面上查询参数 words 和 wordss 的值 console.log("words:", getQueryString("words")); console.log("wordss:", getQueryString("wordss"));
URL:
const urlObj = new URL(location.href); function getQueryString(key) { return urlObj.searchParams.get(key); } // urlObj.searchParams instanceof URLSearchParams 为 true,证明是其实例 console.log("words:", getQueryString("words")); console.log("wordss:", getQueryString("wordss"));
使用 reduce 手写查询:
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[key]; } console.log("words:", getQueryString("words")); console.log("wordss:", getQueryString("wordss"));
折上折
- 优惠1:9折
- 优惠2:200减50
草民版:
function discount(x) { return x * 0.9; } function reduce(x) { return x > 200 ? x - 50 : x; } const print = console.log; // 享受九折 print(reduce(discount(100))); // 90 // 享受九折 + 满减 print(reduce(discount(250))); // 175
黄金版:
function discount(x) { return x * 0.9; } function reduce(x) { return x > 200 ? x - 50 : x; } function getPriceMethod(discount, reduce) { return function _getPrice(x) { return reduce(discount(x)); }; } const method = getPriceMethod(discount, reduce); const print = console.log; print(method(100)); print(method(250));
王者版:
function compose(...funcs) { if (funcs.length === 0) { return (arg) => arg; } return funcs.reduce( (a, b) => (...args) => a(b(...args)) ); } function discount(x) { console.log("discount"); return x * 0.9; } function reduce(x) { console.log("reduce"); return x > 200 ? x - 50 : x; } function discountPlus(x) { console.log("discountPlus"); return x * 0.95; } // 从后往前执行传入的函数 const getPrice = compose(discountPlus, reduce, discount); const print = console.log; print(getPrice(200)); print(getPrice(250));
打印结果如下图:
Promise顺序执行
function runPromises(promiseCreators, initData) { return promiseCreators.reduce(function (promise, next) { return promise.then((data) => next(data)); }, Promise.resolve(initData)); } function login(data) { console.log("login: data", data); return new Promise((resolve) => { setTimeout(() => { return resolve({ token: "token", }); }, 500); }); } function getUserInfo(data) { console.log("getUserInfo: data", data); return new Promise((resolve) => { setTimeout(() => { return resolve({ name: "user-1", id: 988, }); }, 300); }); } function getOrders(data) { console.log("getOrders: data", data); return new Promise((resolve) => { setTimeout(() => { return resolve([ { orderId: 1, productId: 100, price: 100, }, ]); }, 100); }); } const initData = { name: "name", pwd: "pwd" }; Promise.resolve(initData) .then((data) => login(data)) .then((data) => getUserInfo(data)) .then((data) => getOrders(data)) .then((data) => console.log("orders", data)); // 使用 reduce 封装的 runPromises 方法,确保返回 Promise 且执行结果是下一个函数的入参 runPromises([login, getUserInfo, getOrders], initData).then((res) => { console.log("res", res); });
数组分组
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; }, {}); } // 按照长度分组 let result = group(["apple", "pear", "orange", "peach"], (v) => v.length); console.log(result); // 按照份数分组 result = group( [ { name: "tom", score: 60, }, { name: "Jim", score: 40, }, { name: "Nick", score: 88, }, ], (v) => v.score >= 60 ); console.log(result);
打印结果如下:
7.手写数组方法
Array.isArray
- 判断是否是数组
const arr = ["1"]; console.log("isArray:", Array.isArray(arr));
非基本使用:
const arr = ["1"]; const proxy = new Proxy(arr, {}); console.log("isArray:", Array.isArray(proxy)); // true
为什么上面 Array.isArray 判断代理对象是否数组返回 true 呢?
const arr = ["1"]; const proxy = new Proxy(arr, {}); const log = console.log; log("__proto__:", proxy.__proto__ === Array.prototype); // __proto__: true log("instanceof:", proxy instanceof Array); // instanceof: true log("toString", Object.prototype.toString.call(Proxy)); // toString [object Function] log("Proxy.prototype:", Proxy.prototype); // Proxy.prototype: undefined log("proxy instanceof Proxy:", proxy instanceof Proxy); // 报错
实际 Array.isArray 判断的是 Proxy里面的 target 属性
接下来我们真正手写下 Array.isArray 的方法
- Object.prototype.toString
Array.isArray = function (obj) { return Object.prototype.toString.call(obj) === "[object Array]"; }; const arr = ["1"]; const proxy = new Proxy(arr, {}); console.log(Array.isArray(arr)); console.log(Array.isArray(proxy));
- instanceof
Array.isArray = function (obj) { if (typeof obj !== "object" || obj === null) { return false; } return obj instanceof Array; }; const arr = ["1"]; const proxy = new Proxy(arr, {}); console.log(Array.isArray(arr)); console.log(Array.isArray(proxy));
其实还有很多方法可以判断其数据类型,比如 constructor、isPrototypeOf等,不过我还是更推荐上面两种
Array.prototype.entries
- 作用:返回一个新的 Array Iterator 对象,该对象包含数组中每个索引的键/值对
const arr = ["a", "b", "c"]; const iter = arr.entries(); console.log("iter:", iter); // next函数访问 console.log("iter.next():", iter.next()); console.log("iter.next():", iter.next()); console.log("iter.next():", iter.next()); console.log("iter.next():", iter.next()); // for of迭代 for (let [k, v] of arr.entries()) { console.log(k, v); }
打印结果如下:
done 表示遍历是否结束,value 返回当前遍历的值
自己来实现下这个方法:
Array.prototype.entries = function () { // 转换对象(引用数据类型返回自身) const O = Object(this); let index = 0; const length = O.length; return { next() { if (index < length) { return { value: [index, O[index++]], done: false }; } return { value: undefined, done: true }; }, }; }; const arr = ["a", "b", "c"]; const iter = arr.entries(); console.log("iter.next():", iter.next()); console.log("iter.next():", iter.next()); console.log("iter.next():", iter.next()); // 不能正常执行,因为如果要能 for...of 遍历需要去实现 Symbol.iterator for (let [k, v] of arr.entries()) { console.log(`k:${k}`, `v:${v}`); }
下面添加 Symbol.iterator 方法返回 next 即可for...of
Array.prototype.entries = function () { const O = Object(this); let index = 0; const length = O.length; function next() { if (index < length) { return { value: [index, O[index++]], done: false }; } return { value: undefined, done: true }; } return { next, [Symbol.iterator]() { return { next, }; }, }; };
数组还有 Array.prototype.keys,Array.prototype.keys,如果我们像上面这样写等于每个方法里面都要实现[Symbol.iterator],我们可以抽离其逻辑,代码如下:
Array.prototype[Symbol.iterator] = function () { const O = Object(this); let index = 0; const length = O.length; function next() { if (index < length) { return { value: O[index++], done: false }; } return { value: undefined, done: true }; } return { next, }; }; Array.prototype.entries = function () { const O = Object(this); const length = O.length; let entries = []; for (let i = 0; i < length; i++) { entries.push([i, O[i]]); } const itr = this[Symbol.iterator].bind(entries)(); return { next: itr.next, [Symbol.iterator]() { return itr; }, }; }; Array.prototype.keys = function () { const O = Object(this); const length = O.length; let keys = []; for (let i = 0; i < length; i++) { keys.push([i]); } const itr = this[Symbol.iterator].bind(keys)(); return { next: itr.next, [Symbol.iterator]() { return itr; }, }; }; Array.prototype.values = function () { const O = Object(this); const length = O.length; let keys = []; for (let i = 0; i < length; i++) { keys.push([O[i]]); } const itr = this[Symbol.iterator].bind(keys)(); return { next: itr.next, [Symbol.iterator]() { return itr; }, }; }; const arr = ["a", "b", "c"]; var iter = arr.entries(); console.log("iter.next().value:", iter.next().value); console.log("iter.next().value:", iter.next().value); console.log("iter.next().value:", iter.next().value); for (let [k, v] of arr.entries()) { console.log(`k:${k}`, `v:${v}`); } var iter = arr.keys(); console.log("iter.next().value:", iter.next().value); console.log("iter.next().value:", iter.next().value); console.log("iter.next().value:", iter.next().value); for (let k of arr.keys()) { console.log(`k:${k}`); } var iter = arr.values(); console.log("iter.next().value:", iter.next().value); console.log("iter.next().value:", iter.next().value); console.log("iter.next().value:", iter.next().value); for (let k of arr.values()) { console.log(`k:${k}`); }
Array.prototype.includes
- 判断数组是否含有某值,可判断NaN
const arr = [1, 2, 3, { a: 1 }, null, undefined, NaN, ""]; console.log("includes null:", arr.includes(null)); // includes null: true console.log("indexOf null:", arr.indexOf(null)); // indexOf null: 4 console.log("includes NaN:", arr.includes(NaN)); // includes NaN: true console.log("indexOf NaN:", arr.indexOf(NaN)); // indexOf NaN: -1
手写该方法
Number.isNaN = function (param) { if (typeof param === "number") { return isNaN(param); } return false; }; Array.prototype.includes = function (item, fromIndex) { // call, apply调用,严格模式 if (this == null) { throw new TypeError("无效的this"); } let O = Object(this); let len = O.length >> 0; if (len <= 0) { return false; } const isNAN = Number.isNaN(item); for (let i = 0; i < len; i++) { if (O[i] === item) { return true; } else if (isNAN && Number.isNaN(O[i])) { return true; } } return false; }; const obj = { a: 3 }; const arr = [1, 2, 3, { a: 1 }, null, undefined, NaN, "", 0, obj, obj]; console.log("includes null:", arr.includes(null)); console.log("includes NaN:", arr.includes(NaN));
其实 includes 还有第二个参数,表示从哪个下标开始检查,我们也来写写该方法
注意参数的情况
- 转为整数:TolntegerOrlnfinity
- +lnfinity , -Infinity
- 可能为负数
Number.isNaN = function (params) { if (typeof params === "number") { return isNaN(params); } return false; }; // 转换整数 function ToIntegerOrInfinity(argument) { const num = Number(argument); // NaN 和 +0、-0 if (Number.isNaN(num) || num == 0) { return 0; } if (num === Infinity || num == -Infinity) { return num; } let inter = Math.floor(Math.abs(num)); if (num < 0) { inter = -inter; } return inter; } Array.prototype.includes = function (item, fromIndex) { // 严格模式 if (this == null) { throw new TypeError("无效的this"); } const O = Object(this); const len = O.length >> 0; if (len <= 0) { return false; } let n = ToIntegerOrInfinity(fromIndex); if (fromIndex === undefined) { n = 0; } if (n === +Infinity) { return false; } // 负无穷转换为0 if (n === -Infinity) { n = 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 (O[i] === item) { return true; } else if (isNAN && Number.isNaN(O[i])) { return true; } } return false; }; const arr = ["a", "b", "c"]; console.log("arr include -100->0:", arr.includes("c", -100)); // true console.log("arr include -100->0:", arr.includes("a", -1)); // false console.log("arr include 1:", arr.includes("a", -Infinity)); // true console.log("arr include 1:", arr.includes("a", Infinity)); // false
Array.from
有三个参数
- arrayLike:类数组对象或者可遍历对象(Map、Set)等
- mapFn:可选参数,在最后生成数组后执行一次map方法后返回
- thisArg:可选参数,实际是Array.from(obj).map(mapFn, thisArg)
特殊值处理
console.log("Array.from1:", Array.from({})); console.log("Array.from2:", Array.from("")); console.log("Array.from3:", Array.from({ a: 1, length: "10" })); console.log("Array.from4:", Array.from({ a: 1, length: "ss" })); console.log("Array.from5:", Array.from([NaN, null, undefined, 0])); // 长度极限问题 // const max = Math.pow(2, 32); // console.log("Array.from:", Array.from({ 0: 1, 1: 2, length: max - 1 })); // 极限 // console.log("Array.from:", Array.from({ 0: 1, 1: 2, length: max })); // 失败
执行结果如下:
自己实现一个:
//类数组的特征 let maxSafeInteger = Math.pow(2, 32) - 1; let ToIntegerOrInfinity = function (value) { let 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)); }; let ToLength = function (value) { let len = ToIntegerOrInfinity(value); return Math.min(Math.max(len, 0), maxSafeInteger); }; let isCallable = function (fn) { return typeof fn === "function" || toStr.call(fn) === "[object Function]"; }; Array.from = function (arrayLike, mapFn, thisArg) { let C = this; //判断对象是否为空 if (arrayLike == null) { throw new TypeError("Array.from requires an array-like object - not null or undefined"); } //检查mapFn是否是方法 if (typeof mapFn !== "function" && typeof mapFn !== "undefined") { throw new TypeError(mapFn + "is not a function"); } let items = Object(arrayLike); //判断 length 为数字,并且在有效范围内。 let len = ToLength(items.length); if (len <= 0) return []; let A = isCallable(C) ? Object(new C(len)) : new Array(len); for (let i = 0; i < len; i++) { let value = items[i]; if (mapFn) { A[i] = typeof thisArg === "undefined" ? mapFn(value, i) : mapFn.call(thisArg, value, i); } else { A[i] = value; } } return A; }; console.log("Array.from1:", Array.from({ a: 1, length: "10" })); console.log("Array.from2:", Array.from({ a: 1, length: "ss" })); console.log( "Array.from3:", Array.from({ 0: 1, 1: 2, 4: 5, length: 4 }, (x) => x + x) ); function MyArray(length) { const len = length * 2; return new Array(len); } function MyObject(length) { return { length, }; } console.log("Array.from:MyArray", Array.from.call(MyArray, { length: 5 })); console.log("Array.from:MyObject", Array.from.call(MyObject, { length: 5 }));
打印结果如下:
Array.prototype.flat
- 指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回
const array = [1, 3, 4, [4, 5], [6, [7, 8]], [, ,], [undefined, null, NaN]]; console.log("flat 1:", array.flat(1)); console.log("flat 2:", array.flat(2));
执行结果如下:
reduce + 递归
const array = [1, [1, , ,]]; const flat = (arr) => { return arr.reduce((pre, cur) => { return pre.concat(Array.isArray(cur) ? flat(cur) : cur); }, []); }; console.log(flat(array)); // [ 1, 1 ]
上面的实现存在几个弊端:
- 无法指定躺平深度
- 性能差的一批(递归 + concat)
- 丢数据(空值reduce无法遍历)
正规军入场:
let has = Object.prototype.hasOwnProperty; let maxSafeInteger = Math.pow(2, 32) - 1; let toInteger = 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)); }; let toLength = function (value) { let len = toInteger(value); return Math.min(Math.max(len, 0), maxSafeInteger); }; let push = Array.prototype.push; Array.prototype.flat = function (deep) { let O = Object(this); let sourceLen = toLength(O.length); let depthNum = 1; if (deep !== undefined) { depthNum = toLength(deep); } if (depthNum <= 0) { return O; } let arr = []; let val; for (let i = 0; i < sourceLen; i++) { if (has.call(O, i)) { val = O[i]; if (Array.isArray(val)) { push.apply(arr, val.flat(depthNum - 1)); } else { arr.push(val); } } else { arr.push(undefined); } } return arr; }; let array = [1, 3, [4, 5], [6, [7, 8, [9, , 10]]], [, ,], [undefined, null, NaN]]; console.log(array.flat(2));
打印结果如下:
8.实战:数组合并
准备好两条数据,对 uid 相同的数据进行合并
export const usersInfo = Array.from({ length: 200 }, (val, index) => { return { uid: `${index + 1}`, name: `user-name-${index}`, age: index + 10, avatar: `http://www.my-avatar.com/${index + 1}`, }; }); export const scoresInfo = Array.from({ length: 10 }, (val, index) => { return { uid: `${index + 1}`, score: ~~(Math.random() * 10000), comments: ~~(Math.random() * 10000), stars: ~~(Math.random() * 1000), }; });
基础版本:
- 两层for循环,通过key关联
import * as data from "./data.js"; const { usersInfo, scoresInfo } = data; console.time("merge data"); for (let i = 0; i < usersInfo.length; i++) { let user: any = usersInfo[i]; for (let j = 0; j < scoresInfo.length; j++) { let score = scoresInfo[j]; if (user.uid == score.uid) { user.score = score.score; user.comments = score.comments; user.stars = score.stars; } } } console.timeEnd("merge data"); console.log(usersInfo);
hash基础版:
- 数组转换为map对象。数组查找变为属性查找
import * as data from "./data.js"; const { usersInfo, scoresInfo } = data; console.time("merge data"); const scoreMap = scoresInfo.reduce((obj, cur) => { obj[cur.uid] = cur; return obj; }, Object.create(null)); for (let i = 0; i < usersInfo.length; i++) { const user: any = usersInfo[i]; const score = scoreMap[user.uid]; if (score != null) { user.score = score.score; user.comments = score.comments; user.stars = score.stars; } } console.timeEnd("merge data"); console.log(usersInfo);
hash跳出版:
import * as data from "./data.js"; const { usersInfo, scoresInfo } = data; console.time("merge data"); const scoreMap = scoresInfo.reduce((obj, cur) => { obj[cur.uid] = cur; return obj; }, Object.create(null)); // 被合并数据的条数 const len = scoresInfo.length; // 已合并的条数 let count = 0; // 已遍历的次数 let walkCount = 0; for (let i = 0; i < usersInfo.length; i++) { const user: any = usersInfo[i]; const score = scoreMap[user.uid]; walkCount++; if (score != null) { count++; user.score = score.score; user.comments = score.comments; user.stars = score.stars; if (count >= len) { break; } } } console.timeEnd("merge data"); console.log(`合并完毕:遍历次数${walkCount}, 实际命中次数${count}, 预期命中次数${len}`); console.log(usersInfo);
数据合并-基础 hash 跳出-倒叙版
- 在跳出版的基础上,一个是从前向后,一个是从后往前
- 适应场景∶分页拉取数据,新数组添加在最后,倒叙更快
import * as data from "./data.js"; const { usersInfo, scoresInfo } = data; console.time("merge data"); const scoreMap = scoresInfo.reduce((obj, cur) => { obj[cur.uid] = cur; return obj; }, Object.create(null)); const len = scoresInfo.length; let count = 0; let walkCount = 0; for (let i = usersInfo.length - 1; i >= 0; i--) { const user: any = usersInfo[i]; const score = scoreMap[user.uid]; walkCount++; if (score != null) { count++; user.score = score.score; user.comments = score.comments; user.stars = score.stars; if (count >= len) { break; } } } console.timeEnd("merge data"); console.log(`合并完毕:遍历次数${walkCount}, 实际命中次数${count}, 预期命中次数${len}`); console.log(usersInfo);
加载全部内容