前言
最近工作任务太多,到了年底冲刺阶段,以至于没有时间浏览前端新的技术点,在每天偶尔的空闲时间,梳理了面试中遇到频率比较高的手写算法题,都是比较基础部分,也在此做个总结分享。
1.防抖
对于防抖的理解,即在频繁触发的事件结束指定的时间后,执行一次。如输入框输入查询。
/**
*
* @param {function} fn
* @param {number} [ms=1000]
* @return {function}
*/
function debounce (fn, ms = 1000) {
// 创建一个函数作用域,并初始化变量 timeout
let timeout = null;
return function () {
if (timeout) {
// 清除执行
clearTimeout(timeout)
}
// 不再有事件,最后执行一次
timeout = setTimeout(() => {
// 调用函数并传入参数,绑定this
fn.apply(this, arguments)
}, ms)
}
}
2.节流
对于节流的理解,即频繁触发的事件,加以控制按一定的时间间隔执行,如鼠标拖动窗口缩放。
function throttle (fn, ms = 200) {
// 创建一个函数作用域,并初始化变量 timeout
let timeout = null;
return function () {
// 上一次循环没有执行,则直接跳过
if (timeout) {
// 节流和防抖的区别,即在此处不会清除前一次创建
return;
}
timeout = setTimeout(() => {
fn.apply(this, arguments);
// 可以执行下一次循环了
timeout = null;
}, ms)
}
}
3.深拷贝
深拷贝,其主要就是依据数据值类型区分进行递归
function deepClone(value) {
// 简单值,直接返回
if (typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') {
return value;
}
// undefined null
if (typeof value === 'undefined' || (typeof value === 'object' && !value )) {
return value;
}
// Date
if (target instanceof Date) {
return new Date(target);
}
// RegExp
if (target instanceof RegExp) {
return new RegExp(target);
}
// Array
if (Array.isArray(value)) {
const newArr = [];
value.forEach(item => {
newArr.push(deepClone(item))
})
return newArr;
}
// Object
if (typeof value === 'object' && value) {
const newObj = {};
for (let k in value) {
newObj[k] = deepClone(value[k])
}
return newObj
}
// function 函数拷贝的意义不大,可直接返回
if (typeof value === 'function') {
return value;
}
}
4. call apply实现
call和apply的实现基本一致,区别在于传递参数,这里以call举例
Function.prototype._call = function(ctx){
// ctx 指向修改对象,window 或者 obj
// this 指向 _call的调用着 fn
// 调用对象为function
if (typeof this !== 'function') console.error('type error')
// 默认指向的对象,如果不存在默认指向window
ctx = ctx || window;
// 唯一值
let fn = Symbol();
// _call函数的this指向调用函数,如fn._call(), _call函数里面的this指向fn, 即将fn作为ctx的一个属性指向的函数
ctx[fn] = this;
// args,保存第一个参数外的所有参数
let args = [...arguments].splice(1);
// ctx[fn],指向fn函数,调用即修改fn函数的this指向,并传入参数
let result = ctx[fn](...args);
// 删除属性
delete ctx[fn];
// 返回函数执行的结果
return result;
}
5. 函数柯里化
柯里化,即接受多个参数的函数调用,可以分次传入参数调用
function curryFn (fn) {
if (typeof fn !== 'function') {
console.error('curryFn arguments required function')
}
// 返回一个函数
return function curried () {
// 判断当前函数已经接受的参数个数,与fn本身需要的参数个数是否一致,一致则执行fn并返回结果
if (arguments.length >= fn.length) {
return fn.apply(this, arguments)
} else {
// 缓存之前参数
var arg = arguments;
// 再返回一个匿名函数,接受剩余不够参数
return function () {
return curried.apply(this, [...arg, ...arguments]) // 之前参数,当前参数
}
}
}
}
6. 函数组合
函数组合,是函数式编程对函数的使用技巧。
function compose (...funcs) {
// funcs,传递的函数集合
return function proxy(...args) {
// args, 第一次调用函数传递的参数
const funLen = funcs.length;
// 没有fn,直接返回参数
if (funLen.length === 0) {
return args;
}
// 只有一个fn,执行fn并返回结果
if (funLen.length === 1) {
return funLen[0](...args)
}
// 多个fn
funcs.reverse().reduce((x,y) => {
return typeof x === 'function' ? y(x(...args)) : y(x)
})
}
}
7. 铺平数组
数组扁平化,即将一个多维的数组,展开成一个一维数组返回,其主要通过循环遍历数组每一项值,值为数组类型则进行递归。
/**
* 数组扁平化
*
* @param {Array} arr
* @return {Array}
*/
function flatArr(arr) {
// 非数组直接返回
if(!Array.isArray(arr)) {
return arr;
}
const _crr = [];
// 递归
arr.forEach(v => {
if(Array.isArray(v)) {
// 值为数组,递归
_crr.push(...flatArr(v))
} else {
// 非数组,则直接添加
_crr.push(v)
}
})
return _crr;
}
8. arguments的注意点
function test(){
console.log(arguments)
}
- arguments是一个对应于传递给函数的参数的--类数组
- 箭头函数没有arguments
- 通过arguments可以访问和设置函数参数
9. 平行结构树数据构建树数据
/**
* @describe 平行结构树数据转树结构
*
* @param {Array} [list]
* @return {Array}
*/
function listToTree (list = []) {
// 限制元数据类型为数组
if (!(list instanceof Array)) {
return []
}
if (list.length === 0) {
return []
}
const createObj = {};
// map id--item
for (let i = 0, l = list.length; i<l; i++) {
if (!createObj[list[i].id]) {
createObj[list[i].id] = list[i];
}
}
// 构建树,利用了引用数据类型的特殊性
for (let i = 0, l = list.length; i<l; i++) {
if (list[i].pid) {
createObj[list[i].pid].children ? createObj[list[i].pid].children.push(list[i]) : createObj[list[i].pid].children = [list[i]]
}
}
const treeArr = [];
// 过滤掉多余子节点,剩余根节点
for (let k in createObj) {
if (!createObj[k].pid) {
treeArr.push(createObj[k])
}
}
return treeArr;
}
10. 树数据扁平化
/**
* @describe 树数据扁平化
*
* @param {Array} [tree]
* @return {Array}
*/
function flatTreeToArr(tree = []) {
if (!(tree instanceof Array)) {
return []
}
if (tree.length === 0) {
return tree;
}
let createArr = [];
for (let i = 0, l = tree.length; i < l; i++) {
createArr.push(tree[i])
// 递归
if (tree[i].children && tree[i].children.length > 0) {
createArr = createArr.concat(flatTreeToArr(tree[i].children))
}
// 删除原有children
delete tree[i].children
}
return createArr;
}
11. 两个数组相加
说明,a = [1,2,3,4,5], b=[7,8,9],返回 [1,3,1,3,4],即从两个数组的右边开始相加,返回加之后的数组
/**
* @describe 找出两个数组中长度较小的一个
*
* @param {*} l1
* @param {*} l2
* @return {*}
*/
function findMin(l1,l2){
return l1.length - l2.length > 0 ? l2 : l1;
}
/**
* @describe 找出两个数组中长度较大的一个
*
* @param {*} l1
* @param {*} l2
* @return {*}
*/
function findMax(l1, l2) {
return l1.length - l2.length > 0 ? l1 : l2;
}
/**
*
*
*/
function addTwoNumbers(l1, l2) {
if (l1.length === 0) {
return l2
}
if(l2.length === 0){
return l1
}
const ma = findMax(l1,l2)
const mi = findMin(l1,l2)
// 暂存相加
const rnArr = [];
// 进位值
let add = 0;
// 较大长度索引
let maLength = ma.length - 1;
// 倒序遍历较小长度一项
for(i = mi.length - 1; i>=0; i--) {
if(mi[i] + ma[maLength] >= 10) {
// 相加大于10,进位1
rnArr.unshift(mi[i] + ma[maLength] - 10 + add)
add = 1;
} else {
// 相加小于10,进位0
rnArr.unshift(mi[i] + ma[maLength])
add = 0;
}
// 较大索引左移动
maLength--;
}
// 较大剩余未加部分
const moreArr = ma.splice(0, ma.length - mi.length);
// 是否含有进位
add > 0 ? moreArr[moreArr.length - 1] = moreArr[moreArr.length - 1] + 1 : null;
return [...moreArr,...rnArr]
};
12. 数字项数组最后一项加1
说明,即最后一项加1,返回加之后的数组,这里主要考虑了数组的长度不固定,很可能超过javascript的最大精度表示,所以采用了BigInt
function plusOne (digits) {
// 空数组
if(digits.length === 0) {
return digits;
}
// 转为数字
const numArr = BigInt(digits.join(''))
// 加1和
const total = numArr + BigInt(1);
// 数字转数组,由于每一项是字符串,再转数字
return total.toString().split('').map(item => Number(item))
};
13. 无限加
此方法跟函数柯里化比较像,不过柯里化是讲一个接受固定参数的函数柯里化,此处是函数不固定参数,可以随意调用
add(1)(2)(3) = 6
add(1)(2)(3)(4) = 10
function moreAdd(x) {
// 缓存和
let sum = x;
// 返回一个函数,接受参数并处理 和 + 新参数,再返回这个函数,无限循环
let temp = function (item) {
sum += item;
return temp;
}
// 每个对象都有toString()方法,重写toString(),调用返回函数的toString()返回结果
temp.toString = () => sum
return temp;
}
console.log(moreAdd(1)(2)(3).toString())
14. 是否是回文字符串
回文,即左边开始读和右边开始读一样
/**
* 验证一个字符串是否是回文,忽略大小写和其他字符
*
* @param {string} s
* @return {boolean}
*/
function isPalinStr(str){
// 空或1个字符
if(str.length < 2) {
return true;
}
// 匹配非 0-9,a-z
const reg = /[^0-9a-z]/g;
// 不区分大小写
const smallStr = str.toLowerCase();
// 忽略其他符号
const realStr = smallStr.replace(reg, '');
// 中间位置
const midleIndex = parseInt(realStr.length / 2);
let result = true;
for(let i = 0; i < midleIndex; i++) {
if(realStr.charAt(i) !== realStr.charAt(realStr.length - 1 - i)) {
result = false;
break;
}
}
return result;
}
15. 有效的括号
/**
*
* ({[()]})
* 利用栈,遍历字符串,遇到左括号,压入栈,遇到右括号,弹出栈顶的值必须和现在的右括号成对。
* @param {string} str
* @return {boolean}
*/
function isValidKuoHao(str){
const keyMap = {
"(": ")",
"{": "}",
"[": "]"
}
// 空字符 false
if(str.length===0) {
return false;
}
// 单字符
if(str.length === 1) {
// 一个左括号,false
if(keyMap[str]) {
return false
}
// 不是左括号,也不是右括号 ?
return !keyMap[str] && !Object.values(keyMap).includes(str) ? true : false;
}
let result = true;
// 模拟栈
const keyArr = [];
for(let i = 0, l = str.length; i < l; i++) {
// 左括号,压栈
if(keyMap[str.charAt(i)]) {
keyArr.push(str.charAt(i))
}
// 右括号
if(Object.values(keyMap).includes(str.charAt(i))) {
// 先弹出一个值
if(keyMap[keyArr.pop()] !== str.charAt(i)) {
// 弹出的左括号如果和现在的右括号不成对,直接返回false
result = false;
break
}
}
}
// 栈为空,则是有效的括号,否则不是
keyArr.length > 0 ? result = false : null;
return result;
}
16. 最长公共前缀
给定一个数组['asd', 'as', 'asdfr'],寻找该数组公共前缀最长字符
/**
*
*
* @param {string[]} strs
* @return {string}
*/
function longestCommonPrefix(strs){
// 空数组
if (strs.length === 0) {
return '';
}
// 只含一位,公共全部
if (strs.length === 1) {
return strs[0]
}
// 保存数组每一项字符串长度
const itemStrLengthmap = {};
strs.forEach((item, index) => {
itemStrLengthmap[index] = item.length;
})
// 获取所有字符串长度
const lengthArr = Object.values(itemStrLengthmap);
// 包含空字符串
if(lengthArr.includes(0)){
return ''
}
let result = '';
// 最小长度
const minLen = Math.min.apply(null, lengthArr);
w:for(let k =0; k <= minLen; k++) {
const activeStr = strs[0].charAt(k);
i:for(let i = 0, l = strs.length; i<l; i++) {
if(strs[i].charAt(k) !== activeStr) {
break w;
}
if(i === l - 1) {
result += activeStr;
}
}
}
return result;
}
17. 合并两个有序数组
/**
* 合并两个有序数组
*
* @param {Array} list1
* @param {Array} list2
* @return {Array}
*/
function mergeTwoLists (list1,list2) {
// 记录插入的位置,下次遍历从此开始,因为是有序数组
let forIndex = 0;
// 遍历list1,向list2插入
w:for(let i = 0, l = list1.length; i<l; i++) {
i:for(let k = forIndex; k < list2.length; k++) {
if(list1[i] <= list2[k]) {
list2.splice(k, 0, list1[i])
// 记录插入位置
forIndex = k;
break i;
}
if(k === list2.length - 1 && list1[i] > list2[k]) {
// 添加最后
list2.push(list1[i])
// 记录插入位置
forIndex = k + 1;
break i;
}
}
}
return list2
}
18. 未重复字符串最长字符
举例,qqqqweeee,未重复最长字符为,qwe
var lengthOfLongestSubstring = function(s) {
if(s.length < 2) {
return s
}
const obj = {};
const toMap = function (s) {
// 字符串转数组
const arr = s.split('');
// 保存未重复字符
const startArr = [];
for(let i = 0, l = arr.length; i < l; i++) {
if (startArr.includes(arr[i])) {
// 保存当前未重复字符
obj[startArr.length] = startArr.join('')
const findRepeatOldIndex = startArr.findIndex(n => n === arr[i])
// 未重复字符串保留当前重复位置的右侧字符
const concatArr = startArr.splice(findRepeatOldIndex + 1);
// 右侧字符和未遍历字符🈴️成新的字符串,重新开始寻找
const yuArr = [...concatArr, ...arr.splice(i)];
yuArr.length > 0 ?toMap(yuArr.join('')) : null;
break;
} else {
startArr.push(arr[i])
}
}
obj[startArr.length] = startArr.join('')
}
toMap(s)
return obj[Math.max(...Object.keys(obj))]
};
19. 原地删除有序数组重复项
说明,重点在于原地删除,即返回的数组等于原数组
/**
*
*
* @param {Array} arr
* @return {number}
* @descripe 原地删除有序数组重复项
*/
// 原地删除,即返回的数组==原数组
function removeRepeatArr(arr){
// 移动卡尺
let step = 0;
for(let i = step; i < arr.length; i+=step) {
if(arr[i] === arr[i + 1]) {
// 比较当前元素和下一位元素是否重复,重复则删除下一位,循环位置不变,继续与下一个元素比较
arr.splice(i+1, 1)
// 不移动
step = 0
} else {
// 当前元素和下一个原属不重复,循环位置递增1,移动下一位
step = 1;
}
}
return arr;
}
写在最后
刀越磨越利,脑越用越灵,一起加油!