22道JavaScript高頻手寫面試題

JavaScript筆試部分

點擊關注本公衆號獲取文檔最新更新,並能夠領取配套於本指南的 《前端面試手冊》 以及最標準的簡歷模板.javascript

實現防抖函數(debounce)

防抖函數原理:在事件被觸發n秒後再執行回調,若是在這n秒內又被觸發,則從新計時。前端

那麼與節流函數的區別直接看這個動畫實現便可。java

手寫簡化版:node

// 防抖函數
const debounce = (fn, delay) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};

適用場景:git

  • 按鈕提交場景:防止屢次提交按鈕,只執行最後提交的一次
  • 服務端驗證場景:表單驗證須要服務端配合,只執行一段連續的輸入事件的最後一次,還有搜索聯想詞功能相似

生存環境請用lodash.debounce程序員

實現節流函數(throttle)

防抖函數原理:規定在一個單位時間內,只能觸發一次函數。若是這個單位時間內觸發屢次函數,只有一次生效。es6

// 手寫簡化版github

// 節流函數
const throttle = (fn, delay = 500) => {
  let flag = true;
  return (...args) => {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, args);
      flag = true;
    }, delay);
  };
};

適用場景:面試

  • 拖拽場景:固定時間內只執行一次,防止超高頻次觸發位置變更
  • 縮放場景:監控瀏覽器resize
  • 動畫場景:避免短期內屢次觸發動畫引發性能問題

深克隆(deepclone)

簡單版:正則表達式

const newObj = JSON.parse(JSON.stringify(oldObj));

侷限性:

  1. 他沒法實現對函數 、RegExp等特殊對象的克隆
  2. 會拋棄對象的constructor,全部的構造函數會指向Object
  3. 對象有循環引用,會報錯

面試版:

/**
 * deep clone
 * @param  {[type]} parent object 須要進行克隆的對象
 * @return {[type]}        深克隆後的對象
 */
const clone = parent => {
  // 判斷類型
  const isType = (obj, type) => {
    if (typeof obj !== "object") return false;
    const typeString = Object.prototype.toString.call(obj);
    let flag;
    switch (type) {
      case "Array":
        flag = typeString === "[object Array]";
        break;
      case "Date":
        flag = typeString === "[object Date]";
        break;
      case "RegExp":
        flag = typeString === "[object RegExp]";
        break;
      default:
        flag = false;
    }
    return flag;
  };

  // 處理正則
  const getRegExp = re => {
    var flags = "";
    if (re.global) flags += "g";
    if (re.ignoreCase) flags += "i";
    if (re.multiline) flags += "m";
    return flags;
  };
  // 維護兩個儲存循環引用的數組
  const parents = [];
  const children = [];

  const _clone = parent => {
    if (parent === null) return null;
    if (typeof parent !== "object") return parent;

    let child, proto;

    if (isType(parent, "Array")) {
      // 對數組作特殊處理
      child = [];
    } else if (isType(parent, "RegExp")) {
      // 對正則對象作特殊處理
      child = new RegExp(parent.source, getRegExp(parent));
      if (parent.lastIndex) child.lastIndex = parent.lastIndex;
    } else if (isType(parent, "Date")) {
      // 對Date對象作特殊處理
      child = new Date(parent.getTime());
    } else {
      // 處理對象原型
      proto = Object.getPrototypeOf(parent);
      // 利用Object.create切斷原型鏈
      child = Object.create(proto);
    }

    // 處理循環引用
    const index = parents.indexOf(parent);

    if (index != -1) {
      // 若是父數組存在本對象,說明以前已經被引用過,直接返回此對象
      return children[index];
    }
    parents.push(parent);
    children.push(child);

    for (let i in parent) {
      // 遞歸
      child[i] = _clone(parent[i]);
    }

    return child;
  };
  return _clone(parent);
};

