手撕前端面试题

968 阅读6分钟

引言

大多数公司,应该都会在年后开启春招。这是校招生进入大厂的最后一搏了,其中令人生畏的要属手撕代码了,本文将为梳理一些前端常见面试题,祝您早日拿到心仪 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;
}

总结

以上是个人对常见前端手写面试题的一些总结,希望能对大家有所帮助。如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞。