先复习下数组的常用方法:JavaScript相关细节题 | QT-7274 (qblog.top)
排序类
排序算法 | 最好情况 | 平均情况 | 最坏情况 | 空间复杂度 | 是否稳定 |
---|---|---|---|---|---|
快速排序 | O(n log n) | O(n log n) | O(n^2) | O(log n) | 不稳定 |
冒泡排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序 | O(n) | O(n^2) | O(n^2) | O(1) | 稳定 |
归并排序 | O(n log n) | O(n log n) | O(n log n) | O(n) | 稳定 |
基数排序 | O(nk) | O(nk) | O(nk) | O(n + k) | 稳定 |
计数排序 | O(n + k) | O(n + k) | O(n + k) | O(k) | 稳定 |
桶排序 | O(n + k) | O(n + k) | O(n^2) | O(n + k) | 稳定 |
希尔排序 | O(n log n) | O(n(log n)^2) | O(n(log n)^2) | O(1) | 不稳定 |
堆排序 | O(n log n) | O(n log n) | O(n log n) | O(1) | 不稳定 |
快速排序
快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
快速排序的最坏运行情况是 O(n²),比如说顺序数列的快排。但它的平摊期望时间是 O(nlogn),且 O(nlogn) 记号中隐含的常数因子很小,比复杂度稳定等于 O(nlogn) 的归并排序要小很多。所以,对绝大多数顺序性较弱的随机数列而言,快速排序总是优于归并排序。
对快速排序进行优化:
- 三数取中:在选择基准元素时,使用数组中间、头部和尾部的三个元素的中位数作为基准元素。
- 插入排序:在数组的长度小于某个值(如10)时,使用插入排序算法而不是快速排序。插入排序在处理小数组时比快速排序更快。
- 随机化数组:在每次执行快速排序时,随机打乱数组,以增加算法的随机性。
- 尾递归优化:使用尾递归优化快速排序的实现,避免栈溢出。
算法步骤
- 从数列中挑出一个元素,称为 "基准" ;
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序;
代码实现
答案
/** * --- 测试用例 --- * * 输入:[1, 34, 5, 76, 8, 6, 9, 7, 6, 3] * 输出:[1, 3, 5, 6, 6, 7, 8, 9, 34, 76] * * --- 说明 --- * * 思考:快速排序是稳定的吗?稳定性是指如果两个元素的相对顺序在排序前后保持不变,那么这个排序算法就是稳定的。 * 解答:base 的每次选择,会导致快排是不稳定排序。 */ const quickSort = (nums) => { // 当数组被划分到只剩下一个或没有元素时,终止递归 if (nums.length < 2) { return nums; } else { var left = []; var right = []; var pivot = Math.floor(nums.length / 2); // Math.floor 向下取整,计算出基准值的索引 var base = nums.splice(pivot, 1)[0]; // 从数组中移除基准值,保存在变量 base 中(注意 splice 返回一个数组) for (let i = 0; i < nums.length; i++) { if (nums[i] < base) { left.push(nums[i]); // 小于基准值的去左边 } else { right.push(nums[i]); // 大于基准值的去右边 } } } return quickSort(left).concat([base], quickSort(right)); // 连接数组 }
注意:
- splice方法返回的是一个数组,基准值是一个数字。
- 拼接的是基准值所在数组,而不是基准值本身。
冒泡排序
冒泡排序(Bubble Sort)也是一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮"到数列的顶端。
算法步骤
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
- 针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
代码实现
答案
/** * --- 测试用例 --- * * 输入:[5, 2, 4, 7, 9, 8, 3, 6, 3, 8, 3] * 输出:[2, 3, 3, 3, 4, 5, 6, 7, 8, 8, 9] * * --- 说明 --- * * 思考:冒泡排序是稳定的吗? * 解答:稳定。相等的元素不发生交换 */ const bubbleSort = (nums) => { for (var i = 0; i < nums.length - 1; i++) { for (var j = 0; j < nums.length - 1 - i; j++) { if (nums[j] > nums[j + 1]) { let tmp = nums[j]; nums[j] = nums[j + 1]; nums[j + 1] = tmp; } } } return nums; }
注意:交换是 nums[i] 和 nums[j+1] 进行交换,而不是 nums[i] 和 nums[j]
选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
算法步骤
- 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
- 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
- 重复第二步,直到所有元素均排序完毕。
代码实现
答案
/** * --- 测试用例 --- * * 输入:[6, 45, 3, 2, 5, 6, 8, 4, 3, 4, 56, 67, 5] * 输出:[2, 3, 3, 4, 4, 5, 5, 6, 6, 8, 45, 56, 67] * * --- 说明 --- * * 思考:选择排序是稳定的吗? * 解答:要看代码是如何实现的,在本例中由于有交换,所以是不稳定排序。 */ const selectSort = (nums) => { var idx; // 最小值的索引 for (var i = 0; i < nums.length - 1; i++) { idx = i; // 从 i 后的元素开始比较,遍历完数组后锁定最小元素对应的索引 for (var j = i + 1; j < nums.length; j++) { if (nums[j] < nums[idx]) { idx = j; } } // 判断 i 和 i 之后元素中的这个最小元素的大小关系 if (nums[i] > nums[idx]) { let tmp = nums[idx]; nums[idx] = nums[i]; nums[i] = tmp; } } return nums; }
注意:
- 外层循环终止为
i < nums.length - 1;
因为剩下最后一个元素时就不用排序了;但是内层循环终止为j < nums.length;
因为内层需要比较 i 之后的每一个元素,包括最后一个元素; - 注意判断的符号方向,例如要将最小值置换到最前。
- 外层循环终止为
插入排序
算法步骤
- 从第一个元素开始,将其视为已排序部分。
- 取出下一个元素,在已排序部分中从后往前进行比较。
- 如果已排序部分中的元素大于当前元素,将该元素向后移动一个位置,为当前元素腾出插入位置。
- 重复步骤3,直到找到已排序部分中的元素小于或等于当前元素。
- 将当前元素插入到已排序部分中的合适位置。
- 重复步骤2到步骤。
- 直到所有元素都被插入到正确的位置。
- 时间复杂度O(n^2)
代码实现
答案
function insertionSort(arr){ for(let i = 1; i < arr.length; i++){ const temp = arr[i] // 待插入的元素 let index = i // 插入位置的索引 while(index > 0){ if(arr[index - 1] > temp) // 已排序部分的当前元素大于 temp { arr[index] = arr[index - 1] // 当前腾出插入位置 }else{ break; // 跳出内层循环,此时index为待插入元素的位置 } index-- } arr[index] = temp } return arr }
注意:
- 插入排序是从后面未排序的元素中选择一个,与已排序的元素全部比较一次,注意 index - 1;
- 外层循环终止
i < nums.length;
,因为要从后续所有元素中选择插入元素,哪怕剩下最后一个元素,也仍需要和前面所有已排序的元素比较一次。
归并排序
算法步骤
- 将待排序的数组不断二分,直到每个子数组只有一个元素为止。
- 将相邻的子数组进行合并,合并过程中按照从小到大的顺序将元素放入新的数组中。
- 重复上述合并过程,直到所有子数组合并为一个完整的有序数组。
- 分的时间复杂度O(logN),并的时间复杂度O(n),总体时间复杂度是O(nlogN)
代码实现
答案
function mergeSort(arr) { if (arr.length <= 1) { return arr; } const mid = Math.floor(arr.length / 2); const left = arr.slice(0, mid); const right = arr.slice(mid); return merge(mergeSort(left), mergeSort(right)); } function merge(left, right) { let result = []; let i = 0; let j = 0; while (i < left.length && j < right.length) { if (left[i] < right[j]) { result.push(left[i]); i++; } else { result.push(right[j]); j++; } } // 如果left数组的元素已全部添加到result数组中,则将right数组中剩余的元素添加到result数组中。 反之亦然。 return result.concat(left.slice(i)).concat(right.slice(j)); }
基数排序
算法步骤
- 确定待排序数组中的最大值,并计算出最大值的位数。
- 创建10个空桶(0到9),用于存放按照当前位数进行排序的数字。
- 从最低位开始,对待排序数组中的每个数字进行遍历。
- 对每个数字,根据当前位数的值将其放入相应的桶中。
- 按照桶的顺序,依次取出桶中的数字,形成一个新的数组。
- 重复步骤3到步骤5,依次对更高位进行排序,直到最高位。
- 经过多轮的排序后,待排序数组将变成有序的。
代码实现
答案
// 获取数字的某一位上的值 function getDigit(num, place) { return Math.floor(Math.abs(num) / Math.pow(10, place)) % 10; } // 获取数字的最大位数 function getMaxDigits(arr) { let max = 0; for (let i = 0; i < arr.length; i++) { max = Math.max(max, Math.floor(Math.log10(Math.abs(arr[i]))) + 1); } return max; } // 基数排序 function radixSort(arr) { const maxDigits = getMaxDigits(arr); for (let i = 0; i < maxDigits; i++) { let buckets = Array.from({ length: 10 }, () => []); for (let j = 0; j < arr.length; j++) { const digit = getDigit(arr[j], i); buckets[digit].push(arr[j]); } arr = [].concat(...buckets); } return arr; }
计数排序
算法步骤
- 首先,确定待排序数组中的最大值max和最小值min。
- 创建一个计数数组count,长度为max-min+1,并将所有元素初始化为0。
- 遍历待排序数组,统计每个元素出现的次数,并在计数数组中相应位置进行累加。
- 对计数数组进行前缀和操作,即将每个位置的值更新为前面所有位置值的累加和。
- 创建一个与待排序数组长度相同的临时数组temp。
- 从待排序数组末尾开始,遍历每个元素,根据其值在计数数组中的位置,将其放入临时数组temp中,并将计数数组中对应位置的值减1。
- 将临时数组temp中的元素复制回待排序数组,排序完成。
代码实现
答案
function countingSort(arr) { // 确定最大值和最小值 let max = Math.max(...arr); let min = Math.min(...arr); // 创建计数数组并初始化为0 let count = new Array(max - min + 1).fill(0); // 统计元素出现的次数 for (let i = 0; i < arr.length; i++) { count[arr[i] - min]++; } // 对计数数组进行前缀和操作 for (let i = 1; i < count.length; i++) { count[i] += count[i - 1]; } // 创建临时数组 let temp = new Array(arr.length); // 将元素放入临时数组中 for (let i = arr.length - 1; i >= 0; i--) { temp[--count[arr[i] - min]] = arr[i]; } // 将临时数组复制回原数组 for (let i = 0; i < arr.length; i++) { arr[i] = temp[i]; } return arr; } // 示例用法 let arr = [5, 2, 9, 1, 5, 6, 3]; console.log("排序前:", arr); arr = countingSort(arr); console.log("排序后:", arr);
希尔排序
算法步骤
希尔排序(Shell Sort)是一种改进的插入排序算法,它通过将待排序的数组分割成多个子序列来进行排序。以下是希尔排序的算法步骤:
- 首先,确定一个增量(increment)序列,通常以递减的方式生成。常用的增量序列是希尔增量序列,如n/2、n/4、n/8...直到增量为1。
- 根据确定的增量,将待排序数组分割成多个子序列。每个子序列包含相距为增量的元素。
- 对每个子序列进行插入排序。可以使用任何插入排序算法,如简单插入排序或二分插入排序。
- 逐渐减小增量,重复步骤2和步骤3,直到增量为1。
- 最后,使用增量为1的插入排序对整个数组进行最后一次排序。
代码实现
答案
// 希尔排序 function shellSort(arr) { const len = arr.length; let gap = Math.floor(len / 2); while (gap > 0) { for (let i = 1; i < arr.length; i++) { const temp = arr[i] // 待插入的元素 let index = i // 插入位置的索引 while (index > 0) { if (arr[index - 1] > temp) // 已排序部分的当前元素大于 temp { arr[index] = arr[index - 1] // 当前腾出插入位置 } else { break; // 跳出内层循环,此时index为待插入元素的位置 } index-- } arr[index] = temp } gap = Math.floor(gap / 2); } return arr; }
注意:希尔排序其实就是改进版的插入排序。
桶排序
算法步骤
- 确定桶的数量和范围:根据待排序数组的特性,确定需要的桶的数量。通常情况下,桶的数量可以选择为待排序数组的长度。同时,确定每个桶所能容纳元素的范围。
- 将元素分配到桶中:遍历待排序数组,根据元素的值将其分配到对应的桶中。
- 对每个桶进行排序:对每个桶中的元素进行排序。可以使用任何排序算法,如插入排序、快速排序等。
- 合并桶中的元素:将排序后的每个桶中的元素按照顺序合并,形成一个有序的数组。
- 返回排序后的数组。
代码实现
答案
function bucketSort(arr, bucketSize = 5) { if (arr.length === 0) { return arr; } // 确定最大值和最小值 let minValue = arr[0]; let maxValue = arr[0]; for (let i = 1; i < arr.length; i++) { if (arr[i] < minValue) { minValue = arr[i]; } else if (arr[i] > maxValue) { maxValue = arr[i]; } } // 计算桶的数量 const bucketCount = Math.floor((maxValue - minValue) / bucketSize) + 1; const buckets = new Array(bucketCount); for (let i = 0; i < buckets.length; i++) { buckets[i] = []; } // 将元素分配到桶中 for (let i = 0; i < arr.length; i++) { const bucketIndex = Math.floor((arr[i] - minValue) / bucketSize); buckets[bucketIndex].push(arr[i]); } // 对每个桶进行排序,并合并结果 const sortedArr = []; for (let i = 0; i < buckets.length; i++) { if (buckets[i].length > 0) { insertionSort(buckets[i]); // 可以使用任何排序算法对桶内的元素进行排序 sortedArr.push(...buckets[i]); } } return sortedArr; } // 辅助函数:插入排序 function insertionSort(arr) { for (let i = 1; i < arr.length; i++) { const current = arr[i]; let j = i - 1; while (j >= 0 && arr[j] > current) { arr[j + 1] = arr[j]; j--; } arr[j + 1] = current; } } // 示例用法 const arr = [53, 89, 150, 36, 633, 233, 99]; console.log("排序前:", arr); const sortedArr = bucketSort(arr); console.log("排序后:", sortedArr);
堆排序
算法步骤
- 构建最大堆:将待排序的数组视为一个完全二叉树,并从最后一个非叶子节点开始,从右至左进行下沉操作,使得每个节点的值都大于或等于其子节点的值。这样就构建了一个最大堆。
- 堆排序:将最大堆中的根节点(最大值)与堆中的最后一个元素交换位置,然后将堆的大小减1。接着对交换后的根节点进行下沉操作,使得新的根节点再次成为最大值。重复这个过程,直到堆的大小为1,即完成了堆排序。
- 堆排序的关键在于构建最大堆和进行下沉操作。下沉操作是通过将当前节点与其子节点进行比较,如果当前节点小于其子节点,则交换它们的位置,并继续向下比较,直到满足最大堆的条件。
- 堆排序的时间复杂度为O(nlogn),其中n是待排序数组的长度。
代码实现
答案
//nums为要排序的数组,维护堆,使其保证为大顶堆 function heapify(heap, i, len) { let max = i // 大顶堆的顶点下标 let left = 2 * i + 1 // 左孩子的下标 let right = 2 * i + 2 // 右孩子的下标 // 在保证下标合法的情况下,通过和左右孩子比较,找出最大的顶点下标 if (left < len && heap[left] > heap[max]) { max = left } if (right < len && heap[right] > heap[max]) { max = right } // 说明有一个孩子比顶点大 if (max !== i) { [heap[i], heap[max]] = [heap[max], heap[i]] // 交换后仍要维护之后的节点 heapify(heap, max, len) } } //第一步 建立最大堆(升序)从最后一个非叶子节点开始 // 父节点:(i - 1) / 2 取整;左孩子:(i * 2) + 1,右孩子:(i * 2) + 2 for (let i = Math.floor(nums.length / 2) - 1; i >= 0; i--) { heapify(nums, i, nums.length) } //第二步 排序 for (let i = nums.length - 1; i >= 0; i--) { [nums[0], nums[i]] = [nums[i], nums[0]] //重新维护 heapify(nums, 0, i) }
URL操作类
URL参数拆解
代码
答案
split分割
/** * --- 题目描述 --- * * 实现一个函数,可以对 url 中的 query 部分做拆解,返回一个 key: value 形式的 object * * --- 实例 --- * * 输入:'http://sample.com/?a=1&e&b=2&c=xx&d#hash' * 输出:{a: 1, b: 2, c: 'xx', d: ''} */ function getQueryObj(url) { // 将传入的URL按照'?'进行分割,取分割后的第二部分(即?到结尾之间的字符串) // 在第二部分中再按照'#'进行分割,取分割后的第一部分(即去掉hash) // 将第一部分按照'&'进行分割,得到一个数组arr,每个元素都是一个查询参数 let arr = url.split('?')[1].split('#')[0].split('&'); const res = {}; arr.forEach(e => { const [key, value] = e.split('='); // 将每个元素按照'='分割,得到一个键和一个值 if (!value) { // 可加入对对象数组的判断 res[key] = ''; } else { res[key] = value; } }) return res; }
正则表达式
function getURLParams(url) { let params = {}; url = url.split('#')[0]; url.replace(/[?&]+([^=&]*)(=([^&]*)?)?/gi, function(match, equals,key, value) { // 匹配到的整个字符串 键 值 //[?&]+:匹配一个或多个 ? 或 &。[?&] 是一个字符集,匹配其中的任意一个字符,+ 表示匹配一次或多次。 // ([^=&]+):匹配一个或多个非 = 和 & 的字符。[^=&] 是一个反向字符集,匹配除 = 和 & 以外的任意一个字符,+ 表示匹配一次或多次。这部分匹配的是查询参数的键。 // (=([^&]*)?)? 这个表达式的意思是,匹配一个等号和它后面的值,这个部分可以完全不出现。这用于处理 URL 查询参数可能没有等号和值的情况。 // 如果正则表达式中包含了捕获组(用圆括号 () 包围的部分),那么在替换函数中,这些捕获组的内容会作为额外的参数传递给替换函数。 key = decodeURIComponent(key); // 如果查询参数中有一个键是 hello%20world,直接使用这个键,我们会得到 hello%20world,而不是正确的 hello world。但是,如果我们使用 decodeURIComponent 函数解码这个键,就可以得到正确的 hello world。 value = equals ? decodeURIComponent(value) : ''; if (params[key]) { // 在 URL 查询参数中,一个键可能对应一个值,也可能对应多个值。当一个键对应多个值时,通常我们会将这些值存储在一个数组中 if(Array.isArray(params[key])) { params[key].push(value); } else { params[key] = [params[key], value]; // 假设 params[key] 已经是一个数组,如 ['value1', 'value2'],如果你直接使用 params[key] = [params[key], value];,那么 params[key] 将变为 [['value1', 'value2'], 'value3'],而不是我们期望的 ['value1', 'value2', 'value3']。 } } else { params[key] = value; } }); return params; } let url = "http://example.com/?param1=abc¶m2=def"; console.log(getURLParams(url)); // 输出: { param1: 'abc', param2: 'def' }
暴力解法
let search = window.location.search; let search = "?ie=utf-8&f=8&f=9&rsv_bp=1&tn=44004473_16_oem_dg&wd=mdn%20%E6%80%8E%E4%B9%88%E5%AF%B9%E4%B8%AD%E6%96%87%E8%BF%9B%E8%A1%8Cencode&oq=window.location.search&rsv_pq=80a41f180011305d&rsv_t=c725ldO%2FogrEf83WjL5vA%2B7sQvsvDoA4r8QtXgPXRrlH3PWNenJKQPppP95KZy5IamKx0YmFtVEg&rqlang=cn&rsv_dl=tb&rsv_enter=1&rsv_sug3=31&rsv_sug1=3&rsv_sug7=100&rsv_sug2=0&rsv_btype=t&inputT="; search = search.slice(1); let obj = {}; const arr = search.split("&"); arr.forEach((item) => { let [key, val] = item.split("="); key = decodeURIComponent(key); val = decodeURIComponent(val); if (obj[key]) { if(Array.isArray(obj[key])) { obj[key].push(val); } else { obj[key] = [obj[key], val]; } } else { obj[key] = val; } }); console.log(obj);
字符串类
实现一个字符串匹配算法indexOf
答案
function myIndexOf(str1,str2) { if (str2.length <= 0) return -1 const shortStr = str1.length >= str2.length ? str2 : str1 const longStr = shortStr === str1 ? str2 : str1 firstChar = shortStr[0] let left = 0 for (var i = 0; i < longStr.length; i++) { // 如果当前字符与 firstChar 相等,将 left 更新为当前索引位置 i ,以记录目标字符串在长字符串中的起始索引。 left = longStr[i] === firstChar ? i : left // 通过 substr 方法截取长字符串中与目标字符串长度相等的子字符串,与目标字符串进行比较。如果相等,说明找到了目标字符串在长字符串中的位置,返回 left 。 if (longStr.substr(left,shortStr.length) === shortStr) return left } return -1 } console.log(myIndexOf('zhpp','pp')); // 2
同理可以实现lastIndexOf功能:
function strLastIndexOf(str1,str2) { let longStr = str1.length > str2.length ? str1 : str2 let shortStr = longStr === str1 ? str2 : str1 let lastChar = shortStr[shortStr.length - 1]; let right = longStr.length - 1; for (let i = longStr.length - 1; i >= 0; i--) { right = longStr[i] === lastChar ? i : right; if (longStr.substr(right - shortStr.length + 1, shortStr.length) === shortStr) { return right - shortStr.length + 1; } } return -1 }
字符串格式化
字节
let str = `
1 2 4342 3 4 5
23 4
4 5
`;
let ans = str.trim().split('\n').map(line => line.trim().split(/\s+/).map(x => Number(x)))
// trim 方法用于去除字符串的首尾空白字符,split('\n') 用于按照换行符分割字符串,split(/\s+/) 用于按照一个或多个空格分割字符串,map(Number) 用于将每个元素转换为数字。
判断一个字符串是否为驼峰字符串
例如:judge('ByteDance','BD') -> true judge('Bytedance','BD') -> false
答案
function judge(str,abbr){
// 使用正则表达式 /[A-Z][a-z]*/g 匹配字符串中的所有以大写字母开头的单词,并将它们的首字母取出来。
return str.match(/[A-Z][a-z]*/g).map(item=>item[0]).join('') === abbr
}
压缩字符串
例如:将abbccccaaa->a1b2c4a3
let char = 'abbccccaaa'
function ShortenStr(str) {
let res = ''
let count = 1
for (let i = 1; i <= str.length; i++) {
if (str[i] === str[i - 1]) {
count++
} else {
res += str[i - 1] + count
count = 1
}
}
return res
}
console.log(ShortenChar(char))
字符串转数字
// 如果不是大数的情况
// 1. parseInt
let str = '123';
let res = parseInt(str);
// 2. +
let res1 = +str;
// 3. ~~
let res2 = ~~str;
// 4. Number
let res3 = Number(str);
// 如果是大数的情况
let res4 = BigInt(str);
功能复现
手写用 ES6proxy 如何实现 arr[-1] 的访问
答案
let arr= [1,2,3,4] let proxy = new Proxy(arr,{ get(target,key){ if(key<0){ return target[target.length+parseInt(key)] } return target[key] } }) console.log(proxy[-2]); console.log(proxy[2]);
实现简单路由
答案
function Router() { this.routes = {} this.currentUrl = '' /** * router.routes('/home', () => { // 处理home路径的回调逻辑 }); */ this.routes = function (path, callback) { this.routes[path] = callback || function () {} } // 复现 refresh 实现 this.refresh = function () { // 获取url,去除开头的 # 符号,如果 location.hash.slice(1) 的结果为空字符串或为假值,即没有哈希部分或哈希部分为空,则将默认值 '/' 赋给 curUrl 属性。 this.currentUrl = location.hash.slice(1) || '/' this.routes[this.currentUrl]() } // 什么时候执行 refresh 呢? load和hashchange事件会触发 this.init = function () { window.addEventListener('load', this.refresh.bind(this), false) window.addEventListener('hashchange', this.refresh.bind(this), false) } }
实现可过期的localstorage数据
可以在存储数据时,在数据中加入一个过期时间戳,然后在获取数据时,先判断该数据的时间戳是否已经过期,如果已过期,则将该数据从 localStorage 中删除,并返回 null,否则返回该数据。
答案
function setWithExpiration(key, value, expirationMinutes) { const expirationMS = expirationMinutes * 60 * 1000; const record = { value: value, timestamp: new Date().getTime() + expirationMS }; localStorage.setItem(key, JSON.stringify(record)); } function getWithExpiration(key) { const record = JSON.parse(localStorage.getItem(key)); if (!record) { return null; } if (new Date().getTime() > record.timestamp) { localStorage.removeItem(key); return null; } return record.value; } // 使用方法 setWithExpiration("data", { foo: "bar" }, 1); // 设置 data 数据,1 分钟后过期 const data = getWithExpiration("data"); // 获取 data 数据 console.log(data); // { foo: "bar" } setTimeout(() => { const expiredData = getWithExpiration("data"); // 获取 data 数据 console.log(expiredData); // null,数据已过期,被自动删除 }, 60 * 1000); // 1 分钟后
map键值对转换
答案
题目:
arr = [ { name: "可乐", categories: ["热门", "饮料"], }, { name: "苹果", categories: ["热门", "食物"], }, { name: "洗衣液", categories: ["生活用品"], }, ]; // [ // { name: "热门", categories: ["可乐", "苹果"] }, // { name: "饮料", categories: ["可乐"] }, // { name: "食物", categories: ["苹果"] }, // { name: "生活用品", categories: ["洗衣液"] }, // ];
解法:
function changeFormat(arr){ let res = [] let map = new Map() arr.forEach(item=>{ let categories = item.categories // console.log(categories) categories.map(keyword=>{ if(!map.get(keyword)) { // 注意值必须为数组,否则无法调用 push 方法 map.set(keyword,[item.name]) }else{ // 注意:如果只是设置了种类为键,名称为值,则会出现不同种类有相同的值:{ '热门' => '可乐', '饮料' => '可乐', '食物' => '苹果', '生活用品' => '洗衣液' } map.get(keyword).push(item.name) } }) }) map.forEach((key,val)=>{ res.push({name:val,categories:key}) }) return res }
实现日期格式化函数
答案
function formatDate(date, format) { const pad = (n) => n < 10 ? '0' + n : n; const dateDict = { "YYYY": date.getFullYear(), "MM": pad(date.getMonth() + 1), "DD": pad(date.getDate()), "HH": pad(date.getHours()), "mm": pad(date.getMinutes()), "ss": pad(date.getSeconds()) }; // 代码中使用了正则表达式 / (YYYY|MM|DD|HH|mm|ss) /g 匹配字符串中的占位符,并使用箭头函数 matched => dateDict[matched] 将匹配到的占位符替换为对应的日期值。 return format.replace(/(YYYY|MM|DD|HH|mm|ss)/g, matched => dateDict[matched]); } let date = new Date(); console.log(formatDate(date, 'YYYY-MM-DD')); // 输出形如 '2023-09-08' 的日期 console.log(formatDate(date, 'YYYY/MM/DD HH:mm:ss')); // 输出形如 '2023/09/08 17:20:04'
渲染节点的render函数
答案
{ tag: 'DIV', attrs:{ id:'app' }, children: [ { tag: 'SPAN', children: [ { tag: 'A', children: [] } ] }, { tag: 'SPAN', children: [ { tag: 'A', children: [] }, { tag: 'A', children: [] } ] } ] } 把上诉虚拟Dom转化成下方真实Dom <div id="app"> <span> <a></a> </span> <span> <a></a> <a></a> </span> </div>
/** 1.数字类型转化为字符串 2.字符串类型创建为文本节点 3.对于节点内的tag标签,使用dom创建 4.如果vnode存在attrs属性,遍历后使用dom设置 5.对于孩子进行遍历,dom添加节点后递归 **/ // 真正的渲染函数 function _render(vnode) { // 如果是数字类型转化为字符串 if (typeof vnode === "number") { vnode = String(vnode); } // 字符串类型直接就是文本节点 if (typeof vnode === "string") { return document.createTextNode(vnode); } // 普通DOM const dom = document.createElement(vnode.tag); if (vnode.attrs) { // 遍历属性 Object.keys(vnode.attrs).forEach((key) => { const value = vnode.attrs[key]; dom.setAttribute(key, value); }); } // 子数组进行递归操作 vnode.children.forEach((child) => dom.appendChild(_render(child))); return dom; }
手写map方法
// map本身的方法创建一个新数组,其结果是每一个元素调用提供的函数后返回的结果
Array.prototype._map = function (cb, thisBinding) {
// 排除回调非函数情况
if (typeof cb !== "function") {
throw new TypeError(`${cb} is not a function`);
}
// // 这里的this通常指向调用map的对象(this的特性),通常是一个数组,这是关键的思考
if (this == null || typeof this[Symbol.iterator] !== "function") {
throw new TypeError(`${this} is not a iterable`);
}
// 将可迭代对象转换成数组
// 避免修改原始的this对象
const array = [...this];
const result = [];
// 执行遍历并回调
for (let i = 0; i < array.length; i++) {
// this值被设置为thisBinding,接受三个参数:当前元素,当前元素的索引和原始数组
// 因为map也接受三个参数,例如arr.map(function(element, index, array) {...}
result.push(cb.call(thisBinding, array[i], i, this));
}
return result;
};
模拟 lodash 中的 _.get() 函数
/**
* --- 题目描述 ---
*
* 补充函数的 TODO 部分,模拟 lodash 中的 _.get() 函数。
*
* --- 测试用例 ---
*
* 输入:
* const obj = { selector: { to: { toutiao: "FE Coder"} }, target: [1, 2, { name: 'byted'}]};
* get(obj, 'selector.to.toutiao', 'target[0]', 'target[2].name')
* 输出:
* ['FE coder', 1, 'byted']
*/
// 在你的代码中,你试图从一个对象中获取多个路径的值。每个路径都是独立的,应该从原始对象开始遍历。如果你从上一次的结果开始,那么你实际上是在尝试在上一次结果的基础上寻找新的路径,这显然是不正确的。
//举个例子,假设你有一个对象 {a: {b: 1}, c: 2},你想获取 'a.b' 和 'c' 的值。如果你从原始对象开始,你可以先获取 'a.b' 的值 1,然后再获取 'c' 的值 2。但是如果你从 'a.b' 的结果开始,你就无法获取 'c' 的值,因为在 {b: 1} 这个对象中并没有 'c' 这个属性。
function get(object, ...path) {
return path.map((item) => {
let value = object;
item = item.replace(/\[/g, ".")
.replace(/\]/g, "")
.split('.');
item.map(path => value = value && value[path]); // 如果value为真,返回后面的值,否则返回value
return value;
})
}
// const obj = { selector: { to: { toutiao: "FE Coder"} }, target: [1, 2, { name: 'byted'}]};
// console.log(get(obj, ['selector.to.toutiao', 'target[0]', 'target[2].name'], 'default')) // 输出:["FE Coder", 1, "byted"]
function get(object, paths, defaultValue) {
return paths.map((path) => {
let value = object;
path = path.replace(/\[/g, ".")
.replace(/\]/g, "")
.split('.');
path.map(pathItem => value = value && value[pathItem]); // 如果value为真,返回后面的值,否则返回value
return value === undefined ? defaultValue : value;
})
}
repeat()函数
字节
let timeLog=repeat(2,1000,console.log)
timeLog('hello world')
timeLog('ByteDance')
function repeat(times, interval, func)
{
let count = 0
return function()
{
let args = arguments;
let timer = setInterval(()=>{
if(count < times)
{
count ++
func.apply(this,args)
}else{
clearInterval(timer)
}
},interval)
}
}
多种思路题
数组中的最大值(至少使用三种方法)
答案
const arr = [1, 3, 55, 77, 1000]; // 1. 调用Math let res = Math.max(...arr); // 2. 排序 arr.sort((a, b) => a - b)[arr.length - 1]; // 3. 可以采用快排 // 4. 直接获取最大值 function getMax(arr){ let res = -Infinity; for(let i = 0; i < arr.length; i++){ res = Math.max(res, arr[i]); } return res; }
不用循环求和(至少使用三种方法)
可以初始化判断参数是否为数组,长度是否合法:
if(!arr || Array.isArray(arr) || arr.length === 0) {
throw new Error('参数必须是数组,且不允许为空')
}
forEach方法
let arr = [1, 2, 3, 4, 5]; let sum = 0; arr.forEach(function(item) { sum += item; }); console.log(sum); // 输出: 15
reduce
let arr = [1, 2, 3, 4, 5]; let sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue,0); console.log(sum); // 输出:15
eval函数
let arr = [1,2,3 ,4 ,5]; let sum = eval(arr.join("+")); console.log(sum); // 输出:15
递归
function sumArray(arr) { if (arr.length === 0) { // 基线条件:如果数组为空,那么总和就是0 return 0; } else { return arr[0] + sumArray(arr.slice(1)); // 递归步骤:取数组的第一个元素并加上对剩余部分调用sumArray函数的结果 } } let arr = [1, 2, 3, 4, 5]; console.log(sumArray(arr)); // 输出:15
数组扁平化
数组扁平化是指将多维嵌套的数组转换为一维数组的过程。在扁平化后的数组中,所有的元素都位于同一级别,没有嵌套结构。
举个例子,考虑一个多维嵌套的数组 [1, [2, [3, 4]], 5]
,使用数组扁平化后,将得到一维数组 [1, 2, 3, 4, 5]
。原本嵌套的数组被展开成了一个没有嵌套结构的一维数组。
递归
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
var result = [];
for (var i = 0, len = arr.length; i < len; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]))
}
else {
result.push(arr[i])
}
}
return result;
}
console.log(flatten(arr))
ES6 拓展运算符
var arr = [1, [2, [3, 4]]];
function flatten(arr) {
// 注意这种方法不使用其他数组,即在原数组上进行扁平化,原地扁平化
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
console.log(flatten(arr))
flat方法实现
const arr = [1, [2, 3, [4, 5]], 1, 2, [6, 7]]
Array.prototype.myFlat = function () {
return this.reduce((pre, cur) => {
cur instanceof Array ? pre.push(...cur.myFlat()) : pre.push(cur)
return pre
}, [])
}
// 或
function flatten(arr)
{
return arr.reduce((pre,cur)=>{
if(Array.isArray(cur))
{
return pre.concat(flatten(cur))
}else{
return pre.concat(cur)
}
},[])
}
console.log(arr.myFlat());
数组的合并
set
let arr1 = [1, 2, 3]; let arr2 = [3, 4, 5]; let merged = [...new Set([...arr1,...arr2])]; console.log(merged); // 输出:[1 ,2 ,3 ,4 ,5]
filter
let arr1 = [1, 2, 3]; let arr2 = [3, 4, 5]; let merged = arr1.concat(arr2).filter((item,index,arr) => { // 使用 indexOf() 方法判断当前元素在数组中的第一个索引位置是否与当前索引位置相同,如果相同则表示该元素是第一次出现,保留该元素。 return arr.indexOf(item) === index; }); console.log(merged); // 输出:[1 ,2 ,3 ,4,5]
数组去重
数组去重的十种方法:
Set
let arr = [1, 2, 3, 2, 1]; let uniqueArr = [...new Set(arr)];
Filter
let arr = [1, 2, 3, 2, 1]; let uniqueArr = arr.filter((item,index,array) => array.indexOf(item) === index);
Reduce
let arr = [1, 2, 3 ,4 ,5 ,6]; const result=arr.reduce((unique,item)=>{ return unique.includes(item)?unique:[...unique,item] },[]); console.log(result)
Map
function uniq(arr){ const res=new Map(); return arr.filter(a=>!res.has(a)&&res.set(a,a)) } uniq([0,-0,+0]) // 输出[0,-0]
双层循环 + splice
function dedupe(array){ let len=array.length; for(let i=0;i<len;i++){ for(let j=i+1;j<len;j++){ if(array[i]==array[j]){ array.splice(j--,1); } } } return array; }
排序后相邻比较
function dedupe(array){ array.sort(); let temp=[]; for(let i=0;i<array.length;i++){ if( array[i] !== temp[temp.length-1]){ temp.push( array[i]) } } return temp; }
利用对象属性名称不能相同原理进行去重
function dedupe(array){ var n={},r=[]; //n为hash表,r为临时数组 for(var i=0; i<array.length; i++) //遍历当前数组 { if (!n[array[i]]) //如果hash表中没有当前项 { n[array[i]] = true; //存入hash表 r.push(array[i]); //把当前数组的元素存入临时数组中 } } return r; }
includes方法:判断新数组是否含有老数组的某一项,没有则push进新数组
function dedupe(arr){ let newArr=[]; for(let i=0;i<arr.length;i++){ if(!newArr.includes(arr[i])){ newArr.push(arr[i]); } } return newArr; }
lastIndexOf
function dedupe(array){ return array.filter((item,index,array)=>array.lastIndexOf(item)===index); }
every
function dedupe(array){ let newArr=[]; for(let i=0;i<array.length;i++){ if(newArr.every(item=>item!==array[i])){ newArr.push(array[i]); } } return newArr; }
算法类
使用递归完成1到100的累加
答案
function process(cur, total = 0) { return cur === 0 ? total : process(cur - 1, total + cur); }
注意:不能写成
cur--
,会导致每次传入的cur
值都相同,因为cur
的值在计算之后才会减1。这会导致递归调用无法结束,最终导致栈溢出。
打印出1-10000以内的对称数(回文数)
答案
function isSymmetryNum(start,end){ for(let i = start; i < end + 1; i++){ let str = String(i); if(str.length <= 1) continue; if(str.split("").reverse().join("") === str){ console.log(i); } } } isSymmetryNum(1,10000) // 或用一行代码实现 function palindromes1(num) { return +((''+num).split('').reverse().join('')) === num // split():将一个字符串分割成一个字符串数组。它接受一个参数,这个参数指定了分割字符串的分隔符。 // reverse():将一个数组原地反转数组中元素的顺序。 // join():将数组中的所有元素连接成一个字符串。它接受一个参数,这个参数指定了连接数组元素的分隔符。 }
请实现一个模块 math,支持链式调用math.add(2,4).minus(3).times(2);
答案
class math { constructor(initValue = 0) { this.value = initValue; } add(...args) { this.value = args.reduce((pre, cur) => pre + cur, this.value); return this; } minus(...args) { this.value = this.value - args.reduce((pre, cur) => pre + cur); return this; } times(timer) { this.value = timer * this.value; return this; } getVal() { return this.value; } }
实现(5).add(3).minus(2)功能
答案
(function() { function check(x) { //统一转成数字 return x = isNaN(x) ? 0 : x - 0 } function add (x) { x = check(x) return this + x } function minus (x) { x = check(x) return this - x } Number.prototype.add = add Number.prototype.minus = minus })(); console.log((5).add('12').minus('1')) // 16 console.log((5).add(2)) // 7 console.log((5).add('abc').minus('1')) // 4
有一堆整数,请把他们分成三份,确保每一份和尽量相等(11,42,23,4,5,6 ,4, 5, 6 ,11, 23 ,42 ,56, 78, 90)
答案
var a = [11, 42, 23, 4, 5, 6, 4, 5, 6, 11, 23, 42, 56, 78, 90] function oneToThreeArr(arr){ let res = [{sum: 0, arr: []}, {sum: 0, arr: []}, {sum: 0, arr: []}] //从大到小排序,每次把最大的值给到和最小的数组中 arr = arr.slice().sort((a,b) => b - a);//拷贝一份再排序 arr.map(item => { let min = res.sort((a,b) => a.sum - b.sum)[0];//找到到当前和最小的那个数组 min.sum += item; min.arr.push(item); }) return res; } console.log(oneToThreeArr(a));
求最接近的值
答案
const arr = [3, 56, 56, 23, 7, 76, -2, 345, 45, 76, 3]; const num = 37
function main(arr, num){ // 即求两数之间的距离,找出最小距离的索引 const dif = arr.map(item => Math.abs(item - num)); let minIndex = 0; for(let i = 1; i < arr.length; ++i){ dif[i] < dif[minIndex] && (minIndex = i); } // 此处可以使用内置的方法找到最小值和索引 // const min = Math.min(...dif); // const index = dif.findIndex(i => i === min); return arr[minIndex]; }
素数
答案
function isPrime(num) { if (num <= 1) return false; // 素数定义为大于1的自然数中,除了1和它本身以外不再有其他因数的自然数 if (num === 2) return true; // 2是唯一的偶素数 if (num % 2 === 0) return false; // 排除其他偶数 // 因为如果一个数字n不是素数,那么它一定可以分解成两个因数a和b,其中a和b都不等于1和n本身。那么其中一个因数必然小于等于n的平方根,另一个因数就大于等于n的平方根。 const sqrt = Math.sqrt(num); for(let i = 3; i <= sqrt; i += 2) { if (num % i === 0) return false; } return true; }
查找质因数
用JavaScript实现把一个合数用质因数相乘的形式表示出来,并按要求格式输出。例如: 100=225*5
输入描述: 输入一个整数n 输出描述: 查找并输出整数n的质因数 输入样例: 100 输出样例: 100=225*5
function primeFactors(n) {
var factors = [];
for (var i = 2; i <= Math.sqrt(n); i++) {
while (n % i === 0) {
factors.push(i);
n = n / i;
}
}
if (n > 1) {
factors.push(n);
}
return factors;
}
function formatOutput(n) {
var factors = primeFactors(n);
return n + "=" + factors.join("*");
}
console.log(formatOutput(100)); // 输出:100=2*2*5*5
一个数组,找出每个数组元素右侧第一个比当前数大的数的下标,时间复杂度O(N)
答案
function main(arr){ if(arr.length < 2) return []; // 单调递减栈 const stack = [arr[0]], res = []; for(let i = 1; i < arr.length; ++i){ if(arr[i] > stack[stack.length - 1]){ stack.push(arr[i]); res.push(arr[i]); } } return res; }
将 HTTP header 转换成 js 对象
/**
* --- 题目描述 ---
*
* 实现一个方法,把 HTTP 文本形式(字符串)的 header 转换成 JS 对象。
*
* --- 测试用例 ---
*
* 输入:
* `Accept-Ranges: bytes
* Cache-Control: max-age=6000, public
* Connection: keep-alive
* Content-Type: application/javascript`
* 输出:
* {
* "Accept-Ranges": "bytes",
* "Cache-Control": "max-age=6000, public",
* Connection: "keep-alive",
* "Content-Type": "application/javascript"
* }
*
* --- 解题思路 ---
*
* 1. 首先将每行数据作为数组的一个元素
* 2. 将每个元素使用冒号分割,前面为 `key`,后面为 `value`。
*/
const solution = (s) => {
let res = {};
let arr = s.split("\n");
arr.forEach((e) => {
let tmp = e.split(": ");
console.log(tmp)
res[tmp[0]] = tmp[1];
})
console.log(res)
return res;
}
注意到上面的输出结果中,Connection
没有带引号,这是为什么呢?
在JavaScript中,如果对象中的属性名是一个有效的标识符,并且不包含特殊字符,如空格、连字符等,那么可以省略引号。因此,当属性名符合标识符的规则时,可以不使用引号。
在给对象
res
赋值时,属性名Connection
是一个有效的标识符,不包含特殊字符,因此可以省略引号。其他属性名如果包含特殊字符,如空格、连字符等,就会被包含在引号中。
async await 如何实现
function* generatorFunc() {
const data1 = yield getData()
console.log('data1', data1);
const data2 = yield getDataTwo()
console.log('data2', data2);
}
// 自动执行
function autoGenerateFunc(generatorFunc) {
return function () {
// 生成迭代器
const gen = generatorFunc.apply(this, arguments)
return new Promise((resolve, reject) => {
const step = (p, arg) => {
let genObj;
try {
genObj = gen[p](arg)
} catch (error) {
return reject(error)
}
const { value, done } = genObj;
if (done) {
return resolve(value)
} else {
return Promise.resolve(value).then(val => step('next', val), err => step('throw', err))
}
}
step('next')
})
}
}
判断对象是否存在循环引用
function isCyclic(obj, weakSet = new WeakSet())
{
if(Object.prototype.toString(obj) === '[object Object]')
{
if(weakSet.has(obj))
{
return true
}
weakSet.add(obj)
for(let key in obj)
{
if(isCyclic(obj[key],weakSet)) return true
}
}
return false
}
let cyclicObj1 = {};
cyclicObj1.selfRef = cyclicObj1;
let cyclicObj2 = { a: { b: null } };
cyclicObj2.a.b = cyclicObj2.a;
console.log(isCyclic(cyclicObj1)); // 输出:true
console.log(isCyclic(cyclicObj2)); // 输出:true
console.log(isCyclic({})); // 输出:false
实现函数solution(arr, k)
arr是number数组,k是number,返回前k个最小的数字组成的数组,保持相对顺序 输入:[1,2,3,4,5,3,2],3,输出:[1,2,2] 输入:[1,2,3,4,5,3,2],4,输出:[1,2,3,2] 输入:[1,2,3,4,5,3,2],5,输出:[1,2,3,3,2]
function smallestK(arr, k) {
// 将数组中每个元素和其索引打包在一起
let indexedArr = arr.map((value, index) => ({value, index}));
// 按照元素值大小排序,如果值相同则按照索引排序
indexedArr.sort((a, b) => {
if (a.value !== b.value) {
return a.value - b.value;
} else {
return a.index - b.index;
}
});
// 取出前k个元素,并且只保留值部分
let result = indexedArr.slice(0, k).map(item => item.value);
return result;
}
console.log(smallestK([1,2,3,4,5,3,2],3)); // 输出:[1 ,2 ,2]
console.log(smallestK([1 ,2 ,3 ,4 ,5 ,3 ,2],4)); // 输出:[1 ,2 ,2 ,3]
console.log(smallestK([1 ,2 ,3 ,4 ,5 ,3 ,2],5)); // 输出:[1 ,2 ,2 ,3 ,3]
随机类
产生一个不重复的随机数组
// 参数 数组长度、最小范围、最大范围
function randomUniqueArr(len = 100, min = 0, max = 200) {
if (max - min + 1 < len) {
// 可生成数的范围小于数组长度
return null;
}
const hash = [];
while (hash.length < len) {
const num = Math.round(Math.random() * (max - min)) + min;
if (num < min) continue;
if (hash.indexOf(num) === -1) {
hash.push(num);
}
}
return hash;
}
console.log(randomUniqueArr());
console.log(randomUniqueArr(20, 10, 31));
注意点:
- 判断是否包含数量应该是
max - min + 1 < num
, 比如 1 和 2 之间是可以选中两个的- 取值用
Math.round
保证可以取到最大值和最小值
随机生成字符串
function randomString(len, chars) {
let res = []
for (let i = 0; i < length; i++) {
res.push(chars[Math.floor(Math.random() * chars.length)])
}
return res.join('')
}
结构转换类
将数组转化为树形结构
初始时,数组中的每个元素具有 4 个属性,其中有 id 和 parent_id,现在我们需要根据这两个 id 之间的关系,添加一个 children 属性,使之成为一棵树的结构。
比如有如下数据:
var menu_list = [{
id: '1',
menu_name: '设置',
menu_url: 'setting',
parent_id: 0
}, {
id: '1-1',
menu_name: '权限设置',
menu_url: 'setting.permission',
parent_id: '1'
}, {
id: '1-1-1',
menu_name: '用户管理列表',
menu_url: 'setting.permission.user_list',
parent_id: '1-1'
}, {
id: '1-1-2',
menu_name: '用户管理新增',
menu_url: 'setting.permission.user_add',
parent_id: '1-1'
}, {
id: '1-1-3',
menu_name: '角色管理列表',
menu_url: 'setting.permission.role_list',
parent_id: '1-1'
}, {
id: '1-2',
menu_name: '菜单设置',
menu_url: 'setting.menu',
parent_id: '1'
}, {
id: '1-2-1',
menu_name: '菜单列表',
menu_url: 'setting.menu.menu_list',
parent_id: '1-2'
}, {
id: '1-2-2',
menu_name: '菜单添加',
menu_url: 'setting.menu.menu_add',
parent_id: '1-2'
}, {
id: '2',
menu_name: '订单',
menu_url: 'order',
parent_id: 0
}, {
id: '2-1',
menu_name: '报单审核',
menu_url: 'order.orderreview',
parent_id: '2'
}, {
id: '2-2',
menu_name: '退款管理',
menu_url: 'order.refundmanagement',
parent_id: '2'
}
]
思路是先使用 tmp 将数组中的元素处理成 id: {id...menu_name...menu_url...parent_id}
类的格式,然后针对该数据处理成树状结构。
实现代码如下:
const buildTree = (arr) => {
const tmp = {};
const res = {};
arr.forEach((item) => {
tmp[item.id] = item;
});
Object.values(tmp).forEach((item) => {
if (item.parent_id) { // 如果节点没有父节点,即该节点为根节点
// 从 tmp 对象的值中寻找 id 和当前子对象 parent_id 相等的值
const parent = Object.values(tmp).find((parentItem) => parentItem.id === item.parent_id);
if (!parent.children) { // 该节点的父节点和该节点没有形成关系,第一次循环中将所有父节点的所有孩子置为空
parent.children = {};
}
// 设置父节点的孩子,即当前值
parent.children[item.id] = item;
} else {
res[item.id] = item;
}
});
return res;
};
console.log(buildTree(menu_list));
树形结构转成列表
const data = [
{
id: 1,
text: '节点1',
parentId: 0,
children: [
{
id: 2,
text: '节点1_1',
parentId: 1
}
]
}
]
function treeToList(data) {
let res = [];
const dfs = (tree) => {
tree.forEach((item) => {
if (item.children) {
dfs(item.children);
delete item.children;
}
res.push(item);
});
};
dfs(data);
return res;
}
// 我更喜欢这种,因为在真是业务场景中,应该保留原始解构
// 只是把深层的children拿出来放到第一层 个人想法
function treeToList(data) {
if (!Array.isArray(data) && data.length === 0) return;
// 方式1
return data.reduce((prev, cur) => {
const {children} = cur;
return Array.isArray(children) && children.length > 0 ? prev.concat(treeToList(children), cur) : prev.concat(cur);
}, [])
列表转换为数组
let arr = [
{ id: 1, name: '部门1', pid: 0 },
{ id: 2, name: '部门2', pid: 1 },
{ id: 3, name: '部门3', pid: 1 },
{ id: 4, name: '部门4', pid: 3 },
{ id: 5, name: '部门5', pid: 4 },
{ id: 6, name: '部门6', pid: 0 },
]
function get_tree(arr) {
const list = []
arr.forEach(element => {
const children_arr = arr.filter(ele => {
return element.id === ele.pid
})
if (children_arr.length > 0) {
element.children = children_arr
}
if (element.pid === 0) {
list.push(element)
}
});
return list
}
console.log(get_tree(arr));
逆对象扁平
逆对象扁平(Inverse Object Flattening)是指将扁平化的数据结构转换回原始的嵌套对象结构。
function unflattenObject(data) {
let result = {};
for (let key in data) {
let keys = key.split('.');
keys.reduce((r, k, i, a) => {
return r[k] || (r[k] = isNaN(a[i + 1]) ? (a.length - 1 === i ? data[key] : {}) : []);
}, result);
}
return result;
}
let testObj = {
'a.b.c': 1,
'd.e': [2,3],
};
console.log(unflattenObject(testObj)); // 输出:{ a: { b: { c: 1 } }, d: { e: [2,3] } }
将十进制数字转为二进制数字字符串
使用内置方法
const x = 10
console.log(x.toString(2)) // "1010"
手动实现
let x = 10
let str = ''
while (x) {
str += x & 1 ? '1' : '0'
x >>= 1
}
console.log(str.split('').reverse().join('')) // "1010"
多维数组全排列
循环递归
function permute(arr) { if (arr.length === 1) return [arr[0]]; let result = []; for (let i = 0; i < arr.length; i++) { let restArr = [...arr.slice(0, i), ...arr.slice(i + 1)]; result = result.concat(permute(restArr).map(item => arr[i] + item)); } return result; } let testArr = [[1,2],[3,4],[5,6]]; console.log(permute(testArr)); // 输出:[[1,3,5],[1,3,6],[1,4,5],[1,4,6],[2 ,3 ,5] ,[2 ,3 ,6] ,[2 ,4 ,5] ,[2 ,4 ,6]]
reduce
function generateCombinations(arr) { return arr.reduce((acc, val) => { return acc.map(v => val.map(item => [...v, item])).flat(); }, [[]]); } let testArr = [[1, 2], [3, 4], [5, 6]]; console.log(generateCombinations(testArr)); /** 输出:[[1 ,3 ,5] ,[1 ,3 ,6] ,[1 ,4 ,5] ,[1 ,4 ,6] ,[2 ,3 ,5] ,[2 ,3 ,6] ,[2 ,4 ,5],[2,4,6]] //当执行到第一次循环时, acc 的初始值为 [[]] , val 的值为 [1, 2] 。在第一次循环中, val.map(item => [...v, item]) 将会对 val 数组中的每个元素进行遍历,将当前元素与 v 中的每个元素进行组合。由于 v 的初始值是空数组 [] ,所以对于 val 数组中的第一个元素 1 ,会生成一个新的数组 [1] ,对于 val 数组中的第二个元素 2 ,也会生成一个新的数组 [2] 。 因此,第一次循环结束时, acc.map(v => val.map(item => [...v, item])).flat() 的结果就是 [ [1], [2] ] 。 接下来,进入第二次循环, acc 的值为 [ [1], [2] ] , val 的值为 [3, 4] 。在第二次循环中,同样会对 val 数组中的每个元素进行遍历,并将当前元素与 acc 中的每个元素进行组合。对于 val 数组中的第一个元素 3 ,会生成两个新的数组 [1, 3] 和 [2, 3] ,对于 val 数组中的第二个元素 4 ,也会生成两个新的数组 [1, 4] 和 [2, 4] 。 因此,第二次循环结束时, acc.map(v => val.map(item => [...v, item])).flat() 的结果就是 [ [1, 3], [2, 3], [1, 4], [2, 4] ] 。 **/
集合的所有子集
问题描述: 集合的所有子集 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集。示例如下:
输入样例1:nums = [1,2,3]
输出样例1:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
输入样例2:nums = [0]
输出样例2:[[],[0]]
function subsets(nums) {
const res = [];
backtrack(nums, res, [], 0);
return res;
}
function backtrack(nums, res, subset, start) {
res.push([...subset]);
for (let i = start; i < nums.length; i++) {
subset.push(nums[i]);
backtrack(nums, res, subset, i + 1);
subset.pop();
}
}
console.log(subsets([1,2,3])); // 输出:[[],[1],[1,2],[1,2,3],[1,3],[2],[2,3],[3]]
console.log(subsets([0])); // 输出:[[],[0]]
闭包应用类
实现 执行一次的函数
function one(callback){
let flag = true;
// 注意是匿名函数
return function(...args){
if(!flag) return;
flag = false;
return callback(...args);
}
}
管道函数
function pipe(...fns) {
return function(...args) {
if (fns.length === 0) return;
if (fns.length === 1) return fns[0](...args);
return fns.reduce((acc, fn) => {
if (typeof acc === 'function') {
acc = acc(...args);
args = []; // 清空参数列表,因为之后的函数应该接收上一个函数的结果作为输入
}
return fn(acc); // 将上一步骤得到结果作为当前操作器输入
});
};
}
实现一个函数a,使其奇数次调用时返回1,偶数次调用时返回2(不能使用全局变量)
function createFunction() {
let count = 0;
return function() {
count++;
if (count % 2 === 0) {
return 2;
} else {
return 1;
}
};
}
let a = createFunction();
console.log(a()); // 输出:1
console.log(a()); // 输出:2
console.log(a()); // 输出:1
实现一个同时实现sum(x,y)和sum(x)(y)
// 答案一
function sum(a,b){
if(b) {
return a+b
}else{
return function(c){
return a+c
}
}
}
// 答案二
function sum(){
var arg=arguments
if(arg.length==2) {
return arg[0]+arg[1];
}else{
return function(c){
return arg[0]+c
}
}
}
柯里化函数
function myCurrying(fn) {
function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
}
}
return curried;
}
原理类
深拷贝
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj); // 检查是否已经拷贝过该对象,以防止循环引用导致的无限递归
let cloneObj = new obj.constructor(); // 获取原始对象的构造函数,确保拷贝对象与原始对象具有相同的类型和结构
// 找到的是所属类原型上的constructor,而原型上的 constructor 指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
// 验证
o = {a: 1, d: {c: '4'}};
res = deepClone(o);
console.log(res);
console.log(res == o);
浅拷贝
// 首先定义一个对象
const hero = {
name: 'Batman',
city: 'Gotham'
};
// **********************方法一**********************
const heroEnhancedClone = {
...hero,
name: 'Batman Clone',
realName: 'Bruce Wayne'
};
// 验证
heroEnhancedClone; // { name: 'Batman Clone', city: 'Gotham', realName: 'Bruce Wayne' }
// **********************方法二**********************
const { ...heroClone } = hero;
// 验证
heroClone; // { name: 'Batman', city: 'Gotham' }
hero === heroClone; // => false
// **********************方法三**********************
const hero = {
name: 'Batman',
city: 'Gotham'
};
// 验证
const heroClone = Object.assign({}, hero);
heroClone; // { name: 'Batman', city: 'Gotham' }
hero === heroClone; // => false
手写 call apply bind 函数
Function.prototype._call = function (obj, ...args) {
!obj && (obj = globalThis);
// this代表要执行的函数
obj._fn = this;
const res = obj._fn(...args);
delete obj._fn;
return res;
};
Function.prototype._apply = function (obj, args) {
// 第二个参数必须为数组或类数组对象, 否则初始化为空对象
const arr = [];
for (let i = 0; i < args?.length; ++i) {
arr.push(args[i]);
}
return this._call(obj, ...arr);
};
Function.prototype._bind = function (obj, ...args1) {
!obj && (obj = globalThis);
return (...args2) => {
obj._fn = this;
const res = obj._fn(...[...args1, ...args2]);
delete obj._fn;
return res;
};
};
手写 new
function funcNew(obj, ...args) {
const newObj = Object.create(obj.prototype);
const result = obj.apply(newObj, args);
return (typeof result === 'object' && result !== null) ? result : newObj;
}
先通过一个例子来理解 new 的作用吧:
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
const p = new Person('orange')
console.log(p.name) // 'orange'
p.sayName(); // 'orange'
代码中我们新建了一个对象 Person,它具有属性 name,且在 Person.prototype 上定义了函数 sayName。
当我们通过 new 创建一个新的实例 p 时,便同时具有了属性 p.name 和 p.sayName(),关系如下图:
知道了原理,就可以自己实现了。也就是说,自己写一个函数 funcNew(),使得 const p = new Person('orange') 和 const p = funcNew('orange') 得到的 p 完全相同,于是得到了答案中的代码。
答案中最后一行代码如何理解?
前面的例子我们只考虑了 Person 中没有返回值的情况,如有有返回值,new 一个实例将会受到 Person 中返回值的影响。比如说:
function Person(name) {
this.name = name;
return {age: 35}
}
Person.prototype.sayName = function() {
console.log(this.name);
}
const p = new Person('orange')
console.log(p) // { age: 35 }
console.log(p.name) // undefined
p.sayName(); // TypeError: p.sayName is not a function
/**
* --- Person 返回非对象,return 不影响结果 ---
*/
function Person(name) {
this.name = name;
return 'free'
}
Person.prototype.sayName = function() {
console.log(this.name);
}
const p = new Person('orange')
console.log(p) // Person { name: 'orange' }
console.log(p.name) // orange
p.sayName(); // orange
上面的例子中,如果返回了一个对象,我们需要返回该对象;如果不是对象,则 return
没用,正常处理。
如何自己实现一个 instanceof
?
/*
* --- 手动实现 instanceof ---
*/
function newInstanceOf (leftValue, rightValue) {
if (typeof leftValue !== 'object' || rightValue == null) {
return false;
}
let rightProto = rightValue.prototype;
leftValue = leftValue.__proto__;
while (true) {
if (leftValue === null) return false;
if (leftValue === rightProto) return true;
leftValue = leftValue.__proto__;
}
}
/*
* --- 验证 ---
*/
const a = [];
const b = {};
function Foo () {}
var c = new Foo()
function Child () {}
function Father() {}
Child.prototype = new Father()
var d = new Child()
console.log(newInstanceOf(a, Array)) // true
console.log(newInstanceOf(b, Object)) // true
console.log(newInstanceOf(b, Array)) // false
console.log(newInstanceOf(a, Object)) // true
console.log(newInstanceOf(c, Foo)) // true
console.log(newInstanceOf(d, Child)) // true
console.log(newInstanceOf(d, Father)) // true
console.log(newInstanceOf(123, Object)) // false
console.log(123 instanceof Object) // false
解读
这个问题既考察了 instanceof 的原理,又考察了原型链,还考察了代码能力,是一个好问题。
在实现代码中,我们判断 leftValue 是否为 rightValue 的实例,思想是在 leftValue 的原型链上,即 leftValue.proto 上寻找是否存在 rightValue.prototype。原理图如下:
寄生组合式继承
在寄生组合式继承中,我们使用了构造函数的继承,并通过原型链的方式继承了原型对象的属性和方法。
function Parent(name) {
this.name = name
}
Parent.prototype.getName = function () {
return this.name
}
function Son(name, age) {
// 这里其实就等于 this.name = name
Parent.call(this, name)
this.age = age
}
Son.prototype.getAge = function () {
return this.age
}
//Son.prototype.__proto__ = Object.create(Parent.prototype)
Reflect.setPrototypeOf(Son.prototype, Parent.prototype);
Reflect.setPrototypeOf(Son, Parent);
// 相当于
// Son.__proto__ = Parent;
// Son.prototype.__proto__ = Parent.prototype;
// 每个对象都有一个内部属性 __proto__ ,它指向该对象的原型对象。通过将子类的原型对象的 __proto__ 属性指向父类的原型对象,子类就能够继承父类原型对象上的属性和方法。
const son1 = new Son('shao', 20)
console.log(son1.getName()) // shao
console.log(son1.getAge()) // 20
发布订阅者模式
class EventEmitter {
constructor() {
this.event = {
}
}
on(type,cb) {
// 类似于订阅微信公众号的订阅方法,但不会触发事件
// 既然不会触发,说明我们肯定是通过on方法存储了这个方法
// 如果this.event有该类型的事件,那么直接往该事件中增加该回调函数
// 如果this.evnet没有,新建事件类型和回调函数
if(!this.event[type])
{
this.event[type] = [cb]
}else{
this.event[type].push(cb)
}
}
once(type, cb) {
// 代表我订阅公众号之后,它只通知我一次
let fn = (...args) =>{
cb(...args)
this.off(type,fn)
}
this.on(type,fn)
}
emit(type, ...args) {
// 类似于订阅公众号之后,公众号一有消息就会通知给我们
// 判断是否订阅了say事件,如果没有,返回空
// 如果订阅了,遍历执行say事件中的回调函数
if(!this.event[type])
{
return
}else{
this.event[type].forEach(cb => {
cb(...args)
});
}
}
off(type, cb) { // 就是我们常见的取消关注该公众号了
if(!this.event[type])
{
return
}else{
this.event[type] = this.event[type].filters(item => item !== cb)
}
}
}
// 运行示例
// let ev = new EventEmitter();
// const fun1 = (str) => {
// console.log(str);
// }
// ev.on('say', fun1);
// ev.emit('say', 'visa');
// ev.off('say', fun1);
// ev.once('say', fun1)
封装异步的fetch,使用async await方式来使用
// 异步方法
async function myFetchAsync(url, options) {
try {
//等 获取到数据
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
return response;
} catch (error) {
console.error(error);
throw error;
}
}
使用 setTimeout 实现 setInterval
function mySetInterval(fn, delay, ...args) {
let timer = null;
const task = () => {
timer = setTimeout(() => {
if (timer) {
fn.apply(this, args);
task();
}
}, delay);
}
task();
return () => {
clearTimeout(timer);
timer = null;
}
}
// 用于取消定时任务的执行
const cancel = mySetInterval(console.log, 1000, 'mySetInterval')
setTimeout(() => {
cancel();
}, 4500)
使用setInterval 实现 setTimeout
function _setTimeout(fn, delay, ...args) {
const timer = setInterval(() => {
fn.apply(this, args);
clearInterval(timer);
},delay)
}
settimeout系统补偿时间
为了弥补系统执行代码所需的时间,需要额外增加的时间
// 系统补偿时间
function _timerSetInterval(fn, delay, ...args) {
let current = Date.now();
let timer = null;
const task = () => {
current += delay;
timer = setTimeout(() => {
fn.apply(this, args);
task();
}, current - Date.now());
};
task();
return () => clearTimeout(timer);
}
_timerSetInterval(()=>{console.log(new Date()),1000})
settimeout准时
function setTimeoutWithOffset(fn, interval, ...args) {
let startTime = Date.now()
let count = 0// 执行次数
let timer = null
const task = () => {
// offset = timeNow - (startTime + count * interval)
const offset = Date.now() - (startTime + count * interval)
console.log(`第${count + 1}次 系统延迟时间${offset}`)//
timer = setTimeout(() => {
fn.apply(this, args);
count++
task();
}, interval - offset);
};
task();
return () => clearTimeout(timer);
}
//TEST
const stop = setTimeoutWithOffset(() => console.log(123), 1000)
setTimeout(() => {
stop()
}, 5000);
封装remove child.remove()销毁自身
Element.prototype._remove = function () {
if (!(this instanceof Element) || this.tagName === 'HTML') {
throw new TypeError(`${this} is not an element or cannot be removed`);
}
const el = this;
el.parentElement.removeChild(el);
}
输出一个等腰三角形
输出n = 3 输出:
*
***
*****
function printTriangle(n) {
for(let i = 0; i < n; i++) {
let str = '';
// 添加左侧的空格
for(let j = 0; j < n - i - 1; j++) {
str += ' ';
}
// 添加星号
for(let k = 0; k < 2 * i + 1; k++) {
str += '*';
}
console.log(str);
}
}
printTriangle(5);
实现 jsonp
JSONP 利用了 <script>
标签没有同源限制的特性,通过动态创建 <script>
标签,并指定请求的 URL,将数据作为回调函数的参数返回,从而实现跨域数据请求。
function jsonp(url, jsonpCallback, success) {
let script = document.createElement('script');
script.src = url;
script.async = true;
window[jsonpCallback] = function(data) {
success && success(data);
};
document.body.appendChild(script);
}
JavaScript清空数组
数组的length属性
var array = [1, 2, 3]; array.length = 0;
splice方法
var array = [1, 2, 3]; array.splice(0,array.length);
新建一个新的空数组覆盖原来的
var array = [1, 2, 3]; array = [];
shift
var array = [1, 2, 3]; while(array.length > 0) { array.shift(); }
pop
var array = [1, 2, 3]; while(array.length > 0) { array.pop(); }
slice
var array = [1, 2, 3]; array = array.slice(array.length);
filter
var array = [1, 2, 3]; // 7.1 array = array.filter(() => false); // 7.2 array = array.filter(item => item !== undefined);
fill
var array = [1, 2, 3]; array.fill(undefined);
对象的合并
题目:
var obj1 = {
a: 1,
b: {
c: 2,
d: 3
},
e: 4,
h: {
i: 5
}
}
var obj2 = {
a: 111,
b: {
c: 222,
f: 333
},
g: 444,
h: 666
}
// 实现一个mergeObject(obj1, obj2)方法,得到下面的对象,并转化成JSON输出到#app
var obj = {
a: 111,
b: {
c: 222,
d: 3,
f: 333
},
e: 4,
g: 444,
h: 666
}
这个mergeObject方法的主要逻辑是:
遍历obj2,如果key在obj1中不存在,直接复制该key及value到obj
如果key在两者中都存在,且value为对象,递归调用mergeObject进行合并
function isObject(obj) { return Object.prototype.toString.call(obj) === '[object Object]'; } function mergeObject(obj1, obj2) { let result = {...obj1}; for (let key in obj2) { if (isObject(obj2[key]) && isObject(result[key])) { result[key] = mergeObject(result[key], obj2[key]); } else { result[key] = obj2[key]; } } return result; } var obj1 = { a: 1, b: { c: 2, d: 3 }, e: 4, h: { i: 5 } }; var obj2 = { a: 111, b: { c:222, f :333 }, g :444, h :666 }; var mergedObj = mergeObject(obj1, obj2); // 将结果转化为JSON并输出到#app元素中 document.querySelector('#app').textContent = JSON.stringify(mergedObj, null, 4);
无限延伸数组
function* infiniteArrayGenerator() {
let i = 0;
while (true) {
yield i++;
}
}
const infiniteArray = infiniteArrayGenerator();
console.log(infiniteArray.next().value); // 输出: 0
console.log(infiniteArray.next().value); // 输出: 1
console.log(infiniteArray.next().value); // 输出: 2
// ... 可以继续下去 ...
数组的交集并集差集
const arr1 = [1, 3, 5];
const arr2 = [2, 4, 5, 6];
// 1. 并集
const arr3 = [...new Set(arr1.concat(arr2))];
// 2. 交集
const set = new Set(arr1);
const res = [];
for (let i = 0; i < arr2.length; i++) {
if (set.has(arr2[i]) && res.indexOf(arr2[i]) === -1) {
res.push(arr2[i]);
}
}
console.log(res);
// 3. 差集 这里求的是除了交集之外的
const res2 = [...new Set(arr1.concat(arr2))];
for(let i = 0; i < res2.length; i++){
if(res.indexOf(res2[i])!== -1) res2.splice(i, 1);
}
console.log(res2);
手写事件代理(委托)
function addEventListener(element, type, fn, selector) {
// 检查元素、类型和回调函数是否存在
if (!element || !type || !fn) {
return;
}
// 如果没有选择器,则直接添加事件监听器
if (!selector) {
element.addEventListener(type, fn);
return;
}
// 添加带有事件代理的监听器
element.addEventListener(type, function (event) {
let targets = Array.from(element.querySelectorAll(selector));
if (targets.includes(event.target)) {
fn.call(event.target, event);
}
});
}
数据类型判断
function getType(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回
return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');
}
预加载
const oDiv = document.querySelector("#app");
const oImg = new Image();
oImg.src = "https://avatars.githubusercontent.com/u/85861380?s=80&v=4";
oImg.style.width = "100%";
oImg.onload = function () {
oDiv.appendChild(oImg);
};
设计LRU(最近最少使用)缓存结构
LRU缓存是一种常用的缓存策略,当缓存到达最大容量,最不常使用的数据项会被淘汰。
模板:
class LRUCache {
constructor(capacity: number) {
// write code here
}
get(key: number): number {
// write code here
}
set(key: number, value: number): void {
// write code here
}
}
var LRUCache = function (capacity) {
this.map = new Map()
this.capacity = capacity
}
// 获取数据
LRUCache.prototype.get = function (key) {
if (this.map.has(key)) {
let value = this.map.get(key)
// 重新set,相当于更新到 map最后
this.map.delete(key)
this.map.set(key, value)
return value
}
return -1
}
// 添加数据
LRUCache.prototype.put = function (key, value) {
// 如果有,就删了再赋值
if (this.map.has(key)) {
this.map.delete(key)
}
this.map.set(key, value)
// 判断是不是容量超了,淘汰机制
if (this.map.size > this.capacity) {
// this.map.keys():返回一个新的 Iterator 对象,它包含了 Map 对象中每个元素的键(keys)。
// .next():这个方法返回 Iterator 对象中的下一个元素。因为 keys() 方法刚刚创建了 Iterator 对象,所以 next() 方法返回的是第一个元素。
// .value:这个属性返回由 next() 方法返回的对象中的 value 属性。value 属性就是 Map 对象的第一个键。
this.map.delete(this.map.keys().next().value)
}
}
实现一个 LazyMan
字节
实现一个LazyMan,可以按照以下方式调用:
LazyMan(“Hank”)输出:
Hi! This is Hank!
LazyMan(“Hank”).sleep(10).eat(“dinner”)输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner~
LazyMan(“Hank”).eat(“dinner”).eat(“supper”)输出
Hi This is Hank!
Eat dinner~
Eat supper~
LazyMan(“Hank”).sleepFirst(5).eat(“supper”)输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper
class LLazyMan {
constructor(name) {
this.name = name;
this.queue = [];
this.sayName();
this.run();
}
sayName() {
this.queue.push(() => {
console.log(`Hi! This is ${this.name}`);
});
return this;
}
sleep(time) {
this.queue.push(() => this.timeWait(time));
return this;
}
sleepFirst(time) {
this.queue.unshift(() => this.timeWait(time));
return this;
}
timeWait(time) {
return new Promise((resolve) =>
setTimeout(() => {
console.log(`Wake up after ${time}`);
resolve();
}, time * 1000)
);
}
eat(param) {
this.queue.push(() => {
console.log(`Eat ${param}`);
});
return this;
}
run() {
setTimeout(async () => {
for (const task of this.queue) {
await task();
}
}, 0);
}
}
const LazyMan = (name) => new LLazyMan(name);
// LazyMan("Hank").sleepFirst(5).eat("supper");
// LazyMan("Hank").eat("dinner").eat("supper");
LazyMan("c2c").sleep(2).eat("s");
Promise类
实现 Promise.all()
引用 MDN:
Promise.all(iterable)
方法返回一个Promise
实例,此实例在iterable
参数内所有的promise
都“完成(resolve)”或参数中不包含promise
时回调完成(resolve);如果参数中promise
有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败promise
的结果。
Promise._all = (iterObj) => {
// 1. iterObj 必须是一个可迭代对象, 否则, 无法正常进行则抛出错误
if(!(typeof iterObj === "object" && iterObj !== null && typeof iterObj[Symbol.iterator] === "function")){
throw new TypeError(`${iterObj} is not iterable`);
}
iterObj = [...iterObj];
/*
* 2. 函数返回值为 `<Promise>` 对象, 当参数 `iterObj` 内所有的 `Promise` 成功,
* 该 `<Promise>` 对象成功, 成功数据为所有成功的 `Promise` 结果数组,
* 有一个不成功, 则该 `<Promise>` 不成功, 失败数据为失败原因字符串
*/
return new Promise((resolve, reject) => {
const len = iterObj.length;
let count = 0;
if(len === 0) return resolve([]);
const res = new Array(len);
iterObj.forEach(async (item, index) => {
// 将当前元素包装为一个Promise对象
const newItem = Promise.resolve(item);
try{
const result = await newItem;
res[index] = result;
if(++count === len){
resolve(res)
}
}catch(err){
reject(err);
}
})
})
}
// 验证:
function test(){
try{
Promise._all(null).then(res=>console.log(res), rej=>console.log(rej));
// throw err: null is not iterable
}catch(e){
console.log(e)
}
try{
Promise._all({}).then(res=>console.log(res), rej=>console.log(rej));
// throw err: [object object] is not iterable
}catch(e){
console.log(e)
}
Promise._all([]).then(res=>console.log(res), rej=>console.log(rej));
// []
Promise._all(new Set()).then(res=>console.log(res), rej=>console.log(rej));
// []
Promise._all(new Map()).then(res=>console.log(res), rej=>console.log(rej));
// []
Promise._all([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3),
4,
]).then(res=>console.log(res), rej=>console.log(rej))
// [1, 2, 3, 4]
Promise._all([
Promise.reject(1),
Promise.resolve(2),
Promise.resolve(3),
4,
]).then(res=>console.log(res), rej=>console.log(rej))
// 1
}
test();
完整实现Promise A+
实现过程详见:
// 第一步
// 手写 Promise
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function runMicroTask(callback) {
if (process && process.nextTick) {
process.nextTick(callback);
} else if (MutationObserver) {
const p = document.createElement("p");
const observer = new MutationObserver(callback);
observer.observe(p, {
childList: true,
});
p.innerHTML = "1";
} else {
// 将回调函数添加到宏任务队列中,并在当前宏任务执行完毕后尽快执行,可以保证回调函数的执行不会阻塞当前的宏任务,
setTimeout(callback, 0);
}
}
// 判断一个对象是否为 Promise 对象
function isPromise(obj) {
return !!(obj && typeof obj === "object" && typeof obj.then === "function");
}
class MyPromise {
/**
*
* @param {Function} executor 任务执行器,立即执行
*/
constructor(executor) {
this._state = PENDING;
this._value = undefined;
this._handlers = []; // 处理函数形成的队列
try {
executor(this._reject.bind(this), this._resolve.bind(this));
} catch (error) {
this._reject(error);
}
}
/**
* 向处理队列添加函数
* @param {Function} executor
* @param {Function} state
*/
_pushHandler(executor, state, resolve, reject) {
this._handlers.push({
executor,
state,
resolve,
reject,
});
}
/**
* 根据实际情况,执行队列
* @returns
*/
_runHandlers() {
if (this._state === PENDING) {
// 目前任务仍在挂起
return;
}
// 如果当前状态不是挂起,为成功或失败
while (this._handlers[0]) {
const handler = this._handlers[0];
this._runOneHandler(handler);
// 处理一个handler后还要删除刚处理的这一个,用for-of循环会出问题
this._handlers.shift();
}
}
/**
* 处理每一个handler的函数
* @param {Object} handler
*/
_runOneHandler({ executor, state, resolve, reject }) {
runMicroTask(() => {
if (this._state !== state) {
// 状态不一致,直接返回
return;
}
// 参数是非函数
if (typeof executor !== "function") {
this._state === FULFILLED ? resolve(this._value) : reject(this.value);
return;
}
try {
const result = executor(this._value);
if (isPromise(result)) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
});
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
this._pushHandler(onFulfilled, FULFILLED, resolve, reject);
this._pushHandler(onRejected, REJECTED, resolve, reject);
this._runHandlers();
});
}
/**
*
* @param {String} newState
* @param {any} value
*/
_changeState(newState, value) {
// 这段代码是为了使得状态不会改变
if (this._state !== PENDING) {
// 说明目前状态已经更改
return;
}
this._state = newState;
this._value = value;
this._runHandlers(); // 状态变化,运行执行队列
}
/**
*
* @param {any} data 任务完成的相关数据
*/
_resolve(data) {
this._changeState(FULFILLED, data);
}
/**
*
* @param {any} data 任务失败的相关数据
*/
_reject(reason) {
this._changeState(REJECTED, reason);
}
}
// 测试
MyPromise.deferred = function() {
const defer = {}
defer.promise = new MyPromise((resolve, reject) => {
defer.resolve = resolve
defer.reject = reject
})
return defer
}
try {
module.exports = MyPromise
} catch (e) {
}
实现Promise.finally
/**
* 无论成功还是失败都会执行回调
* @param {Function} onSettled
*/
Promise.prototype.myfinally = function (onSettled) {
return this.then(
(data) => {
onSettled(); // 实现了收不到参数了
return data;
},
(reason) => {
onSettled();
throw reason;
}
);
// finally函数 返回结果应该是无效的
}
/******test finally*******/
Promise.resolve(123)
.then((res) => {
console.log(res); //123
return Promise.reject(456);
})
.myfinally(() => {
console.log("finally");
return "finally本身不返回值";
})
.then(
() => {},
(err) => {
console.log(err); //456
return 789;
}
)
.myfinally(() => console.log("finally"))
.then((res) => console.log(res)); //789
实现Promise.allSettled
/**
* 等待所有的Promise有结果后
* 该方法返回的Promise完成
* 并且按照顺序将所有结果汇总
* @param {Iterable} proms
*/
Promise.myAllSettled = function (iterObj) {
if(!(typeof iterObj === 'object' && iterObj !== null && typeof iterObj[Symbol.iterator] === 'function'))
{
throw new TypeError('it is not iterable')
}
iterObj = [...iterObj]
return new Promise((resolve, reject)=>{
const len = iterObj.length
const res = new Array(len)
let count = 0
iterObj.forEach((item,index) => {
Promise.resolve(item)
.then((value)=>{
res[index] = {status:'fulfilled',value}
})
.catch((err)=>{
res[index] = {status:'rejected',err}
})
.finally(()=>{
count++
if(count === len) resolve(res)
})
});
})
};
const pro = new Promise((resolve, reject) => {
setTimeout(() => {
reject(3);
}, 1000);
});
Promise.allSettled([pro, Promise.resolve(1), Promise.reject(2)]).then(
(data) => {
console.log(data);
}
);
Promise.mySettled([pro, Promise.resolve(1), Promise.reject(2)]).then((data) => {
console.log(data);
});
实现Promise.race
/**
* 返回的Promise与第一个有结果的一致
* @param {iterator} proms
*/
Promise.race = function (promises) {
if (promises == null || typeof promises[Symbol.iterator] !== "function") {
throw new Error(`传入的参数不是可迭代对象`);
}
promises = [...promises];
return new Promise((resolve, reject) => {
for (let p of promises) {
Promise.resolve(p).then(resolve,reject);
}
});
};
实现 Promise.prototype.catch
/**
* 本质就是then,只是少传了一个onFulfilled
* 所以仅处理失败的场景
* @param {*} onRejected
*/
Promise.prototype.catch = function(onRejected) {
return this.then(null, onRejected);
}
实现Promise.then
function myResolve(value) {
// 如果 value 已经是 Promise 对象,则直接返回该 Promise 对象
if (value instanceof Promise) {
return value;
}
// 如果 value 是 thenable 对象,则包装成 Promise 对象并返回
if (value && typeof value.then === 'function') {
return new Promise((resolve, reject) =>{
value.then(resolve, reject);
});
}
// 将传入的值作为 Promise 的成功值,并返回 Promise 对象
return new Promise((resolve) =>{
resolve(value);
});
}
使用Promise封装AJAX请求
function getJson(url){
let promise = new Promise((resolve,reject)=>{
let xhr = new XMLHttpRequest()
//创建一个http请求
xhr.open('Get',url,true)
xhr.onreadystatechange=function(){
if(this.readyState!==4)return
if(this.status>=200&&this.status<400){
resolve(this.response)
}else{
reject(new Error(this.statusText))
}
}
//设置错误监听函数
xhr.onerror=function(){
reject(new Error(this.statusText))
}
//设置响应数据类型
xhr.setRequestHeader('Accept',"application/json")
//发送http请求
xhr.send(null)
})
return promise
}
中断Promise请求
// 根据promise a+ 规范来说
// then()接受两个参数 如果参数是不是函数将被忽略
// onFulfilled 将在promise fulfilled 后调用并接受一个参数
// onRejected 将在promise rejected 后调用 并接受一个参数
// 另外 then 一定返回的是promise
// 若两参数是函数,当函数返回的是一个新的promise对象时
//原promise 跟新promise 状态保持一致
// 如果返回的promise 是个pending 状态 将保留直到转换为fulfilled / rejected
// promise中断请求 不就是在then的时候将返回值新promise保持状态为penging
// 那么这个promise 的链也将会中止(等待)
Promise.resolve().then(()=>{
// pending
console.log(1)
return new Promise(()=>{})
// 后面的then 将不会调用
}).then(()=>{
console.log(2)
})
Promise串行
Promise 串行是指按照特定的顺序依次执行一系列的 Promise 对象
//Promise串行
async function serialPromise(taskarr) {
let res = [];
for (const task of taskarr) {
try {
res.push(await task());
} catch (err) {
res.push(null);
}
}
return res;
}
实现有并行限制的 Promise 调度器
class Scheduler {
constructor(max) {
// 最大可并发任务数
this.max = max;
// 当前并发任务数
this.count = 0;
// 阻塞的任务队列
this.queue = [];
}
async add(fn) {
if (this.count >= this.max) {
// 若当前正在执行的任务,达到最大容量max
// 阻塞在此处,等待前面的任务执行完毕后将resolve弹出并执行
await new Promise(resolve => this.queue.push(resolve));
}
// 当前并发任务数++
this.count++;
// 使用await执行此函数
const res = await fn();
// 执行完毕,当前并发任务数--
this.count--;
// 若队列中有值,将其resolve弹出,并执行
// 以便阻塞的任务,可以正常执行
this.queue.length && this.queue.shift()();
// 如果题目要求同时发送n个请求,当有请求返回后往请求队列里push新的请求,并输出刚刚结束的请求的返回值。即参数是多个任务,添加一个循环
// for (let i = 0; i < tasks.length; i++) {
// add(tasks[i]);
//}
// 返回函数执行的结果
return res;
}
}
使用:
// 延迟函数
const sleep = time => new Promise(resolve => setTimeout(resolve, time));
// 同时进行的任务最多2个
const scheduler = new Scheduler(2);
// 添加异步任务
// time: 任务执行的时间
// val: 参数
const addTask = (time, val) => {
scheduler.add(() => {
return sleep(time).then(() => console.log(val));
});
};
addTask(1000, '1');
addTask(500, '2');
addTask(300, '3');
addTask(400, '4');
// 2
// 3
// 1
// 4
上一次请求未实现,之后的无响应
let runningTask = 0;
async function callAsyncFunction(promiseFn) {
if(runningTask===0){
console.log("Calling async function...");
runningTask++;
// 将 promiseFn 包装成一个新的 Promise,以便可以在其执行完毕后进行下一步操作
// 返回当前的 promise,以便可以在外部处理其结果
return new Promise(async (resolve, reject) => {
try {
// await不会捕获错误,所以用try...catch
const result = await promiseFn();
resolve(result);
runningTask--;
} catch (err) {
reject(err);
runningTask--;
}
});
}else{
return null;
}
}
封装一个工具函数输入一个promiseA返回一个promiseB如果超过1s没返回则抛出异常,如果正常则输出正确的值。
function promiseUtil(promise) {
return Promise.race([
promise,
new Promise((_, reject) => {
setTimeout(() => {
reject("error");
}, 1000);
}),
]);
}
请求5s未完成就终止(和上一个题其实是一样的)
const funWait = (call, timeout = 5000) => {
let wait = new Promise((resolve, reject) => {
setTimeout(() => {
reject('请求超时')
}, timeout)
})
return Promise.race([call(), wait])
}
const t = () => new Promise(resolve => setTimeout(resolve, 4000))
const t2 = () => new Promise(resolve => setTimeout(resolve, 6000))
funWait(t).then(res => {
console.log("t1 resolve")
}).catch(err => {
console.log("t1 timeout")
})
funWait(t2).then(res => {
console.log("t2 resolve")
}).catch(err => {
console.log("t2 timeout", err)
})
Promise.retry 超时重新请求,并在重试一定次数依然失败时输出缓存内容
/**
* 超时重新请求,并在重试一定次数依然失败时输出缓存内容
* @param {*} promiseFactory 一个返回 Promise 的函数,表示要执行的异步操作。
* @param {*} maxRetries 一个整数,表示最大的重试次数。
* @param {*} timeout 一个整数,表示每次重试的间隔时间(单位为毫秒)。
* @param {*} cache 一个可选的参数,表示缓存的内容,如果重试多次依然失败,则会返回该缓存内容。
* @returns promise
*/
function retry(promiseFactory, maxRetries, timeout, cache=null) {
return new Promise((resolve, reject) => {
let retries = 0;
const executePromise = () => {
promiseFactory()
.then(resolve)
.catch((error) => {
retries++;
if (retries >= maxRetries) {
if (cache) {
resolve(cache);
} else {
reject(error);
}
} else {
setTimeout(executePromise, timeout);
}
});
};
executePromise();
});
}
// ----------test----------
!(() => {
const fetchData = () => {
// 返回一个 Promise 对象,表示异步请求数据
return fetch('http://example.com/data')
.then((response) => response.json())
.then((data) => {
// 处理数据
return data;
});
};
retry(fetchData, 3, 10000, '缓存内容')
.then((data) => {
// 成功获取数据
console.log(data);
})
.catch((error) => {
// 请求失败或超时,并且重试多次依然失败
console.error(error);
});
})()
应用类
JavaScript常规应用实现
手写防抖(debounce)
// debounce
function debounce(fn, delay=500) {
// timer 写在闭包中,因此防抖也是闭包的一个应用
let timer = null;
return function() {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay)
}
}
// 验证
input1.addEventListener('keyup', debounce(() => {
console.log(input1.value);
}), 600)
手写节流(throttle)
// 节流
function throttle(fn, delay = 100) {
let timer = null
return function() {
// 如果 timer 不为 null ,说明函数正在进行节流,直接返回,不执行后续的函数调用。
if (timer) {
return
}
// 如果 timer 为 null ,则创建一个定时器,延迟指定的时间间隔后执行一个回调函数。
timer = setTimeout(() => {
// 回调函数中,调用原始函数 fn ,并使用 apply 方法将当前上下文和参数传递给原始函数。
fn.apply(this, arguments)
// 执行完回调函数后,将 timer 变量重新设为 null ,表示函数执行完毕。
timer = null
}, delay)
}
}
div1.addEventListener(('drag', throttle(function (e) {
console.log(e.offsetX, e.offsetY)
})))
防抖函数的作用是在指定的时间间隔内,只有最后一次函数调用会被执行。当连续触发函数时,防抖函数会清除之前设置的定时器,并重新设置一个新的定时器,延迟一段时间后执行函数。这样可以确保只有在指定的时间间隔内没有新的函数调用时,才会执行函数。
节流函数的作用是在指定的时间间隔内,限制函数的执行频率。当连续触发函数时,节流函数会在指定的时间间隔内执行一次函数,然后忽略后续的函数调用,直到过了指定的时间间隔后,才会再次执行函数。
判断两个对象是否相等
调库
/*
* @param x {Object} 对象1
* @param y {Object} 对象2
* @return {Boolean} true 为相等,false 为不等
*/
console.log(_.isEqual(x, y))
实现
/*
* @param x {Object} 对象1
* @param y {Object} 对象2
* @return {Boolean} true 为相等,false 为不等
*/
const deepEqual = function(x, y) {
// 指向同一内存时
if (x === y) {
return true;
} else if ((typeof x === 'object' && x != null) && (typeof y === 'object' && y != null)) {
if (Object.keys(x).length != Object.keys(y).length) {
return false;
}
for (let prop in x) {
if (y.hasOwnProperty(prop)) {
if (!deepEqual(x[prop], y[prop])) {
return false;
}
} else {
return false;
}
}
return true;
}
return false;
}
实现代码中,以下边界情况无法处理:
- 其中某个属性本身是一个对象
- 某个属性的值为
NaN
- 一个对象的属性的值为
undefined
,另一个对象中没有这个属性
一次性处理海量DOM节点
假设一个
ul
下有一万个li
,li
的innerHTML
是从0
到9999
,当点击某个li
时输出该li
代表的值,如何实现
采用事件委托:
window.onload = function () {
var uli = document.getElementById("ul");
uli.onclick = function(event) {
alert(event.target.innerText);
}
}
首先,我们当然不可能为每一个 li 标签手动添加一个 click 事件(容易累死);其次,我们可能会想到使用 for 循环遍历每个元素,然后为其添加 click 事件,但这样会频繁操作 DOM,降低性能,卡到爆炸。
而事件委托意义就在于此:减少 DOM 操作,从而减少浏览器的重绘与重排次数,提升性能。
事件委托的原理是,将 li 上监听的 click 事件委托到 ul 上。这里运用到了 事件冒泡 的机制,即 onclick 事件以 li -> ul -> body -> html -> document 的冒泡顺序逐步传递。
所以,我们可以选择在其中的 ul 上监听 click 事件,从而实现事件委托。
如何创建 100000 个
呢?总不能复制粘贴 100000 次吧?
创建 100000 个
ul
标签的 innerHTML
即可。
/* --- create100000li.js --- */
window.onload = function() {
var ul = document.getElementsByTagName("ul");
var arr = [];
for (let i = 0; i < 100000; i++) {
arr.push(i);
}
ul[0].innerHTML = '<li>' + arr.join('</li><li>') + '</li>'
}
手写 Promise 加载一张图片(预加载)
function loadImg(src) {
const p = new Promise(
(resolve, reject) => {
const img = document.createElement('img')
img.onload = () => {
resolve(img)
}
img.onerror = () => {
const err = new Error(`图片加载失败 ${src}`)
reject(err)
}
img.src = src
}
)
return p
}
const url = 'https://pic.leetcode-cn.com/1604237471-xbJgZl-%E5%9B%BE%E7%89%871.png';
loadImg(url).then(img => {
console.log(img.width)
return img
}).then(img => {
console.log(img.height)
}).catch(ex => console.error(ex))
图片懒加载
const images = document.querySelectorAll("img")
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
// 判断当前元素是否可见
if (entry.isIntersecting) { // 如果一个条目时交叉的(即对应的元素在视口中)
// 创建一个自定义属性data-src存放真正要显示的图片路径,原本img自带的src放一张默认图片
const img = entry.target
const data_src = img.getAttribute('data-src') // 该属性包含了真正的图片URL
img.setAttribute('src', data_src)
// 解除观察,有几张图片就触发几次,因为图片已经开始加载
observer.unobserve(img)
}
})
})
images.forEach(image => {
// 对每一个图片对象进行观察
observer.observe(image)
})
无限动画
const box = document.getElementById('box');
let rotation = 0; // 初始旋转角度为 0
function animate() {
rotation += 0.1; // 每次旋转增加 0.1 弧度
box.style.transform = `rotate(${rotation}rad)`; // 更新旋转角度
requestAnimationFrame(animate); // 请求下一次重绘
}
requestAnimationFrame(animate); // 启动动画
如何用 ajax 原生实现一个 post
请求
function ajax_post(url, data) {
// 1. 异步对象 ajax
var ajax = new XMLHttpRequest();
// 2. url 方法
ajax.open('post', url);
// 3. 设置请求报文
ajax.setRequestHeader('Content-type', 'text/plain');
// 4. 发送
if (data) {
ajax.send(data);
} else {
ajax.send();
}
// 5. 注册事件
ajax.onreadystatechange = function () {
if (ajax.readyState === 4 && ajax.status === 200) {
console.log(ajax.respenseText);
}
}
}
每隔一秒输出一个数字
使用 let (推荐)
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000 * i)
}
使用闭包
for (var i = 0; i < 10; i++) {
(function(j) {
setTimeout(() => {
console.log(j);
}, 1000 * j)
})(i)
}
使用第三个参数
for(var i = 0; i <= 5; i++){
setTimeout((j) => {
console.log(j);
},i * 1000, i)
}
Promise实现
const delayPrint = (delay, ...args) => {
return new Promise((res, rej) => {
setTimeout(() => {
console.log(...args)
res()
},delay)
})
}
(async () => {
for (let i = 1; i <= 5; i++){
await delayPrint(1000,i)
}
})()
正则表达式模板字符串
String.prototype.render = function (data) {
return this.replace(/{{[.\s\S]*?}}/g, match => {
if ((match = match.substring(2, match.length - 2).trim()) == "") {
return "";
} else if (match.startsWith("#")) {
return eval(match.substr(1, match.length - 1));
} else {
return data[match] ? data[match] : "";
}
})
}
const data = {
name: "小明",
age: 16,
school: "第三中学",
classroom: "教室2"
}
console.log(
"{{ name }} 今年 {{ age }} 岁,就读于 {{ school }} 今天在 {{ classroom }} 上课,{{ name }} {{ #data.age >= 18 ? '成年了' : '未成年' }}".render(data)
);
// 小明 今年 16 岁,就读于 第三中学 今天在 教室2 上课,小明 未成年
console.log(
`{{name}}说了句{{#
if (data.age >= 18) {
"我已经成年了!"
} else {
"我还没有成年!"
}
}}`.render(data)
);
// 小明说了句我还没有成年!
交通灯
回调方式实现
function red() {
console.log("red");
}
function yellow() {
console.log("yellow");
}
function green() {
console.log("green");
}
// 回调方案实现
const step = (timer, light, callback) => {
setTimeout(() => {
if (light === "red") {
red();
} else if (light === "green") {
green();
} else if (light === "yellow") {
yellow();
}
callback()
}, timer);
};
const runStep = () =>{
step(3000,'red',()=>{
step(2000,'green',()=>{
step(1000,'yellow',runStep)
})
})
}
runStep()
Promise方案
function red() {
console.log("red");
}
function yellow() {
console.log("yellow");
}
function green() {
console.log("green");
}
const step = (timer, light) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (light === "red") {
red();
}
else if (light === "green") {
green();
}
else if (light === "yellow") {
yellow();
}
resolve();
}, timer);
});
};
const runStep = () => {
step(3000, "red")
.then(() => {
step(1000, "green");
})
.then(() => {
step(2000, "yellow");
})
.then(runStep);
};
runStep()
Async/Await 方案实现
const step = (timer, light) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (light === "red") {
red();
}
else if (light === "green") {
green();
}
else if (light === "yellow") {
yellow();
}
resolve();
}, timer);
});
};
const runStep = async () =>{
while(1){
await step(3000,'red'),
await step(2000,'green'),
await step(1000,'yellow')
}
}
runStep()
sleep函数实现
sleep
函数是一种用于在代码执行过程中暂停一段时间的函数。它通常用于模拟延迟操作或控制代码的执行速度。
普通版本
function sleep (slTime) {
let start = new Date()
while(new Date - start <= slTime) {}
}
const t5 = new Date()
sleep(3000)
const t6 = new Date()
console.log(t6 - t5)
Promise实现
const sleep = (timer = 3000) =>{
return new Promise((resolve,reject)=>{
setTimeout(() => {
resolve()
}, timer);
})
}
async,await实现
function sleep (slTime) {
return new Promise(resolve =>{
setTimeout(resolve,slTime)
})
}
(async function test(){
const t1 = new Date();
await sleep(3000)
const t2 = new Date();
console.log(t2 - t1);
}())
JS异步数据流,实现并发异步请求,结果顺序输出
//JS异步数据流,实现并发异步请求,结果顺序输出
const timer = [3000, 2000, 1000, 5000, 5000];
function myTimeout(timer) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(timer);
}, timer);
});
}
async function orderPrint(timer) {
const promises = timer.map((time) => myTimeout(time));
for (const p of promises) {
console.log(await p);
}
}
orderPrint(timer);
处理高并发, 100 条数据,带宽为 10, 跑满带宽
async function asyncPool(poolLimit, array, iteratorFn) {
let poolList = [] // 并发队列
let ret = [] // 所有的任务
for(let i of array) {
let p = new Promise.resolve(iteratorFn(i)) // 添加Promise处理,统一处理非Promise的任务
ret.push(p)
let _p = p.then(() => { poolList.splice(poolList.indexOf(_p), 1)}) // 任务完成将任务推出队列
poolList.push(_p) // 将任务放到并发队列中去
if(poolList.length >= poolLimit) { // 当任务数达到并发上限
await Promise.race(poolList) // 等待队列中的任务完成
}
}
return Promise.all(ret) // 返回任务执行结果
}
设计一个简单的任务队列, 要求分别在 1,3,4 秒后打印出 "1", "2", "3";
题目:
new Quene()
.task(1000, () => {
console.log(1)
})
.task(2000, () => {
console.log(2)
})
.task(1000, () => {
console.log(3)
})
.start()
function Quene() { ... } //补全代码
实现:
function Queue() {
this.quene = [];
}
Queue.prototype.task = function (time, callback) {
this.quene.push({ time, callback });
return this;
};
Queue.prototype.start = function () {
const quene = this.quene;
let result = Promise.resolve();
quene.forEach((item) => {
result = result.then(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(item.callback());
}, item.time);
});
});
});
return result;
};
//test
new Queue()
.task(1000, () => {
console.log(1)
})
.task(2000, () => {
console.log(2)
})
.task(1000, () => {
console.log(3)
})
.start()
或
// 设计一个简单的任务队列, 要求分别在 1,3,4 秒后打印出 "1", "2", "3";
function Quene() {
const queue = []
function task(delay, fn) {
const promise = () => Promise.resolve().then(() =>
new Promise((res) =>
setTimeout(() => {
fn()
res()
}, delay)
)
)
queue.push(promise)
return this
}
async function start() {
for(let task of queue) {
await task()
}
}
return {
task: task,
start: start
}
}
new Quene()
.task(1000, () => {
console.log(1)
})
.task(2000, () => {
console.log(2)
})
.task(1000, () => {
console.log(3)
})
.start()
输入50a6we8y20x 输出50个a,6个we,8个y,20个x
// (\d+) 表示匹配一个或多个数字,并将其捕获为第一个分组 $1 ; ([a-z]+) 表示匹配一个或多个小写字母,并将其捕获为第二个分组 $2 。
// 回调函数中,通过 $2.repeat(parseInt($1)) 的方式,将第二个分组中的字母重复出现多次,重复次数由第一个分组中的数字决定。
var extendsString = (str) => str.replace(/(\d+)([a-z][A-Z]+)/g, (_, $1, $2) => $2.repeat(parseInt($1)));
输入一串字符串,根据字符串求出每个字母的数量并返回结果对象。(数字为1时可省略)
function main(str){
const len = str.length;
const map = new Map();
const res = {};
for(let i = 0; i < len; ++i){
map.has(str.charAt(i)) ? map.set(str.charAt(i), map.get(str.charAt(i)) + 1) : map.set(str.charAt(i), 1);
}
for (const key of map.keys()) {
res[key] = map.get(key);
}
return res;
}
创建包含10个1的数组多种方法
使用Array构造函数和fill()方法:
var arr = new Array(10).fill(1);
使用循环
var arr = []; for (let i = 0; i < 10; i++) { arr.push(1); }
使用Array.from()方法:
var arr = Array.from({length: 10}, () => 1);
使用扩展运算符和map()方法:
var arr = [...new Array(10)].map(() => 1);
字符串的repeat和split方法:
var arr = "1".repeat(10).split("")
对输入的字符串:去除其中的字符'b';去除相邻的'a'和'c'。
样例:
‘aacbd’ -> 'ad'
'aabcd' -> 'ad'
'aaabbccc' -> ''
不允许使用类似string.replace函数 对输入的字符串:去除其中的字符'b';去除相邻的'a'和'c'。
function removeChars(str) {
let stack = [];
for (let i = 0; i < str.length; i++) {
if (str[i] === 'b') continue;
if ((str[i] === 'a' && stack[stack.length - 1] === 'c') ||
(str[i] === "c" && stack[stack.length - 1] === "a")) {
// 如果当前字符与堆栈顶部字符可以配对,则弹出顶部字符
stack.pop();
} else {
// 否则将当前字符推入堆栈
stack.push(str[i]);
}
}
return stack.join('');
}
用一行代码,将数组中的字符串和字符串对象(new String(123))直接判定出来
let arr = ['hello', new String('123'), 123, true, {}, [], null];
let stringItems = arr.filter(item => typeof item === 'string' || item instanceof String);
实现一个函数,把一个字符串数组([‘zm’, ‘za’, ‘b’, ‘lm’, ‘ln’, ‘k’]) 格式化成一个对象 { ‘b’: [‘b’], ‘k’: [‘k’], ‘l’: [‘lm’, ‘ln’], ‘z’: [‘za’, ‘zm’] }
function format(arr){
const res = {};
for(let item of arr){
// 获取字符串的首字母
const char = item.slice(0, 1);
// 如果首字母没有包含在对象中,说明是第一次遇到该首字母,将其作为键,创建一个以当前元素为唯一成员的数组,并存储在 res 中。
// 如果首字母包含在对象中,说明之前已经有其他元素具有相同的首字母,将当前元素追加到对应的数组中。
!res[char] ? (res[char] = [item]) : (res[char].push(item));
}
return res;
}
before(num,fn)接受两个参数,第一个参数是数字,第二个参数是函数,调用before函数num次数以内,返回与fn执行相同的结果,超过num次数返回最后一次fn的执行结果。
function before(num, fn) {
let count = 0;
let lastResult;
return function(...args) {
if (count < num) {
lastResult = fn.apply(this, args);
count++;
}
return lastResult;
};
}
let testFn = function(x) { return x * x; };
let beforeTestFn = before(3, testFn);
console.log(beforeTestFn(2)); // 输出 4
console.log(beforeTestFn(3)); // 输出 9
console.log(beforeTestFn(4)); // 输出 16
console.log(beforeTestFn(5)); // 输出 16,因为已经超过了3次调用,所以返回最后一次执行结果。
移除空属性
let obj={a:null,b:'哈哈哈'}
//对象移除为空的属性
for (const [key, value] of Object.entries(obj)) {
if (value === null || value === "" || value === undefined) { //筛选条件可根据实际情况自行调整
Reflect.deleteProperty(obj, key);
}
}
console.log(obj) //{b:'哈哈哈'}
['ab', 'c', 'ab', 'd', 'c'] => ['ab1', 'c1' ,'ab2', 'd', 'c2']
const arr = ['ab', 'c', 'ab', 'd', 'c'];
let map = new Map();
for (let i = 0; i < arr.length; i++) {
map.set(arr[i], (map.get(arr[i]) || 0) + 1);
}
// 筛选出不是首次出现的字符组
map = Array.from(map).filter((item) => item[1] !== 1);
map = new Map(map);
for (let i = arr.length - 1; i >= 0; i--) {
// 从后向前扫描数组,后续扫描出现一次,map中的次数减一
if (map.get(arr[i]) > 0) {
let tmp = arr[i];
let val = map.get(arr[i]);
arr[i] += map.get(arr[i]);
map.set(tmp, val - 1);
}
}
console.log(arr);
寻找当前出现次数最多的三个标签
function findThree(){
const doms = Array.from(document.querySelectorAll("*")).map(node => node.tagName);
const map = new Map();
for (const item of doms) {
map.has(item) ? map.set(item, map.get(item) + 1) : map.set(item, 1);
}
return Array.from(map.entries()).sort((a, b) => b[1] - a[1]).slice(0, 3);
// 如果只需要键,那么.map(item=>item[0])
}
URL反转
// 时间复杂度O(n) 空间复杂度O(1)
function reverseURL(url) {
const arr = [];
let res = "";
for(let i = url.length - 1; i >= 0; --i){
url[i] !== "." && (res = url[i] + res);
if(i === 0 || url[i] === "."){
arr.push(res);
res = "";
}
}
for(let i = 0; i < arr.length; ++i){
res += arr[i];
if(i !== arr.length - 1){
res += "."
}
}
return res;
}
实现一个计数器,支持重置
class Count {
}
const instance = new Count()
console.log(instance.add())//1
console.log(instance.add())//2
console.log(instance.add())//3
console.log(instance.reset())//1
console.log(instance.add())//1
class Count {
constructor(value = 0){
this.value = value;
}
add(){
this.value++;
return this.value;
}
reset(){
this.value = 1;
return this.value;
}
}
const instance = new Count()
console.log(instance.add())//1
console.log(instance.add())//2
console.log(instance.add())//3
console.log(instance.reset())//1
console.log(instance.add())//2
颜色生成
function color() {
const _color = () => {
let val = (Math.floor(Math.random() * 255) + 1).toString(16)
return val.padStart(2, '0')
}
return `#${_color()}${_color()}${_color()}`
}
判断A、B数组的包含关系(值和数量),A属于B返回1,B属于A返回2,两者相等返回0,其他返回-1
const A = [1, 3, 5, 6, 7];
const B = [1, 3, 5, 6, 8];
function judge(A, B) {
let str1 = A.sort((a, b) => a - b).join("");
let str2 = B.sort((a, b) => a - b).join("");
if(str1 === str2) return 0;
else if (str1.includes(str2)) return 2;
else if (str2.includes(str1)) return 1;
return -1;
}
console.log(judge(A, B));
实现内存函数缓存函数
const cachedFn = (function (fn) {
let cache = {};
return function (...args) {
argsKey = args.toString()
if (argsKey in cache) {
return cache[argsKey];
}
return (cache[argsKey] = fn.apply(this, args));
};
})(fn);
用代码实现把字符串转换成base64编码
function encode(str){
const encodestr = encodeURI(str);
const base64 = btoa(encodestr);
return base64;
}
封装一个通用的fetch请求
function Qrequest(url,options){
return fetch(url,options)
.then(checkStatus)
.then(parseJSON)
.catch(err=>{
throw err
})
}
function checkStatus(res){
if(res.ok)
{
return res
}else{
throw new Error(res.statusText)
}
}
function parseJSON(res){
return res.json()
}
Vue.js类
手写defineProperty
// 手写实现Vue.js的数组、对象响应式监听
// 利用Object.defineProperty(obj, prop, descriptor)实现
// 参数列表:
// obj:需要定义属性的对象
// prop:需要定义的属性
// descriptor:属性的描述描述符
// 返回值:返回此对象
// 基本流程:遍历为数组、对象的每一个值、属性进行绑定监听函数,
// 为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者
// 当值改变的时候,就会通知更新,作为发布者发出通知
// 将事件发送给dev中存储的对应订阅者watcher,并调用对应的update方法更新视图
function observe(target) {
// 不是对象或者数组直接返回
if (typeof target !== "object" || target === null) return target;
// 如果是数组则需要更改原型
if (Array.isArray(target)) {
// 对于数组直接扩展方法对影响原型,所以需要处理
// 该方法创建新对象,隐式原型原型指向Array.prototype
let arrPrototype = Array.prototype;
const arrProto = Object.create(arrPrototype);
target.forEach((methodName) => {
arrProto[methodName] = function () {
arrPrototype[methodName].call(this, ...arguments);
};
});
target.__proto__ = arrProto; //如果target为数组则改变原型指向,使用重写后的方法
}
for (let key in target) {
// 遍历
defineProperty(target, key, target[key]);
}
}
function defineProperty(target, key, value) {
//监听函数
observe(value); //当value也为对象时进行递归深度监听 例如上面定义的obj.a
Object.defineProperty(target, key, {
get() {
return value;
},
set(newValue) {
if (newValue !== value) {
update(value, newValue);
//通知更新,作为发布者发出通知,
// 实际中会将事件发送给dev中存储的对应watcher,并调用对应的update方法更新视图
value = newValue;
observe(newValue); //当新值赋值为对象时, 对该对象绑定监听
}
},
});
}
function update(oldValue, newValue) {
//模拟更新操作
console.log(`oldValue: ${oldValue}, newValue: ${newValue}`);
}
let obj = {
a: {
b: 1,
c: 2,
},
d: 4,
};
observe(obj); //监听对象,绑定defineProperty方法
console.log(obj.a.b); //调用get方法
obj.a.b = 3; //直接修改b的值 监听成功
obj.a = 4; //把a赋值成基本类型 监听成功
obj.d = 5; //直接修改d的值 监听成功
obj.d = {
//把d修改成对象 监听成功
e: 8,
};
let arr = ["test", "try", ["deep"]];
observe(arr); //监听数组,绑定defineProperty方法
console.log(arr[0]); //调用get方法
arr[0] = "change"; //修改数组的第一层值
arr[2][0] = "66" // 修改数组的第二层值
console.log(arr[2]); //验证修改成功