前言
基本上面试的时候,经常会遇到手撕 XXX 之类的问题,这次准备梳理总结一遍,巩固我们原生 JS 基础的同时,下次想复习面试手撕题的时候,找起来方便,也节省时间。
代码在这里 👉GitHub
梳理的顺序是随机的,不按照难以程度。
实现一个事件委托(易错)
事件委托这里就不阐述了,比如给 li 绑定点击事件
看错误版,(容易过的,看「面试官水平了」)👇
ul.addEventListener("click", function (e) { console.log(e, e.target); if (e.target.tagName.toLowerCase() === "li") { console.log("打印"); // 模拟fn } });
「有个小 bug,如果用户点击的是 li 里面的 span,就没法触发 fn,这显然不对」👇
<ul id="xxx"> 下面的内容是子元素1 <li> li内容>>> <span> 这是span内容123</span> </li> 下面的内容是子元素2 <li> li内容>>> <span> 这是span内容123</span> </li> 下面的内容是子元素3 <li> li内容>>> <span> 这是span内容123</span> </li> </ul>
这样子的场景就是不对的,那我们看看高级版本 👇
function delegate(element, eventType, selector, fn) { element.addEventListener( eventType, (e) => { let el = e.target; while (!el.matches(selector)) { if (element === el) { el = null; break; } el = el.parentNode; } el && fn.call(el, e, el); }, true ); return element; }
实现一个可以拖拽的 DIV
这个题目看起来简单,你可以试一试 30 分钟能不能完成,直接贴出代码吧 👇
<div id="xxx"></div>
var dragging = false; var position = null; xxx.addEventListener("mousedown", function (e) { dragging = true; position = [e.clientX, e.clientY]; }); document.addEventListener("mousemove", function (e) { if (dragging === false) return null; const x = e.clientX; const y = e.clientY; const deltaX = x - position[0]; const deltaY = y - position[1]; const left = parseInt(xxx.style.left || 0); const top = parseInt(xxx.style.top || 0); xxx.style.left = left + deltaX + "px"; xxx.style.top = top + deltaY + "px"; position = [x, y]; }); document.addEventListener("mouseup", function (e) { dragging = false; });
手写防抖和节流函数
「节流 throttle」,规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。场景 👇
- scroll 滚动事件,每隔特定描述执行回调函数
- input 输入框,每个特定时间发送请求或是展开下拉列表,(防抖也可以)
节流重在加锁「flag = false」
function throttle(fn, delay) { let flag = true, timer = null; return function (...args) { let context = this; if (!flag) return; flag = false; clearTimeout(timer); timer = setTimeout(function () { fn.apply(context, args); flag = true; }, delay); }; }
「防抖 debounce」,在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。场景 👇
- 浏览器窗口大小 resize 避免次数过于频繁
- 登录,发短信等按钮避免发送多次请求
- 文本编辑器实时保存
防抖重在清零「clearTimeout(timer)」
function debounce(fn, delay) { let timer = null; return function (...args) { let context = this; if (timer) clearTimeout(timer); timer = setTimeout(function () { fn.apply(context, args); }, delay); }; }
实现数组去重
这个是 Array 数组测试用例 👇
var array = [ 1, 1, "1", "1", null, null, undefined, undefined, new String("1"), new String("1"), /a/, /a/, NaN, NaN, ];
如何通过一个数组去重,给面试官留下深印象呢 👇
使用 Set
let unique_1 = (arr) => [...new Set(arr)];
使用 filter
function unique_2(array) { var res = array.filter(function (item, index, array) { return array.indexOf(item) === index; }); return res; }
使用 reduce
let unique_3 = (arr) => arr.reduce((pre, cur) => (pre.includes(cur) ? pre : [...pre, cur]), []);
使用 Object 键值对 🐂🐂,这个也是去重最好的效果 👇
function unique_3(array) { var obj = {}; return array.filter(function (item, index, array) { return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true); }); }
使用obj[typeof item + item] = true
,原因就在于对象的键值只能是字符串
,所以使用typeof item + item
代替
实现柯里化函数
柯里化就是把接受「多个参数」的函数变换成接受一个「单一参数」的函数,并且返回接受「余下参数」返回结果的一种应用。
思路:
- 判断传递的参数是否达到执行函数的 fn 个数
- 没有达到的话,继续返回新的函数,并且返回 curry 函数传递剩余参数
let currying = (fn, ...args) => fn.length > args.length ? (...arguments) => currying(fn, ...args, ...arguments) : fn(...args);
测试用例 👇
let addSum = (a, b, c) => a + b + c; let add = curry(addSum); console.log(add(1)(2)(3)); console.log(add(1, 2)(3)); console.log(add(1, 2, 3));
实现数组 flat
「将多维度的数组降为一维数组」
Array.prototype.flat(num) // num表示的是维度 // 指定要提取嵌套数组的结构深度,默认值为 1 使用 Infinity,可展开任意深度的嵌套数组
写这个给面试官看的话,嗯嗯,应该会被打死,写一个比较容易的 👇
let flatDeep = (arr) => { return arr.reduce((res, cur) => { if (Array.isArray(cur)) { return [...res, ...flatDep(cur)]; } else { return [...res, cur]; } }, []); };
「你想给面试官留下一个深刻印象的话」,可以这么写,👇
function flatDeep(arr, d = 1) { return d > 0 ? arr.reduce( (acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), [] ) : arr.slice(); } // var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]]; // flatDeep(arr1, Infinity);
可以传递一个参数,数组扁平化几维,简单明了,看起来逼格满满 🐂🐂🐂
深拷贝
深拷贝解决的就是「共用内存地址所导致的数据错乱问题」
思路:
- 递归
- 判断类型
- 检查环(也叫循环引用)
- 需要忽略原型
function deepClone(obj, map = new WeakMap()) { if (obj instanceof RegExp) return new RegExp(obj); if (obj instanceof Date) return new Date(obj); if (obj == null || typeof obj != "object") return obj; if (map.has(obj)) { return map.get(obj); } let t = new obj.constructor(); map.set(obj, t); for (let key in obj) { if (obj.hasOwnProperty(key)) { t[key] = deepClone(obj[key], map); } } return t; } //测试用例 let obj = { a: 1, b: { c: 2, d: 3, }, d: new RegExp(/^\s+|\s$/g), }; let clone_obj = deepClone(obj); obj.d = /^\s|[0-9]+$/g; console.log(clone_obj); console.log(obj);
实现一个对象类型的函数
核心:Object.prototype.toString
let isType = (type) => (obj) => Object.prototype.toString.call(obj) === `[object ${type}]`; // let isArray = isType('Array') // let isFunction = isType('Function') // console.log(isArray([1,2,3]),isFunction(Map))
isType 函数 👆,也属于「偏函数」的范畴,偏函数实际上是返回了一个包含「预处理参数」的新函数。
「一劳永逸」送你21道高频JavaScript手写面试题(下):https://developer.aliyun.com/article/1483403