轉(zhuǎn)自掘金:https://juejin.im/post/6875152247714480136
作為前端開發(fā),JS是重中之重,最近結(jié)束了面試的高峰期,基本上offer也定下來了就等開獎(jiǎng),趁著這個(gè)時(shí)間總結(jié)下32個(gè)手寫JS問題,這些都是高頻面試題,希望對(duì)你能有所幫助。
關(guān)于源碼都緊遵規(guī)范,都可跑通MDN示例,其余的大多會(huì)涉及一些關(guān)于JS的應(yīng)用題和本人面試過程
01.數(shù)組扁平化
數(shù)組扁平化是指將一個(gè)多維數(shù)組變?yōu)橐粋€(gè)一維數(shù)組:
const arr = [1, [2, [3, [4, 5]]], 6];
// => [1, 2, 3, 4, 5, 6]
方法一:使用flat()
const res1 = arr.flat(Infinity)
方法二:利用正則
const res2 = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']');
方法三:函數(shù)遞歸
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);
方法四:使用reduce(其實(shí)也屬于遞歸)
const flatten = arr => {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, [])
}
const res4 = flatten(arr);
02.數(shù)組去重
const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
// => [1, '1', 17, true, false, 'true', 'a', {}, {}]
方法一:利用Set
const unique1 = [...new Set(arr)];
方法二:利用indexOf 或者 includes
const unique2 = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
//if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
if (!res.includes(arr[i])) res.push(arr[i]);
}
return res;
}
方法三:利用filter
const unique4 = arr => {
return arr.filter((item, index) => {
return arr.indexOf(item) === index;
});
}
03. debounce(防抖)
觸發(fā)高頻時(shí)間后n秒內(nèi)函數(shù)只會(huì)執(zhí)行一次,如果n秒內(nèi)高頻時(shí)間再次觸發(fā),則重新計(jì)算時(shí)間。
const debounce = (fn, time) => {
let timeout = null;
return function() {
clearTimeout(timeout)
timeout = setTimeout(() => {
fn.apply(this, arguments);
}, time);
}
};
防抖常應(yīng)用于用戶進(jìn)行搜索輸入節(jié)約請(qǐng)求資源,window觸發(fā)resize事件時(shí)進(jìn)行防抖只觸發(fā)一次。
04.throttle(節(jié)流)
高頻時(shí)間觸發(fā),但n秒內(nèi)只會(huì)執(zhí)行一次,所以節(jié)流會(huì)稀釋函數(shù)的執(zhí)行頻率。
const throttle = (fn, time) => {
let flag = true;
return function() {
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this, arguments);
flag = true;
}, time);
}
}
節(jié)流常應(yīng)用于鼠標(biāo)不斷點(diǎn)擊觸發(fā)、監(jiān)聽滾動(dòng)事件。
05. 函數(shù)珂里化
指的是將一個(gè)接受多個(gè)參數(shù)的函數(shù) 變?yōu)?接受一個(gè)參數(shù)返回一個(gè)函數(shù)的固定形式,這樣便于再次調(diào)用,例如f(1)(2)
經(jīng)典面試題:實(shí)現(xiàn)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;
}
06. Promise
實(shí)現(xiàn)思路:Promise源碼實(shí)現(xiàn)
class Promise {
constructor(exector) {
const resolve = () => {
}
const reject = () => {
}
exector(resolve, reject);
}
then() {
}
catch() {
}
static resolve() {
}
static reject() {
}
static all() {
}
static race() {
}
}
具體實(shí)現(xiàn):
// 模擬實(shí)現(xiàn)Promise
// Promise利用三大手段解決回調(diào)地獄:
// 1. 回調(diào)函數(shù)延遲綁定
// 2. 返回值穿透
// 3. 錯(cuò)誤冒泡
// 定義三種狀態(tài)
const PENDING = 'PENDING'; // 進(jìn)行中
const FULFILLED = 'FULFILLED'; // 已成功
const REJECTED = 'REJECTED'; // 已失敗
class Promise {
constructor(exector) {
// 初始化狀態(tài)
this.status = PENDING;
// 將成功、失敗結(jié)果放在this上,便于then、catch訪問
this.value = undefined;
this.reason = undefined;
// 成功態(tài)回調(diào)函數(shù)隊(duì)列
this.onFulfilledCallbacks = [];
// 失敗態(tài)回調(diào)函數(shù)隊(duì)列
this.onRejectedCallbacks = [];
const resolve = value => {
// 只有進(jìn)行中狀態(tài)才能更改狀態(tài)
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// 成功態(tài)函數(shù)依次執(zhí)行
this.onFulfilledCallbacks.forEach(fn => fn(this.value));
}
}
const reject = reason => {
// 只有進(jìn)行中狀態(tài)才能更改狀態(tài)
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// 失敗態(tài)函數(shù)依次執(zhí)行
this.onRejectedCallbacks.forEach(fn => fn(this.reason))
}
}
try {
// 立即執(zhí)行executor
// 把內(nèi)部的resolve和reject傳入executor,用戶可調(diào)用resolve和reject
exector(resolve, reject);
} catch(e) {
// executor執(zhí)行出錯(cuò),將錯(cuò)誤內(nèi)容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捕獲錯(cuò)誤
try {
// 模擬微任務(wù)
setTimeout(() => {
const result = onFulfilled(self.value);
// 分兩種情況:
// 1. 回調(diào)函數(shù)返回值是Promise,執(zhí)行then操作
// 2. 如果不是Promise,調(diào)用新Promise的resolve函數(shù)
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
})
} catch(e) {
reject(e);
}
});
self.onRejectedCallbacks.push(() => {
// 以下同理
try {
setTimeout(() => {
const result = onRejected(self.reason);
// 不同點(diǎn):此時(shí)是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) : resolve(result);
})
} catch(e) {
reject(e);
}
}
});
}
catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
if (value instanceof Promise) {
// 如果是Promise實(shí)例,直接返回
return value;
} else {
// 如果不是Promise實(shí)例,返回一個(gè)新的Promise對(duì)象,狀態(tài)為FULFILLED
return new Promise((resolve, reject) => resolve(value));
}
}
static reject(reason) {
return new Promise((resolve, reject) => {
reject(reason);
})
}
static all(promiseArr) {
const len = promiseArr.length;
const values = new Array(len);
// 記錄已經(jīng)成功執(zhí)行的promise個(gè)數(shù)
let count = 0;
return new Promise((resolve, reject) => {
for (let i = 0; i < len; i++) {
// Promise.resolve()處理,確保每一個(gè)都是promise實(shí)例
Promise.resolve(promiseArr[i]).then(
val => {
values[i] = val;
count++;
// 如果全部執(zhí)行完,返回promise的狀態(tài)就可以改變了
if (count === len) resolve(values);
},
err => reject(err),
);
}
})
}
static race(promiseArr) {
return new Promise((resolve, reject) => {
promiseArr.forEach(p => {
Promise.resolve(p).then(
val => resolve(val),
err => reject(err),
)
})
})
}
}
07. 圖片懶加載
可以給img標(biāo)簽統(tǒng)一自定義屬性data-src='default.png',當(dāng)檢測(cè)到圖片出現(xiàn)在窗口之后再補(bǔ)充src屬性,此時(shí)才會(huì)進(jìn)行圖片資源加載。
function lazyload() {
const imgs = document.getElementsByTagName('img');
const len = imgs.length;
// 視口的高度
const viewHeight = document.documentElement.clientHeight;
// 滾動(dòng)條高度
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;
}
}
}
// 可以使用節(jié)流優(yōu)化一下
window.addEventListener('scroll', lazyload);
08.滾動(dòng)加載
原理就是監(jiān)聽頁(yè)面滾動(dòng)事件,分析clientHeight、scrollTop、scrollHeight三者的屬性關(guān)系。
window.addEventListener('scroll', function() {
const clientHeight = document.documentElement.clientHeight;
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
if (clientHeight + scrollTop >= scrollHeight) {
// 檢測(cè)到滾動(dòng)至頁(yè)面底部,進(jìn)行后續(xù)操作
// ...
}
}, false);
09. 將VirtualDom轉(zhuǎn)化為真實(shí)DOM結(jié)構(gòu)
這是當(dāng)前SPA應(yīng)用的核心概念之一
// vnode結(jié)構(gòu):
// {
// tag,
// attrs,
// children,
// }
//Virtual DOM => DOM
function render(vnode, container) {
container.appendChild(_render(vnode));
}
function _render(vnode) {
// 如果是數(shù)字類型轉(zhuǎn)化為字符串
if (typeof vnode === 'number') {
vnode = String(vnode);
}
// 字符串類型直接就是文本節(jié)點(diǎn)
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);
})
}
// 子數(shù)組進(jìn)行遞歸操作
vnode.children.forEach(child => render(child, dom));
return dom;
}