侷限性:

  1. 一些特殊狀況沒有處理: 例如Buffer對象、Promise、Set、Map
  2. 另外對於確保沒有循環引用的對象,咱們能夠省去對循環引用的特殊處理,由於這很消耗時間
原理詳解 實現深克隆

實現Event(event bus)

event bus既是node中各個模塊的基石,又是前端組件通訊的依賴手段之一,同時涉及了訂閱-發佈設計模式,是很是重要的基礎。

簡單版:

class EventEmeitter {
  constructor() {
    this._events = this._events || new Map(); // 儲存事件/回調鍵值對
    this._maxListeners = this._maxListeners || 10; // 設立監聽上限
  }
}


// 觸發名爲type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  // 從儲存事件鍵值對的this._events中獲取對應事件回調函數
  handler = this._events.get(type);
  if (args.length > 0) {
    handler.apply(this, args);
  } else {
    handler.call(this);
  }
  return true;
};

// 監聽名爲type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  // 將type事件以及對應的fn函數放入this._events中儲存
  if (!this._events.get(type)) {
    this._events.set(type, fn);
  }
};

面試版:

class EventEmeitter {
  constructor() {
    this._events = this._events || new Map(); // 儲存事件/回調鍵值對
    this._maxListeners = this._maxListeners || 10; // 設立監聽上限
  }
}

// 觸發名爲type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  // 從儲存事件鍵值對的this._events中獲取對應事件回調函數
  handler = this._events.get(type);
  if (args.length > 0) {
    handler.apply(this, args);
  } else {
    handler.call(this);
  }
  return true;
};

// 監聽名爲type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  // 將type事件以及對應的fn函數放入this._events中儲存
  if (!this._events.get(type)) {
    this._events.set(type, fn);
  }
};

// 觸發名爲type的事件
EventEmeitter.prototype.emit = function(type, ...args) {
  let handler;
  handler = this._events.get(type);
  if (Array.isArray(handler)) {
    // 若是是一個數組說明有多個監聽者,須要依次此觸發裏面的函數
    for (let i = 0; i < handler.length; i++) {
      if (args.length > 0) {
        handler[i].apply(this, args);
      } else {
        handler[i].call(this);
      }
    }
  } else {
    // 單個函數的狀況咱們直接觸發便可
    if (args.length > 0) {
      handler.apply(this, args);
    } else {
      handler.call(this);
    }
  }

  return true;
};

// 監聽名爲type的事件
EventEmeitter.prototype.addListener = function(type, fn) {
  const handler = this._events.get(type); // 獲取對應事件名稱的函數清單
  if (!handler) {
    this._events.set(type, fn);
  } else if (handler && typeof handler === "function") {
    // 若是handler是函數說明只有一個監聽者
    this._events.set(type, [handler, fn]); // 多個監聽者咱們須要用數組儲存
  } else {
    handler.push(fn); // 已經有多個監聽者,那麼直接往數組裏push函數便可
  }
};

EventEmeitter.prototype.removeListener = function(type, fn) {
  const handler = this._events.get(type); // 獲取對應事件名稱的函數清單

  // 若是是函數,說明只被監聽了一次
  if (handler && typeof handler === "function") {
    this._events.delete(type, fn);
  } else {
    let postion;
    // 若是handler是數組,說明被監聽屢次要找到對應的函數
    for (let i = 0; i < handler.length; i++) {
      if (handler[i] === fn) {
        postion = i;
      } else {
        postion = -1;
      }
    }
    // 若是找到匹配的函數,從數組中清除
    if (postion !== -1) {
      // 找到數組對應的位置,直接清除此回調
      handler.splice(postion, 1);
      // 若是清除後只有一個函數,那麼取消數組,以函數形式保存
      if (handler.length === 1) {
        this._events.set(type, handler[0]);
      }
    } else {
      return this;
    }
  }
};
實現具體過程和思路見 實現event

實現instanceOf

// 模擬 instanceof
function instance_of(L, R) {
  //L 表示左表達式,R 表示右表達式
  var O = R.prototype; // 取 R 的顯示原型
  L = L.__proto__; // 取 L 的隱式原型
  while (true) {
    if (L === null) return false;
    if (O === L)
      // 這裏重點:當 O 嚴格等於 L 時,返回 true
      return true;
    L = L.__proto__;
  }
}

模擬new

new操做符作了這些事:

  • 它建立了一個全新的對象
  • 它會被執行[[Prototype]](也就是__proto__)連接
  • 它使this指向新建立的對象
  • 經過new建立的每一個對象將最終被[[Prototype]]連接到這個函數的prototype對象上
  • 若是函數沒有返回對象類型Object(包含Functoin, Array, Date, RegExg, Error),那麼new表達式中的函數調用將返回該對象引用
// objectFactory(name, 'cxk', '18')
function objectFactory() {
  const obj = new Object();
  const Constructor = [].shift.call(arguments);

  obj.__proto__ = Constructor.prototype;

  const ret = Constructor.apply(obj, arguments);

  return typeof ret === "object" ? ret : obj;
}

實現一個call

call作了什麼:

  • 將函數設爲對象的屬性
  • 執行&刪除這個函數
  • 指定this到函數並傳入給定參數執行函數
  • 若是不傳入參數,默認指向爲 window
// 模擬 call bar.mycall(null);
//實現一個call方法:
Function.prototype.myCall = function(context) {
  //此處沒有考慮context非object狀況
  context.fn = this;
  let args = [];
  for (let i = 1, len = arguments.length; i < len; i++) {
    args.push(arguments[i]);
  }
  context.fn(...args);
  let result = context.fn(...args);
  delete context.fn;
  return result;
};
具體實現參考 JavaScript深刻之call和apply的模擬實現

實現apply方法

apply原理與call很類似,很少贅述

// 模擬 apply
Function.prototype.myapply = function(context, arr) {
  var context = Object(context) || window;
  context.fn = this;

  var result;
  if (!arr) {
    result = context.fn();
  } else {
    var args = [];
    for (var i = 0, len = arr.length; i < len; i++) {
      args.push("arr[" + i + "]");
    }
    result = eval("context.fn(" + args + ")");
  }

  delete context.fn;
  return result;
};

實現bind

實現bind要作什麼

  • 返回一個函數,綁定this,傳遞預置參數
  • bind返回的函數能夠做爲構造函數使用。故做爲構造函數時應使得this失效,可是傳入的參數依然有效
// mdn的實現
if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }

    var aArgs   = Array.prototype.slice.call(arguments, 1),
        fToBind = this,
        fNOP    = function() {},
        fBound  = function() {
          // this instanceof fBound === true時,說明返回的fBound被當作new的構造函數調用
          return fToBind.apply(this instanceof fBound
                 ? this
                 : oThis,
                 // 獲取調用時(fBound)的傳參.bind 返回的函數入參每每是這麼傳遞的
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };

    // 維護原型關係
    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      fNOP.prototype = this.prototype; 
    }
    // 下行的代碼使fBound.prototype是fNOP的實例,所以
    // 返回的fBound若做爲new的構造函數,new生成的新對象做爲this傳入fBound,新對象的__proto__就是fNOP的實例
    fBound.prototype = new fNOP();

    return fBound;
  };
}
詳解請移步 JavaScript深刻之bind的模擬實現 #12

模擬Object.create

Object.create()方法建立一個新對象,使用現有的對象來提供新建立的對象的__proto__。

// 模擬 Object.create

function create(proto) {
  function F() {}
  F.prototype = proto;

  return new F();
}

實現類的繼承

類的繼承在幾年前是重點內容,有n種繼承方式各有優劣,es6普及後愈來愈不重要,那麼多種寫法有點『回字有四樣寫法』的意思,若是還想深刻理解的去看紅寶書便可,咱們目前只實現一種最理想的繼承方式。

function Parent(name) {
    this.parent = name
}
Parent.prototype.say = function() {
    console.log(`${this.parent}: 你打籃球的樣子像kunkun`)
}
function Child(name, parent) {
    // 將父類的構造函數綁定在子類上
    Parent.call(this, parent)
    this.child = name
}

/** 
 1. 這一步不用Child.prototype =Parent.prototype的緣由是怕共享內存,修改父類原型對象就會影響子類
 2. 不用Child.prototype = new Parent()的緣由是會調用2次父類的構造方法(另外一次是call),會存在一份多餘的父類實例屬性
3. Object.create是建立了父類原型的副本,與父類原型徹底隔離
*/
Child.prototype = Object.create(Parent.prototype);
Child.prototype.say = function() {
    console.log(`${this.parent}好,我是練習時長兩年半的${this.child}`);
}

// 注意記得把子類的構造指向子類自己
Child.prototype.constructor = Child;

var parent = new Parent('father');
parent.say() // father: 你打籃球的樣子像kunkun

var child = new Child('cxk', 'father');
child.say() // father好,我是練習時長兩年半的cxk

實現JSON.parse

var json = '{"name":"cxk", "age":25}';
var obj = eval("(" + json + ")");

此方法屬於黑魔法,極易容易被xss攻擊,還有一種new Function大同小異。

簡單的教程看這個半小時實現一個 JSON 解析器

實現Promise

我很早以前實現過一版,並且註釋不少,可是竟然找不到了,這是在網絡上找了一版帶註釋的,目測沒有大問題,具體過程能夠看這篇 史上最易讀懂的 Promise/A+ 徹底實現
var PromisePolyfill = (function () {
  // 和reject不一樣的是resolve須要嘗試展開thenable對象
  function tryToResolve (value) {
    if (this === value) {
    // 主要是防止下面這種狀況
    // let y = new Promise(res => setTimeout(res(y)))
      throw TypeError('Chaining cycle detected for promise!')
    }

    // 根據規範2.32以及2.33 對對象或者函數嘗試展開
    // 保證S6以前的 polyfill 也能和ES6的原生promise混用
    if (value !== null &&
      (typeof value === 'object' || typeof value === 'function')) {
      try {
      // 這裏記錄此次then的值同時要被try包裹
      // 主要緣由是 then 多是一個getter, 也也就是說
      //   1. value.then可能報錯
      //   2. value.then可能產生反作用(例如屢次執行可能結果不一樣)
        var then = value.then

        // 另外一方面, 因爲沒法保證 then 確實會像預期的那樣只調用一個onFullfilled / onRejected
        // 因此增長了一個flag來防止resolveOrReject被屢次調用
        var thenAlreadyCalledOrThrow = false
        if (typeof then === 'function') {
        // 是thenable 那麼嘗試展開
        // 而且在該thenable狀態改變以前this對象的狀態不變
          then.bind(value)(
          // onFullfilled
            function (value2) {
              if (thenAlreadyCalledOrThrow) return
              thenAlreadyCalledOrThrow = true
              tryToResolve.bind(this, value2)()
            }.bind(this),

            // onRejected
            function (reason2) {
              if (thenAlreadyCalledOrThrow) return
              thenAlreadyCalledOrThrow = true
              resolveOrReject.bind(this, 'rejected', reason2)()
            }.bind(this)
          )
        } else {
        // 擁有then 可是then不是一個函數 因此也不是thenable
          resolveOrReject.bind(this, 'resolved', value)()
        }
      } catch (e) {
        if (thenAlreadyCalledOrThrow) return
        thenAlreadyCalledOrThrow = true
        resolveOrReject.bind(this, 'rejected', e)()
      }
    } else {
    // 基本類型 直接返回
      resolveOrReject.bind(this, 'resolved', value)()
    }
  }

  function resolveOrReject (status, data) {
    if (this.status !== 'pending') return
    this.status = status
    this.data = data
    if (status === 'resolved') {
      for (var i = 0; i < this.resolveList.length; ++i) {
        this.resolveList[i]()
      }
    } else {
      for (i = 0; i < this.rejectList.length; ++i) {
        this.rejectList[i]()
      }
    }
  }

  function Promise (executor) {
    if (!(this instanceof Promise)) {
      throw Error('Promise can not be called without new !')
    }

    if (typeof executor !== 'function') {
    // 非標準 但與Chrome谷歌保持一致
      throw TypeError('Promise resolver ' + executor + ' is not a function')
    }

    this.status = 'pending'
    this.resolveList = []
    this.rejectList = []

    try {
      executor(tryToResolve.bind(this), resolveOrReject.bind(this, 'rejected'))
    } catch (e) {
      resolveOrReject.bind(this, 'rejected', e)()
    }
  }

  Promise.prototype.then = function (onFullfilled, onRejected) {
  // 返回值穿透以及錯誤穿透, 注意錯誤穿透用的是throw而不是return,不然的話
  // 這個then返回的promise狀態將變成resolved即接下來的then中的onFullfilled
  // 會被調用, 然而咱們想要調用的是onRejected
    if (typeof onFullfilled !== 'function') {
      onFullfilled = function (data) {
        return data
      }
    }
    if (typeof onRejected !== 'function') {
      onRejected = function (reason) {
        throw reason
      }
    }

    var executor = function (resolve, reject) {
      setTimeout(function () {
        try {
        // 拿到對應的handle函數處理this.data
        // 並以此爲依據解析這個新的Promise
          var value = this.status === 'resolved'
            ? onFullfilled(this.data)
            : onRejected(this.data)
          resolve(value)
        } catch (e) {
          reject(e)
        }
      }.bind(this))
    }

    // then 接受兩個函數返回一個新的Promise
    // then 自身的執行永遠異步與onFullfilled/onRejected的執行
    if (this.status !== 'pending') {
      return new Promise(executor.bind(this))
    } else {
    // pending
      return new Promise(function (resolve, reject) {
        this.resolveList.push(executor.bind(this, resolve, reject))
        this.rejectList.push(executor.bind(this, resolve, reject))
      }.bind(this))
    }
  }

  // for prmise A+ test
  Promise.deferred = Promise.defer = function () {
    var dfd = {}
    dfd.promise = new Promise(function (resolve, reject) {
      dfd.resolve = resolve
      dfd.reject = reject
    })
    return dfd
  }

  // for prmise A+ test
  if (typeof module !== 'undefined') {
    module.exports = Promise
  }

  return Promise
})()

PromisePolyfill.all = function (promises) {
  return new Promise((resolve, reject) => {
    const result = []
    let cnt = 0
    for (let i = 0; i < promises.length; ++i) {
      promises[i].then(value => {
        cnt++
        result[i] = value
        if (cnt === promises.length) resolve(result)
      }, reject)
    }
  })
}

PromisePolyfill.race = function (promises) {
  return new Promise((resolve, reject) => {
    for (let i = 0; i < promises.length; ++i) {
      promises[i].then(resolve, reject)
    }
  })
}

解析 URL Params 爲對象

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url)
/* 結果
{ user: 'anonymous',
  id: [ 123, 456 ], // 重複出現的 key 要組裝成數組,能被轉成數字的就轉成數字類型
  city: '北京', // 中文需解碼
  enabled: true, // 未指定值得 key 約定爲 true
}
*/
function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 後面的字符串取出來
  const paramsArr = paramsStr.split('&'); // 將字符串以 & 分割後存到數組中
  let paramsObj = {};
  // 將 params 存到對象中
  paramsArr.forEach(param => {
    if (/=/.test(param)) { // 處理有 value 的參數
      let [key, val] = param.split('='); // 分割 key 和 value
      val = decodeURIComponent(val); // 解碼
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉爲數字

      if (paramsObj.hasOwnProperty(key)) { // 若是對象有 key,則添加一個值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else { // 若是對象沒有這個 key,建立 key 並設置值
        paramsObj[key] = val;
      }
    } else { // 處理沒有 value 的參數
      paramsObj[param] = true;
    }
  })

  return paramsObj;
}

模板引擎實現

let template = '我是{{name}},年齡{{age}},性別{{sex}}';
let data = {
  name: '姓名',
  age: 18
}
render(template, data); // 我是姓名,年齡18,性別undefined
function render(template, data) {
  const reg = /\{\{(\w+)\}\}/; // 模板字符串正則
  if (reg.test(template)) { // 判斷模板裏是否有模板字符串
    const name = reg.exec(template)[1]; // 查找當前模板裏第一個模板字符串的字段
    template = template.replace(reg, data[name]); // 將第一個模板字符串渲染
    return render(template, data); // 遞歸的渲染並返回渲染後的結構
  }
  return template; // 若是模板沒有模板字符串直接返回
}

轉化爲駝峯命名

var s1 = "get-element-by-id"

// 轉化爲 getElementById
var f = function(s) {
    return s.replace(/-\w/g, function(x) {
        return x.slice(1).toUpperCase();
    })
}

查找字符串中出現最多的字符和個數

例: abbcccddddd -> 字符最多的是d,出現了5次

let str = "abcabcabcbbccccc";
let num = 0;
let char = '';

 // 使其按照必定的次序排列
str = str.split('').sort().join('');
// "aaabbbbbcccccccc"

// 定義正則表達式
let re = /(\w)\1+/g;
str.replace(re,($0,$1) => {
    if(num < $0.length){
        num = $0.length;
        char = $1;        
    }
});
console.log(`字符最多的是${char},出現了${num}次`);

字符串查找

請使用最基本的遍從來實現判斷字符串 a 是否被包含在字符串 b 中,並返回第一次出現的位置(找不到返回 -1)。

a='34';b='1234567'; // 返回 2
a='35';b='1234567'; // 返回 -1
a='355';b='12354355'; // 返回 5
isContain(a,b);
function isContain(a, b) {
  for (let i in b) {
    if (a[0] === b[i]) {
      let tmp = true;
      for (let j in a) {
        if (a[j] !== b[~~i + ~~j]) {
          tmp = false;
        }
      }
      if (tmp) {
        return i;
      }
    }
  }
  return -1;
}

實現千位分隔符

// 保留三位小數
parseToMoney(1234.56); // return '1,234.56'
parseToMoney(123456789); // return '123,456,789'
parseToMoney(1087654.321); // return '1,087,654.321'
function parseToMoney(num) {
  num = parseFloat(num.toFixed(3));
  let [integer, decimal] = String.prototype.split.call(num, '.');
  integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');
  return integer + '.' + (decimal ? decimal : '');
}

正則表達式(運用了正則的前向聲明和反前向聲明):

function parseToMoney(str){
    // 僅僅對位置進行匹配
    let re = /(?=(?!\b)(\d{3})+$)/g; 
   return str.replace(re,','); 
}

判斷是不是電話號碼

function isPhone(tel) {
    var regx = /^1[34578]\d{9}$/;
    return regx.test(tel);
}

驗證是不是郵箱

function isEmail(email) {
    var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/;
    return regx.test(email);
}

驗證是不是身份證

function isCardNo(number) {
    var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/;
    return regx.test(number);
}

公衆號

想要實時關注筆者最新的文章和最新的文檔更新請關注公衆號程序員面試官,後續的文章會優先在公衆號更新.

簡歷模板: 關注公衆號回覆「模板」獲取

《前端面試手冊》: 配套於本指南的突擊手冊,關注公衆號回覆「fed」獲取

相關文章
相關標籤/搜索