引言
大多数公司,应该都会在年后开启春招。这是校招生进入大厂的最后一搏了,其中令人生畏的要属手撕代码了,本文将为梳理一些前端常见面试题,祝您早日拿到心仪 offer。
常见手写面试题
防抖
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能
// 普通版
const debounce = function(fn, wait) {
let timer = null;
return function debounced(...args) {
if(timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(() => {
fn.apply(this, args); // 绑定 this值,获取参数
}, wait);
}
}
// 立即执行版
const debounce = function(fn, wait, immediate) {
let timer = null;
let result = null;
const debounced = function(...args) {
if(timer) {
clearTimeout(timer);
timer = null;
}
if(immediate) {
let callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait)
if(callNow) {
fn.apply(this, args);
}
} else {
timer = setTimeout(() => {
result = fn.apply(this, args); // 立即执行时, 可能有返回值
}, wait)
}
return result;
}
return debounced;
}
// 可取消版
const debounce = function(fn, wait, immediate) {
let timer = null;
let result = null;
const debounced = function(...args) {
if(timer) {
clearTimeout(timer);
timer = null;
}
if(immediate) {
let callNow = !timer;
timer = setTimeout(() => {
timer = null;
}, wait)
if(callNow) result = fn.apply(this, args);
} else {
timer = setTimeout(() => {
fn.apply(this, args);
}, wait)
}
return result;
}
debounced.cancel = function() {
clearTimeout(timer);
timer = null;
}
}
节流
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
- 缩放场景:监控浏览器resize
- 动画场景:避免短时间内多次触发动画引起性能问题
// 时间戳版
// 立即执行版
const throttle = function throttle(fn, wait) {
let startTime = Date.now();
return function(...args) {
let curTime = Date.now();
if(curTime - startTime >= wait) {
fn.apply(this, args);
startTime = curTime;
}
}
}
// 定时器版
// wait 毫秒后执行
const throttle = function throttle(fn, wait) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, wait)
}
}
}
/* 双剑合壁版 */
function throttle(func, wait) {
let timeout, context, args, result;
let previous = 0;
let later = function() {
previous = +new Date();
timeout = null;
func.apply(context, args)
};
let throttled = function() {
var now = +new Date();
//下次触发 func 剩余的时间
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果没有剩余的时间了或者你改了系统时间
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}
Call
Function.prototype.myCall = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError(`${this} is not funciton`);
}
const context = (context === null || context === undefined) ? globalThis : Object(context)
const fn = Symbol();
Object.defineProperty(context, fn, {
value: this,
enumerable: false
})
const result = context[fn](...args); //扩展运算符
delete context[fn];
return result;
}
Apply
Function.prototype.myApply = function(context) {
if (typeof this !== 'function') {
throw new TypeError(`${this} is not funciton`);
}
const context = context || window;
const fn = Symbol()
context[fn] = this;
let result;
if(arguments[1]) { // 此处与call稍有不同,因为apply 至多有两个参数,而call是参数序列
result = context[fn](...arguments[1]);
}else {
result = context[fn]();
}
delete context[fn];
return result;
}
Bind
Function.prototype.myBind = function (context, ...args1) {
if (typeof this !== 'function') {
throw new TypeError(`${this} is not function`);
}
let that = this;
return function F(...args2) {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) { // 此时 this 指向实例
return new that(...arg1, ...args2)
} else {
return that.apply(context, [...args1, ...args2])
}
}
}
Object.create
const _create = function _create(proto) {
function F () {};
F.prototype = proto;
return new F();
}
new
const _new = function _new (Con, ...args) {
const obj = Object.create(Con.prototype);//链接到原型,obj 可以访问到构造函数原型中的属性
const ret = Con.apply(obj, [...args]);//绑定this实现继承,obj可以访问到构造函数中的属性
return typeof ret === 'object' ? ret : obj;//优先返回构造函数返回的对象
}
instanceof
const _instanceof = _instanceof (left, right) {
left = Object.getPrototypeOf(left);
right = right.prototype;
while (true) {
if (left === null) return false; // 原型链的末端
if (left === right) return true;
left = Object.getPrototypeOf(left); // 沿着原型链往上查找
}
}
ajax
const _ajax = function _ajax (config) {
const { method = 'GET', url, data, params, async = true, timeout, cancelToken } = config;
return new Promise((resolve, reject) => {
const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
method = method.toUpperCase();
xhr.open(method, buildURL(url, params), async);
xhr.timeout = timeout;
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
resolve(xhr.responseText);
} else {
reject(xhr.responseText);
}
}
}
if (cancelToken) {
cancelToken.promise.then(function onCanceled(cancel) {
if (!xhr) {
return;
}
xhr.abort();
reject(cancel);
xhr = null;
});
}
switch (method) {
case 'GET':
xhr.send();
break;
case 'POST':
xhr.setRequestHeader('Content-Type', 'application/x-www-urlencoded');
xhr.send(data);
break;
default:
xhr.send();
break;
}
})
}
function buildURL (url, params) {
if (!params) {
return url;
}
var arr = [];
for (const param in params) {
arr.push(encodeURIComponent(param) + '=' + encodeURIComponent(params[param]));
}
return url + '?' + arr.join('&');
}
深拷贝
局限性
- 无法实现对函数 、RegExp等特殊对象的克隆
- 会抛弃对象的constructor,所有的构造函数会指向Object
- 对象有循环引用,会报错
const deepClone = function(obj) {
return JSON.parse(JSON.stringify(obj));
}
const deepClone = function(obj) {
if(typeof obj !== 'object' || obj === null) {
return obj;
}
const newObj = obj instanceof Array ? []:{};
for(var key in obj) {
if(Object.prototype.hasOwnProperty.call(obj, key)) {
newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]): obj[key];
}
}
return newObj;
}
解决了循环引用的问题
// vuex deepCopy 实现
// 源码 src/util.js
function find(list, f) {
return list.filter(f)[0]
}
function deepCopy(obj, cache = []) {
// just return if obj is immutable value 递归结束的条件
if (obj === null || typeof obj !== 'object') {
return obj;
}
// if obj is hit, it is in circular structure
const hit = find(cache, c => c.original === obj);
if (hit) {
return hit.copy
}
const copy = Array.isArray(obj) ? [] : {}
// put the copy into cache at first
// because we want to refer it in recursive(递归) deepCopy
cache.push({
original: obj,
copy
})
Object.keys(obj).forEach(key => {
copy[key] = deepCopy(obj[key], cache)
})
return copy
}
lodash.cloneDeep();
const isObject = target => (target !== null && (typeof target === 'object' || typeof target === 'function'));
const type = target => Object.prototype.toString.call(target).slice(8, -1);
function deepClone (target, map = new WeakMap()) {
if (!isObject(target)) {
return target;
}
if (type(target) === 'Date') {
return new Date(target);
}
if (type(target) === 'RegExp') {
return new RegExp(target);
}
if (map.has(target)) {
return map.get(target);
}
const allDescriptors = Object.getOwnPropertyDescriptors(target);
map.set(target, cloneTarget);
const cloneTarget = Object.create(Object.getPrototypeOf(target), allDescriptors);
for (const key of Reflect.ownKeys(target)) {
cloneTarget[key] = deepClone(target[key], map);
}
return cloneTarget;
}
jsonp
// 客户端
function jsonp(req) {
var url = req.url + '?callback=' + req.callback.name;
var script = document.createElement('script');
script.src = url;
var tag = document.getElementsByTagName('head')[0].append(script);
}
function success(res) {
console.log(res.data);
}
var req = {
url: 'http://localhost:3000/json',
callback: success
}
jsonp(req);// 客户端动态创建script标签,并且添加callback函数
// 服务端
var express = require('express');
const app = express();
app.listen(3000,() => {
console.log('server is running in 3000 port');
})
var data = {
data: {
'hello': 'world'
}
}
app.get('/json', (req, res) => {
var params = req.url.split('?')[1].split('&');
var callback = '';
params.forEach(item => {
var splits = item.split('=');
var key = splits[0];
var value = splits[1];
if(key === 'callback') {
callback = value;
}
})
var ans = callback + '(' + JSON.stringify(data) + ')';
res.send(ans);
}) // 服务端获取callback函数名,将函数名与响应数据拼接,返回,客户端就会自动调用callback函数
数组去重
/* indexOf方法 */
const unique = function(arr) {
var newArr = [];
for(let i=0; i<arr.length; i++) {
if(newArr.indexOf(arr[i]) === -1) {
newArr.push(arr[i]);
}
}
return newArr;
}
/* Object键的唯一性 */
/* 加入typeof key */
const unique = function(arr) {
const obj = {};
for(let i=0; i<arr.length; i++) {
if(obj[arr[i]] !== true) {
obj[arr[i]] = true;
}
}
var newObj = Object.keys(obj).map((item) => {
return item - '0';// 将字符串转为数字
})
return newObj;
}
Array.from(new Set(arr));
判断 Array 类型
// 在obj的原型链上查找Array.prototype
obj instanceof Array;
//obj的原型是不是Array.prototype
Array.prototype.isPrototypeOf(obj);
Array.isArray(obj);
Object.prototype.toString.call(obj) === '[Object Array]';
事件委托/代理
<!--点击li标签的时候,弹出id值-->
<ul onclick="handleClick(event)">
<li id="1">a</li>
<li id="2">b</li>
<li id="3">c</li>
<li id="4">d</li>
<li id="5">e</li>
</ul>
function handleClick(event) {
var dom = event.target;
//event.target 表示当前点击的元素 event.currentTarget 表示事件绑定的元素
alert(dom.getAttribute('id'));
}
提取 search 参数
const search = function(url) {
var query = url.split('?')[1]; // location.search.substring(1) 浏览器环境下
var arr = query.split('&');
var obj = {};
for(let i=0; i<arr.length; i++) {
let key = arr[i].split('=')[0];
let value = arr[i].split('=')[1];
if(value === undefined) {
obj[key] = '';
}else {
obj[key] = value;
}
}
return obj;
}
sleep
function sleep(n) {
var start = +new Date();
while(true) {
if(+new Date() - start > n) {
break;
}
}
}
function sleep(n) {
return new Promise((resolve) => {
setTimeout(resolve, n)
})
}
冒泡排序
// 时间复杂度 N^2 空间复杂度1
const bubbleSort = function bubbleSort (arr) {
for (let i = 0; i < arr.length; i++) {
for (let j = 0; j < arr.length - i - 1; j++) {
if (arr[j] > arr[j+1]) {
let temp = arr[j+1];
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
快排
// 时间复杂度 最差 N^2 平均 NlogN 空间复杂度 logN
const quickSort = function quickSort (arr) {
if (arr.length <= 1) {
return arr;
}
let pivot = Math.floor(arr.length/2);
let midItem = arr.splice(pivot, 1)[0];
let left = [];
let right = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] < midItem) {
right.push(arr[i]);
} else {
left.push(arr[i]);
}
}
return quickSort(left).concat([midItem], quickSort(right));
}
归并排序
// 时间复杂度 NlogN 空间复杂度 N
const mergeSort = function mergeSort(arr) {
if (arr.length < 2) return arr;
let middle = Math.floor(arr.length / 2);
let left = arr.slice(0, middle);
let right = arr.slice(middle);
return merge(mergeSort(left), mergeSort(right));
}
function merge (left, right) {
let result = [];
while (left.length && right.length) {
if (left[0] < right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
if (left.length) {
result.push(left.shift());
}
if (right.length) {
result.push(right.shift());
}
return result;
}
二分查找
const binarySearch = function binarySearch (arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
let mid = (left + right) >> 1;
if (arr[mid] < target) {
left = mid + 1;
} else if (arr[mid] > target) {
right = mid - 1;
} else {
return mid;
}
}
return -1;
}
总结
以上是个人对常见前端手写面试题的一些总结,希望能对大家有所帮助。如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞。