文章轉載自https://mp.weixin.qq.com/s/xy0aeBt5yDbivvnPqIAnsg
前言
基本上面試的時候,經常會遇到手撕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分鍾能不能完成,直接貼出代碼吧👇
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函數👆,也屬於「偏函數」的范疇,偏函數實際上是返回了一個包含「預處理參數」的新函數。
手寫call和apply
改變this指向,唯一區別就是傳遞參數不同👇
// 實現call Function.prototype.mycall = function () { let [thisArg, ...args] = [...arguments] thisArg = Object(thisArg) || window let fn = Symbol() thisArg[fn] = this let result = thisArg[fn](...args) delete thisArg[fn] return result } // 實現apply Function.prototype.myapply = function () { let [thisArg, args] = [...arguments]; thisArg = Object(thisArg) let fn = Symbol() thisArg[fn] = this; let result = thisArg[fn](...args); delete thisArg.fn; return result; } //測試用例 let cc = { a: 1 } function demo(x1, x2) { console.log(typeof this, this.a, this) console.log(x1, x2) } demo.apply(cc, [2, 3]) demo.myapply(cc, [2, 3]) demo.call(cc,33,44) demo.mycall(cc,33,44)
手寫bind
bind它並不是立馬執行函數,而是有一個延遲執行的操作,就是生成了一個新的函數,需要你去執行它👇
// 實現bind Function.prototype.mybind = function(context, ...args){ return (...newArgs) => { return this.call(context,...args, ...newArgs) } } // 測試用例 let cc = { name : 'TianTian' } function say(something,other){ console.log(`I want to tell ${this.name} ${something}`); console.log('This is some'+other) } let tmp = say.mybind(cc,'happy','you are kute') let tmp1 = say.bind(cc,'happy','you are kute') tmp() tmp1()
實現new操作
核心要點👇
- 創建一個新對象,這個對象的
__proto__
要指向構造函數的原型對象 - 執行構造函數
- 返回值為object類型則作為new方法的返回值返回,否則返回上述全新對象
代碼如下👇
function _new() { let obj = {}; let [constructor, ...args] = [...arguments]; obj.__proto__ = constructor.prototype; let result = constructor.apply(obj, args); if (result && typeof result === 'function' || typeof result === 'object') { return result; } return obj; }
實現instanceof
「instanceof」 「運算符」用於檢測構造函數的 prototype
屬性是否出現在某個實例對象的原型鏈上。
語法👇
object instanceof constructor object 某個實例對象 construtor 某個構造函數
原型鏈的向上找,找到原型的最頂端,也就是Object.prototype,代碼👇
function my_instance_of(leftVaule, rightVaule) { if(typeof leftVaule !== 'object' || leftVaule === null) return false; let rightProto = rightVaule.prototype, leftProto = leftVaule.__proto__; while (true) { if (leftProto === null) { return false; } if (leftProto === rightProto) { return true; } leftProto = leftProto.__proto__ } }
實現sleep
某個時間后就去執行某個函數,使用Promise封裝👇
function sleep(fn, time) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(fn); }, time); }); } let saySomething = (name) => console.log(`hello,${name}`) async function autoPlay() { let demo = await sleep(saySomething('TianTian'),1000) let demo2 = await sleep(saySomething('李磊'),1000) let demo3 = await sleep(saySomething('掘金的好友們'),1000) } autoPlay()
實現數組reduce
更多的手寫實現數組方法,看我之前這篇👉 「數組方法」從詳細操作js數組到淺析v8中array.js
直接給出簡易版👇
Array.prototype.myreduce = function(fn, initVal) { let result = initVal, i = 0; if(typeof initVal === 'undefined'){ result = this[i] i++; } while( i < this.length ){ result = fn(result, this[i]) } return result }
實現Promise.all和race
不清楚兩者用法的話,異步MDN👉Promise.race() Promise.all()
// 實現Promise.all 以及 race Promise.myall = function (arr) { return new Promise((resolve, reject) => { if (arr.length === 0) { return resolve([]) } else { let res = [], count = 0 for (let i = 0; i < arr.length; i++) { // 同時也能處理arr數組中非Promise對象 if (!(arr[i] instanceof Promise)) { res[i] = arr[i] if (++count === arr.length) resolve(res) } else { arr[i].then(data => { res[i] = data if (++count === arr.length) resolve(res) }, err => { reject(err) }) } } } }) } Promise.myrace = function (arr) { return new Promise((resolve, reject) => { for (let i = 0; i < arr.length; i++) { // 同時也能處理arr數組中非Promise對象 if (!(arr[i] instanceof Promise)) { Promise.resolve(arr[i]).then(resolve, reject) } else { arr[i].then(resolve, reject) } } }) }
測試用例👇
// 測試用例 let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve(11) }, 2000); }); let p2 = new Promise((resolve, reject) => { reject('asfs') }); let p3 = new Promise((resolve) => { setTimeout(() => { resolve(33); }, 4); }); Promise.myall([p3, p1, 3, 4]).then(data => { // 按傳入數組的順序打印 console.log(data); // [3, 1, 2] }, err => { console.log(err) }); Promise.myrace([p1, p2, p3]).then(data => { // 誰快就是誰 console.log(data); // 2 }, err => { console.log('失敗跑的最快') })
手寫繼承
繼承有很多方式,這里不過多追溯了,可以看看這篇 JS原型鏈與繼承別再被問倒了
主要梳理的是 寄生組合式繼承 和Class繼承怎么使用
「寄生組合式繼承」
function inheritPrototype(subType, superType) { // 創建對象,創建父類原型的一個副本 var prototype = Object.create(superType.prototype); // 增強對象,彌補因重寫原型而失去的默認的constructor 屬性 prototype.constructor = subType; // 指定對象,將新創建的對象賦值給子類的原型 subType.prototype = prototype; }
測試用例👇
// 父類初始化實例屬性和原型屬性 function Father(name) { this.name = name; this.colors = ["red", "blue", "green"]; } Father.prototype.sayName = function () { alert(this.name); }; // 借用構造函數傳遞增強子類實例屬性(支持傳參和避免篡改) function Son(name, age) { Father.call(this, name); this.age = age; } // 將父類原型指向子類 inheritPrototype(Son, Father); // 新增子類原型屬性 Son.prototype.sayAge = function () { alert(this.age); } var demo1 = new Son("TianTian", 21); var demo2 = new Son("TianTianUp", 20); demo1.colors.push("2"); // ["red", "blue", "green", "2"] demo2.colors.push("3"); // ["red", "blue", "green", "3"]
Class實現繼承👇
class Rectangle { // constructor constructor(height, width) { this.height = height; this.width = width; } // Getter get area() { return this.calcArea() } // Method calcArea() { return this.height * this.width; } } const rectangle = new Rectangle(40, 20); console.log(rectangle.area); // 輸出 800 // 繼承 class Square extends Rectangle { constructor(len) { // 子類沒有this,必須先調用super super(len, len); // 如果子類中存在構造函數,則需要在使用“this”之前首先調用 super()。 this.name = 'SquareIng'; } get area() { return this.height * this.width; } } const square = new Square(20); console.log(square.area); // 輸出 400
extends
繼承的核心代碼如下,其實和上述的寄生組合式繼承方式一樣👇
function _inherits(subType, superType) { // 創建對象,創建父類原型的一個副本 // 增強對象,彌補因重寫原型而失去的默認的constructor 屬性 // 指定對象,將新創建的對象賦值給子類的原型 subType.prototype = Object.create(superType && superType.prototype, { constructor: { value: subType, enumerable: false, writable: true, configurable: true } }); if (superType) { Object.setPrototypeOf ? Object.setPrototypeOf(subType, superType) : subType.__proto__ = superType; } }
把實現原理跟面試官扯一扯,這小子基礎還行。
手寫一下AJAX
寫的初略版的,詳細版的就不梳理了,面試的時候,跟面試官好好探討一下吧🐂🐂🐂
var request = new XMLHttpRequest() request.open('GET', 'index/a/b/c?name=TianTian', true); request.onreadystatechange = function () { if(request.readyState === 4 && request.status === 200) { console.log(request.responseText); }}; request.send();
用正則實現 trim()
去掉首位多余的空格👇
String.prototype.trim = function(){ return this.replace(/^\s+|\s+$/g, '') } //或者 function trim(string){ return string.replace(/^\s+|\s+$/g, '') }
實現Object.create方法
//實現Object.create方法 function create(proto) { function Fn() {}; Fn.prototype = proto; Fn.prototype.constructor = Fn; return new Fn(); } let demo = { c : '123' } let cc = Object.create(demo)
實現一個同時允許任務數量最大為n的函數
使用Promise封裝,給你一個數組,數組的每一項是一個Promise對象
function limitRunTask(tasks, n) { return new Promise((resolve, reject) => { let index = 0, finish = 0, start = 0, res = []; function run() { if (finish == tasks.length) { resolve(res); return; } while (start < n && index < tasks.length) { // 每一階段的任務數量++ start++; let cur = index; tasks[index++]().then(v => { start--; finish++; res[cur] = v; run(); }); } } run(); }) // 大概解釋一下:首先如何限制最大數量n // while 循環start < n,然后就是then的回調 }
10進制轉換
給定10進制數,轉換成[2~16]進制區間數,就是簡單模擬一下。
function Conver(number, base = 2) { let rem, res = '', digits = '0123456789ABCDEF', stack = []; while (number) { rem = number % base; stack.push(rem); number = Math.floor(number / base); } while (stack.length) { res += digits[stack.pop()].toString(); } return res; }
數字轉字符串千分位
寫出這個就很逼格滿滿🐂🐂🐂
function thousandth(str) { return str.replace(/\d(?=(?:\d{3})+(?:\.\d+|$))/g, '$&,'); }