欄目為大家介紹32個純手寫JS,鞏固JS基礎(面試高頻),一起學習吧。
作為前端開發,JS是重中之重,最近結束了面試的高峰期,基本上offer也定下來了就等開獎,趁著這個時間總結下32個手寫JS問題,這些都是高頻面試題,希望對你能有所幫助。
關於原始碼都緊遵規範,都可跑通MDN範例,其餘的大多會涉及一些關於JS的應用題和本人面試過程
陣列扁平化是指將一個多維陣列變為一個一維陣列
const arr = [1, [2, [3, [4, 5]]], 6];// => [1, 2, 3, 4, 5, 6]複製程式碼
const res1 = arr.flat(Infinity);複製程式碼
const res2 = JSON.stringify(arr).replace(/\[|\]/g, '').split(',');複製程式碼
但資料型別都會變為字串
const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']');複製程式碼
const flatten = arr => { return arr.reduce((pre, cur) => { return pre.concat(Array.isArray(cur) ? flatten(cur) : cur); }, []) }const res4 = flatten(arr);複製程式碼
const res5 = [];const fn = arr => { for (let i = 0; i < arr.length; i++) { if (Array.isArray(arr[i])) { fn(arr[i]); } else { res5.push(arr[i]); } } } fn(arr);複製程式碼
const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];// => [1, '1', 17, true, false, 'true', 'a', {}, {}]複製程式碼
const res1 = Array.from(new Set(arr));複製程式碼
const unique1 = arr => { let len = arr.length; for (let i = 0; i < len; i++) { for (let j = i + 1; j < len; j++) { if (arr[i] === arr[j]) { arr.splice(j, 1); // 每刪除一個樹,j--保證j的值經過自加後不變。同時,len--,減少迴圈次數提升效能 len--; j--; } } } return arr; }複製程式碼
const unique2 = arr => { const res = []; for (let i = 0; i < arr.length; i++) { if (res.indexOf(arr[i]) === -1) res.push(arr[i]); } return res; }複製程式碼
當然也可以用include、filter,思路大同小異。
const unique3 = arr => { const res = []; for (let i = 0; i < arr.length; i++) { if (!res.includes(arr[i])) res.push(arr[i]); } return res; }複製程式碼
const unique4 = arr => { return arr.filter((item, index) => { return arr.indexOf(item) === index; }); }複製程式碼
const unique5 = arr => { const map = new Map(); const res = []; for (let i = 0; i < arr.length; i++) { if (!map.has(arr[i])) { map.set(arr[i], true) res.push(arr[i]); } } return res; }複製程式碼
類陣列是具有length屬性,但不具有陣列原型上的方法。常見的類陣列有arguments、DOM操作方法返回的結果。
Array.from(document.querySelectorAll('p'))複製程式碼
Array.prototype.slice.call(document.querySelectorAll('p'))複製程式碼
[...document.querySelectorAll('p')]複製程式碼
Array.prototype.concat.apply([], document.querySelectorAll('p'));複製程式碼
Array.prototype.filter = function(callback, thisArg) { if (this == undefined) { throw new TypeError('this is null or not undefined'); } if (typeof callback !== 'function') { throw new TypeError(callback + 'is not a function'); } const res = []; // 讓O成為回撥函數的物件傳遞(強制轉換物件) const O = Object(this); // >>>0 保證len為number,且為正整數 const len = O.length >>> 0; for (let i = 0; i < len; i++) { // 檢查i是否在O的屬性(會檢查原型鏈) if (i in O) { // 回撥函數呼叫傳參 if (callback.call(thisArg, O[i], i, O)) { res.push(O[i]); } } } return res; }複製程式碼
對於>>>0
有疑問的:解釋>>>0的作用
Array.prototype.map = function(callback, thisArg) { if (this == undefined) { throw new TypeError('this is null or not defined'); } if (typeof callback !== 'function') { throw new TypeError(callback + ' is not a function'); } const res = []; // 同理 const O = Object(this); const len = O.length >>> 0; for (let i = 0; i < len; i++) { if (i in O) { // 呼叫回撥函數並傳入新陣列 res[i] = callback.call(thisArg, O[i], i, this); } } return res; }複製程式碼
forEach
跟map類似,唯一不同的是forEach
是沒有返回值的。
Array.prototype.forEach = function(callback, thisArg) { if (this == null) { throw new TypeError('this is null or not defined'); } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function'); } const O = Object(this); const len = O.length >>> 0; let k = 0; while (k < len) { if (k in O) { callback.call(thisArg, O[k], k, O); } k++; } }複製程式碼
Array.prototype.reduce = function(callback, initialValue) { if (this == undefined) { throw new TypeError('this is null or not defined'); } if (typeof callback !== 'function') { throw new TypeError(callbackfn + ' is not a function'); } const O = Object(this); const len = this.length >>> 0; let accumulator = initialValue; let k = 0; // 如果第二個引數為undefined的情況下 // 則陣列的第一個有效值作為累加器的初始值 if (accumulator === undefined) { while (k < len && !(k in O)) { k++; } // 如果超出陣列界限還沒有找到累加器的初始值,則TypeError if (k >= len) { throw new TypeError('Reduce of empty array with no initial value'); } accumulator = O[k++]; } while (k < len) { if (k in O) { accumulator = callback.call(undefined, accumulator, O[k], k, O); } k++; } return accumulator; }複製程式碼
第一個引數是繫結的this,預設為window
,第二個引數是陣列或類陣列
Function.prototype.apply = function(context = window, args) { if (typeof this !== 'function') { throw new TypeError('Type Error'); } const fn = Symbol('fn'); context[fn] = this; const res = context[fn](...args); delete context[fn]; return res; }複製程式碼
於call
唯一不同的是,call()
方法接受的是一個參數列
Function.prototype.call = function(context = window, ...args) { if (typeof this !== 'function') { throw new TypeError('Type Error'); } const fn = Symbol('fn'); context[fn] = this; const res = context[fn](...args); delete context[fn]; return res; }複製程式碼
Function.prototype.bind = function(context, ...args) { if (typeof this !== 'function') { throw new Error("Type Error"); } // 儲存this的值 var self = this; return function F() { // 考慮new的情況 if(this instanceof F) { return new self(...args, ...arguments) } return self.apply(context, [...args, ...arguments]) } }複製程式碼
觸發高頻時間後n秒內函數只會執行一次,如果n秒內高頻時間再次觸發,則重新計算時間。
const debounce = (fn, time) => { let timeout = null; return function() { clearTimeout(timeout) timeout = setTimeout(() => { fn.apply(this, arguments); }, time); } };複製程式碼
防抖常應用於使用者進行搜尋輸入節約請求資源,window
觸發resize
事件時進行防抖只觸發一次。
高頻時間觸發,但n秒內只會執行一次,所以節流會稀釋函數的執行頻率。
const throttle = (fn, time) => { let flag = true; return function() { if (!flag) return; flag = false; setTimeout(() => { fn.apply(this, arguments); flag = true; }, time); } }複製程式碼
節流常應用於滑鼠不斷點選觸發、監聽捲動事件。
指的是將一個接受多個引數的函數 變為 接受一個引數返回一個函數的固定形式,這樣便於再次呼叫,例如f(1)(2)
經典面試題:實現add(1)(2)(3)(4)=10;
、 add(1)(1,2,3)(2)=9;
function add() { const _args = [...arguments]; function fn() { _args.push(...arguments); return fn; } fn.toString = function() { return _args.reduce((sum, cur) => sum + cur); } return fn; }複製程式碼
3個步驟:
ctor.prototype
為原型建立一個物件。function newOperator(ctor, ...args) { if (typeof ctor !== 'function') { throw new TypeError('Type Error'); } const obj = Object.create(ctor.prototype); const res = ctor.apply(obj, args); const isObject = typeof res === 'object' && res !== null; const isFunction = typeof res === 'function'; return isObject || isFunction ? res : obj; }複製程式碼
instanceof
運運算元用於檢測建構函式的prototype
屬性是否出現在某個範例物件的原型鏈上。
const myInstanceof = (left, right) => { // 基本資料型別都返回false if (typeof left !== 'object' || left === null) return false; let proto = Object.getPrototypeOf(left); while (true) { if (proto === null) return false; if (proto === right.prototype) return true; proto = Object.getPrototypeOf(proto); } }複製程式碼
這裡只寫寄生組合繼承了,中間還有幾個演變過來的繼承但都有一些缺陷
function Parent() { this.name = 'parent'; }function Child() { Parent.call(this); this.type = 'children'; } Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child;複製程式碼
Object.is
解決的主要是這兩個問題:
+0 === -0 // true NaN === NaN // false複製程式碼
const is= (x, y) => { if (x === y) { // +0和-0應該不相等 return x !== 0 || y !== 0 || 1/x === 1/y; } else { return x !== x && y !== y; } }複製程式碼
Object.assign()
方法用於將所有可列舉屬性的值從一個或多個源物件複製到目標物件。它將返回目標物件(請注意這個操作是淺拷貝)
Object.defineProperty(Object, 'assign', { value: function(target, ...args) { if (target == null) { return new TypeError('Cannot convert undefined or null to object'); } // 目標物件需要統一是參照資料型別,若不是會自動轉換 const to = Object(target); for (let i = 0; i < args.length; i++) { // 每一個源物件 const nextSource = args[i]; if (nextSource !== null) { // 使用for...in和hasOwnProperty雙重判斷,確保只拿到本身的屬性、方法(不包含繼承的) for (const nextKey in nextSource) { if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; } } } } return to; }, // 不可列舉 enumerable: false, writable: true, configurable: true, })複製程式碼
遞迴的完整版本(考慮到了Symbol屬性):
const cloneDeep1 = (target, hash = new WeakMap()) => { // 對於傳入引數處理 if (typeof target !== 'object' || target === null) { return target; } // 雜湊表中存在直接返回 if (hash.has(target)) return hash.get(target); const cloneTarget = Array.isArray(target) ? [] : {}; hash.set(target, cloneTarget); // 針對Symbol屬性 const symKeys = Object.getOwnPropertySymbols(target); if (symKeys.length) { symKeys.forEach(symKey => { if (typeof target[symKey] === 'object' && target[symKey] !== null) { cloneTarget[symKey] = cloneDeep1(target[symKey]); } else { cloneTarget[symKey] = target[symKey]; } }) } for (const i in target) { if (Object.prototype.hasOwnProperty.call(target, i)) { cloneTarget[i] = typeof target[i] === 'object' && target[i] !== null ? cloneDeep1(target[i], hash) : target[i]; } } return cloneTarget; }複製程式碼
實現思路:Promise原始碼實現
const PENDING = 'PENDING'; // 進行中const FULFILLED = 'FULFILLED'; // 已成功const REJECTED = 'REJECTED'; // 已失敗class Promise { constructor(exector) { // 初始化狀態 this.status = PENDING; // 將成功、失敗結果放在this上,便於then、catch存取 this.value = undefined; this.reason = undefined; // 成功態回撥函數佇列 this.onFulfilledCallbacks = []; // 失敗態回撥函數佇列 this.onRejectedCallbacks = []; const resolve = value => { // 只有進行中狀態才能更改狀態 if (this.status === PENDING) { this.status = FULFILLED; this.value = value; // 成功態函數依次執行 this.onFulfilledCallbacks.forEach(fn => fn(this.value)); } } const reject = reason => { // 只有進行中狀態才能更改狀態 if (this.status === PENDING) { this.status = REJECTED; this.reason = reason; // 失敗態函數依次執行 this.onRejectedCallbacks.forEach(fn => fn(this.reason)) } } try { // 立即執行executor // 把內部的resolve和reject傳入executor,使用者可呼叫resolve和reject exector(resolve, reject); } catch(e) { // executor執行出錯,將錯誤內容reject丟擲去 reject(e); } } then(onFulfilled, onRejected) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; onRejected = typeof onRejected === 'function'? onRejected: reason => { throw new Error(reason instanceof Error ? reason.message:reason) } // 儲存this const self = this; return new Promise((resolve, reject) => { if (self.status === PENDING) { self.onFulfilledCallbacks.push(() => { // try捕獲錯誤 try { // 模擬微任務 setTimeout(() => { const result = onFulfilled(self.value); // 分兩種情況: // 1. 回撥函數返回值是Promise,執行then操作 // 2. 如果不是Promise,呼叫新Promise的resolve函數 result instanceof Promise ? result.then(resolve, reject) : resolve(result); }) } catch(e) { reject(e); } }); self.onRejectedCallbacks.push(() => { // 以下同理 try { setTimeout(() => { const result = onRejected(self.reason); // 不同點:此時是reject result instanceof Promise ? result.then(resolve, reject) : reject(result); }) } catch(e) { reject(e); } }) } else if (self.status === FULFILLED) { try { setTimeout(() => { const result = onFulfilled(self.value); result instanceof Promise ? result.then(resolve, reject) : resolve(result); }); } catch(e) { reject(e); } } else if (self.status === REJECTED){ try { setTimeout(() => { const result = onRejected(self.reason); result instanceof Promise ? result.then(resolve, reject) : reject(result); }) } catch(e) { reject(e); } } }); } catch(onRejected) { return this.then(null, onRejected); } static resolve(value) { if (value instanceof Promise) { // 如果是Promise範例,直接返回 return value; } else { // 如果不是Promise範例,返回一個新的Promise物件,狀態為FULFILLED return new Promise((resolve, reject) => resolve(value)); } } static reject(reason) { return new Promise((resolve, reject) => { reject(reason); }) } }複製程式碼
Promise.all
是支援鏈式呼叫的,本質上就是返回了一個Promise範例,通過resolve
和reject
來改變範例狀態。
Promise.myAll = function(promiseArr) { return new Promise((resolve, reject) => { const ans = []; let index = 0; for (let i = 0; i < promiseArr.length; i++) { promiseArr[i] .then(res => { ans[i] = res; index++; if (index === promiseArr.length) { resolve(ans); } }) .catch(err => reject(err)); } }) }複製程式碼
Promise.race = function(promiseArr) { return new Promise((resolve, reject) => { promiseArr.forEach(p => { // 如果不是Promise範例需要轉化為Promise範例 Promise.resolve(p).then( val => resolve(val), err => reject(err), ) }) }) }複製程式碼
就是實現有並行限制的Promise排程器問題。
詳細實現思路:某條高頻面試原題:實現有並行限制的Promise排程器
class Scheduler { constructor() { this.queue = []; this.maxCount = 2; this.runCounts = 0; } add(promiseCreator) { this.queue.push(promiseCreator); } taskStart() { for (let i = 0; i < this.maxCount; i++) { this.request(); } } request() { if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) { return; } this.runCounts++; this.queue.shift()().then(() => { this.runCounts--; this.request(); }); } } const timeout = time => new Promise(resolve => { setTimeout(resolve, time); }) const scheduler = new Scheduler(); const addTask = (time,order) => { scheduler.add(() => timeout(time).then(()=>console.log(order))) } addTask(1000, '1'); addTask(500, '2'); addTask(300, '3'); addTask(400, '4'); scheduler.taskStart()// 2// 3// 1// 4複製程式碼
script標籤不遵循同源協定,可以用來進行跨域請求,優點就是相容性好但僅限於GET請求
const jsonp = ({ url, params, callbackName }) => { const generateUrl = () => { let dataSrc = ''; for (let key in params) { if (Object.prototype.hasOwnProperty.call(params, key)) { dataSrc += `${key}=${params[key]}&`; } } dataSrc += `callback=${callbackName}`; return `${url}?${dataSrc}`; } return new Promise((resolve, reject) => { const scriptEle = document.createElement('script'); scriptEle.src = generateUrl(); document.body.appendChild(scriptEle); window[callbackName] = data => { resolve(data); document.removeChild(scriptEle); } }) }複製程式碼
const getJSON = function(url) { return new Promise((resolve, reject) => { const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp'); xhr.open('GET', url, false); xhr.setRequestHeader('Accept', 'application/json'); xhr.onreadystatechange = function() { if (xhr.readyState !== 4) return; if (xhr.status === 200 || xhr.status === 304) { resolve(xhr.responseText); } else { reject(new Error(xhr.responseText)); } } xhr.send(); }) }複製程式碼
實現node中回撥函數的機制,node中回撥函數其實是內部使用了觀察者模式。
觀察者模式:定義了物件間一種一對多的依賴關係,當目標物件Subject發生改變時,所有依賴它的物件Observer都會得到通知。
function EventEmitter() { this.events = new Map(); }// 需要實現的一些方法:// addListener、removeListener、once、removeAllListeners、emit// 模擬實現addlistener方法const wrapCallback = (fn, once = false) => ({ callback: fn, once }); EventEmitter.prototype.addListener = function(type, fn, once = false) { const hanlder = this.events.get(type); if (!hanlder) { // 沒有type繫結事件 this.events.set(type, wrapCallback(fn, once)); } else if (hanlder && typeof hanlder.callback === 'function') { // 目前type事件只有一個回撥 this.events.set(type, [hanlder, wrapCallback(fn, once)]); } else { // 目前type事件數>=2 hanlder.push(wrapCallback(fn, once)); } }// 模擬實現removeListenerEventEmitter.prototype.removeListener = function(type, listener) { const hanlder = this.events.get(type); if (!hanlder) return; if (!Array.isArray(this.events)) { if (hanlder.callback === listener.callback) this.events.delete(type); else return; } for (let i = 0; i < hanlder.length; i++) { const item = hanlder[i]; if (item.callback === listener.callback) { hanlder.splice(i, 1); i--; if (hanlder.length === 1) { this.events.set(type, hanlder[0]); } } } }// 模擬實現once方法EventEmitter.prototype.once = function(type, listener) { this.addListener(type, listener, true); }// 模擬實現emit方法EventEmitter.prototype.emit = function(type, ...args) { const hanlder = this.events.get(type); if (!hanlder) return; if (Array.isArray(hanlder)) { hanlder.forEach(item => { item.callback.apply(this, args); if (item.once) { this.removeListener(type, item); } }) } else { hanlder.callback.apply(this, args); if (hanlder.once) { this.events.delete(type); } } return true; } EventEmitter.prototype.removeAllListeners = function(type) { const hanlder = this.events.get(type); if (!hanlder) return; this.events.delete(type); }複製程式碼
可以給img標籤統一自定義屬性src='default.png'
,當檢測到圖片出現在視窗之後再補充src屬性,此時才會進行圖片資源載入。
function lazyload() { const imgs = document.getElementsByTagName('img'); const len = imgs.length; // 視口的高度 const viewHeight = document.documentElement.clientHeight; // 卷軸高度 const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop; for (let i = 0; i < len; i++) { const offsetHeight = imgs[i].offsetTop; if (offsetHeight < viewHeight + scrollHeight) { const src = imgs[i].dataset.src; imgs[i].src = src; } } }// 可以使用節流優化一下window.addEventListener('scroll', lazyload);複製程式碼
原理就是監聽頁面捲動事件,分析clientHeight、scrollTop、scrollHeight三者的屬性關係。
window.addEventListener('scroll', function() { const clientHeight = document.documentElement.clientHeight; const scrollTop = document.documentElement.scrollTop; const scrollHeight = document.documentElement.scrollHeight; if (clientHeight + scrollTop >= scrollHeight) { // 檢測到捲動至頁面底部,進行後續操作 // ... } }, false);複製程式碼
一個Demo:頁面捲動載入的Demo
渲染巨量資料時,合理使用createDocumentFragment和requestAnimationFrame,將操作切分為一小段一小段執行。
setTimeout(() => { // 插入十萬條資料 const total = 100000; // 一次插入的資料 const once = 20; // 插入資料需要的次數 const loopCount = Math.ceil(total / once); let countOfRender = 0; const ul = document.querySelector('ul'); // 新增資料的方法 function add() { const fragment = document.createDocumentFragment(); for(let i = 0; i < once; i++) { const li = document.createElement('li'); li.innerText = Math.floor(Math.random() * total); fragment.appendChild(li); } ul.appendChild(fragment); countOfRender += 1; loop(); } function loop() { if(countOfRender < loopCount) { window.requestAnimationFrame(add); } } loop(); }, 0)複製程式碼
一行程式碼可以解決:
const fn = () => { return [...new Set([...document.querySelectorAll('*')].map(el => el.tagName))].length; }複製程式碼
值得注意的是:DOM操作返回的是類陣列,需要轉換為陣列之後才可以呼叫陣列的方法。
這是當前SPA應用的核心概念之一
// vnode結構:// {// tag,// attrs,// children,// }//Virtual DOM => DOMfunction render(vnode, container) { container.appendChild(_render(vnode)); }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 => render(child, dom)); return dom; }複製程式碼
var a = { b: 123, c: '456', e: '789', }var str=`a{a.b}aa{a.c}aa {a.d}aaaa`;// => 'a123aa456aa {a.d}aaaa'複製程式碼
實現函數使得將str字串中的{}
內的變數替換,如果屬性不存在保持原樣(比如{a.d}
)
類似於模版字串,但有一點出入,實際上原理大差不差
const fn1 = (str, obj) => { let res = ''; // 標誌位,標誌前面是否有{ let flag = false; let start; for (let i = 0; i < str.length; i++) { if (str[i] === '{') { flag = true; start = i + 1; continue; } if (!flag) res += str[i]; else { if (str[i] === '}') { flag = false; res += match(str.slice(start, i), obj); } } } return res; }// 物件匹配操作const match = (str, obj) => { const keys = str.split('.').slice(1); let index = 0; let o = obj; while (index < keys.length) { const key = keys[index]; if (!o[key]) { return `{${str}}`; } else { o = o[key]; } index++; } return o; }複製程式碼
相關免費學習推薦:(視訊)
以上就是32個純手寫JS,鞏固你的JS基礎的詳細內容,更多請關注TW511.COM其它相關文章!