京東前端二面常考手寫面試題(必備)

hello_world_1024發表於2023-03-07

實現釋出-訂閱模式

class EventCenter{
  // 1. 定義事件容器,用來裝事件陣列
    let handlers = {}

  // 2. 新增事件方法,引數:事件名 事件方法
  addEventListener(type, handler) {
    // 建立新陣列容器
    if (!this.handlers[type]) {
      this.handlers[type] = []
    }
    // 存入事件
    this.handlers[type].push(handler)
  }

  // 3. 觸發事件,引數:事件名 事件引數
  dispatchEvent(type, params) {
    // 若沒有註冊該事件則丟擲錯誤
    if (!this.handlers[type]) {
      return new Error('該事件未註冊')
    }
    // 觸發事件
    this.handlers[type].forEach(handler => {
      handler(...params)
    })
  }

  // 4. 事件移除,引數:事件名 要刪除事件,若無第二個引數則刪除該事件的訂閱和釋出
  removeEventListener(type, handler) {
    if (!this.handlers[type]) {
      return new Error('事件無效')
    }
    if (!handler) {
      // 移除事件
      delete this.handlers[type]
    } else {
      const index = this.handlers[type].findIndex(el => el === handler)
      if (index === -1) {
        return new Error('無該繫結事件')
      }
      // 移除事件
      this.handlers[type].splice(index, 1)
      if (this.handlers[type].length === 0) {
        delete this.handlers[type]
      }
    }
  }
}

手寫 new 運算子

在呼叫 new 的過程中會發生以上四件事情:

(1)首先建立了一個新的空物件

(2)設定原型,將物件的原型設定為函式的 prototype 物件。

(3)讓函式的 this 指向這個物件,執行建構函式的程式碼(為這個新物件新增屬性)

(4)判斷函式的返回值型別,如果是值型別,返回建立的物件。如果是引用型別,就返回這個引用型別的物件。

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判斷引數是否是一個函式
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一個空物件,物件的原型為建構函式的 prototype 物件
  newObject = Object.create(constructor.prototype);
  // 將 this 指向新建物件,並執行函式
  result = constructor.apply(newObject, arguments);
  // 判斷返回物件
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判斷返回結果
  return flag ? result : newObject;
}
// 使用方法
objectFactory(建構函式, 初始化引數);

實現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;
};

debounce(防抖)

觸發高頻時間後n秒內函式只會執行一次,如果n秒內高頻時間再次觸發,則重新計算時間。

const debounce = (fn, time) => {
  let timeout = null;
  return function() {
    clearTimeout(timeout)
    timeout = setTimeout(() => {
      fn.apply(this, arguments);
    }, time);
  }
};

防抖常應用於使用者進行搜尋輸入節約請求資源,window觸發resize事件時進行防抖只觸發一次。

判斷物件是否存在迴圈引用

迴圈引用物件本來沒有什麼問題,但是序列化的時候就會發生問題,比如呼叫JSON.stringify()對該類物件進行序列化,就會報錯: Converting circular structure to JSON.

下面方法可以用來判斷一個物件中是否已存在迴圈引用:

const isCycleObject = (obj,parent) => {
    const parentArr = parent || [obj];
    for(let i in obj) {
        if(typeof obj[i] === 'object') {
            let flag = false;
            parentArr.forEach((pObj) => {
                if(pObj === obj[i]){
                    flag = true;
                }
            })
            if(flag) return true;
            flag = isCycleObject(obj[i],[...parentArr,obj[i]]);
            if(flag) return true;
        }
    }
    return false;
}


const a = 1;
const b = {a};
const c = {b};
const o = {d:{a:3},c}
o.c.b.aa = a;

console.log(isCycleObject(o)

查詢有序二維陣列的目標值:

var findNumberIn2DArray = function(matrix, target) {
    if (matrix == null || matrix.length == 0) {
        return false;
    }
    let row = 0;
    let column = matrix[0].length - 1;
    while (row < matrix.length && column >= 0) {
        if (matrix[row][column] == target) {
            return true;
        } else if (matrix[row][column] > target) {
            column--;
        } else {
            row++;
        }
    }
    return false;
};

二維陣列斜向列印:

function printMatrix(arr){
  let m = arr.length, n = arr[0].length
    let res = []

  // 左上角,從0 到 n - 1 列進行列印
  for (let k = 0; k < n; k++) {
    for (let i = 0, j = k; i < m && j >= 0; i++, j--) {
      res.push(arr[i][j]);
    }
  }

  // 右下角,從1 到 n - 1 行進行列印
  for (let k = 1; k < m; k++) {
    for (let i = k, j = n - 1; i < m && j >= 0; i++, j--) {
      res.push(arr[i][j]);
    }
  }
  return res
}

實現節流函式(throttle)

防抖函式原理:規定在一個單位時間內,只能觸發一次函式。如果這個單位時間內觸發多次函式,只有一次生效。

// 手寫簡化版

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

適用場景:

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

參考 前端進階面試題詳細解答

實現日期格式化函式

輸入:

dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd') // 2020/12/01
dateFormat(new Date('2020-04-01'), 'yyyy/MM/dd') // 2020/04/01
dateFormat(new Date('2020-04-01'), 'yyyy年MM月dd日') // 2020年04月01日
const dateFormat = (dateInput, format)=>{
    var day = dateInput.getDate() 
    var month = dateInput.getMonth() + 1  
    var year = dateInput.getFullYear()   
    format = format.replace(/yyyy/, year)
    format = format.replace(/MM/,month)
    format = format.replace(/dd/,day)
    return format
}

實現AJAX請求

AJAX是 Asynchronous JavaScript and XML 的縮寫,指的是透過 JavaScript 的 非同步通訊,從伺服器獲取 XML 文件從中提取資料,再更新當前網頁的對應部分,而不用重新整理整個網頁。

建立AJAX請求的步驟:

  • 建立一個 XMLHttpRequest 物件。
  • 在這個物件上使用 open 方法建立一個 HTTP 請求,open 方法所需要的引數是請求的方法、請求的地址、是否非同步和使用者的認證資訊。
  • 在發起請求前,可以為這個物件新增一些資訊和監聽函式。比如說可以透過 setRequestHeader 方法來為請求新增頭資訊。還可以為這個物件新增一個狀態監聽函式。一個 XMLHttpRequest 物件一共有 5 個狀態,當它的狀態變化時會觸發onreadystatechange 事件,可以透過設定監聽函式,來處理請求成功後的結果。當物件的 readyState 變為 4 的時候,代表伺服器返回的資料接收完成,這個時候可以透過判斷請求的狀態,如果狀態是 2xx 或者 304 的話則代表返回正常。這個時候就可以透過 response 中的資料來對頁面進行更新了。
  • 當物件的屬性和監聽函式設定完成後,最後調用 sent 方法來向伺服器發起請求,可以傳入引數作為傳送的資料體。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 建立 Http 請求
xhr.open("GET", SERVER_URL, true);
// 設定狀態監聽函式
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 當請求成功時
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 設定請求失敗時的監聽函式
xhr.onerror = function() {
  console.error(this.statusText);
};
// 設定請求頭資訊
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 傳送 Http 請求
xhr.send(null);

驗證是否是身份證

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

陣列去重

const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
// => [1, '1', 17, true, false, 'true', 'a', {}, {}]
方法一:利用Set
const res1 = Array.from(new Set(arr));
方法二:兩層for迴圈+splice
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;
}
方法三:利用indexOf
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,思路大同小異。

方法四:利用include
const unique3 = arr => {
  const res = [];
  for (let i = 0; i < arr.length; 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;
  });
}
方法六:利用Map
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;
}

驗證是否是郵箱

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

手寫 Promise.all

1) 核心思路

  1. 接收一個 Promise 例項的陣列或具有 Iterator 介面的物件作為引數
  2. 這個方法返回一個新的 promise 物件,
  3. 遍歷傳入的引數,用Promise.resolve()將引數"包一層",使其變成一個promise物件
  4. 引數所有回撥成功才是成功,返回值陣列與引數順序一致
  5. 引數陣列其中一個失敗,則觸發失敗狀態,第一個觸發失敗的 Promise 錯誤資訊作為 Promise.all 的錯誤資訊。

2)實現程式碼

一般來說,Promise.all 用來處理多個併發請求,也是為了頁面資料構造的方便,將一個頁面所用到的在不同介面的資料一起請求過來,不過,如果其中一個介面失敗了,多個請求也就失敗了,頁面可能啥也出不來,這就看當前頁面的耦合程度了

function promiseAll(promises) {
  return new Promise(function(resolve, reject) {
    if(!Array.isArray(promises)){
        throw new TypeError(`argument must be a array`)
    }
    var resolvedCounter = 0;
    var promiseNum = promises.length;
    var resolvedResult = [];
    for (let i = 0; i < promiseNum; i++) {
      Promise.resolve(promises[i]).then(value=>{
        resolvedCounter++;
        resolvedResult[i] = value;
        if (resolvedCounter == promiseNum) {
            return resolve(resolvedResult)
          }
      },error=>{
        return reject(error)
      })
    }
  })
}
// test
let p1 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(1)
    }, 1000)
})
let p2 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(2)
    }, 2000)
})
let p3 = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve(3)
    }, 3000)
})
promiseAll([p3, p1, p2]).then(res => {
    console.log(res) // [3, 1, 2]
})

實現陣列的push方法

let arr = [];
Array.prototype.push = function() {
    for( let i = 0 ; i < arguments.length ; i++){
        this[this.length] = arguments[i] ;
    }
    return this.length;
}

模板引擎實現

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; // 如果模板沒有模板字串直接返回
}

實現字串的repeat方法

輸入字串s,以及其重複的次數,輸出重複的結果,例如輸入abc,2,輸出abcabc。

function repeat(s, n) {
    return (new Array(n + 1)).join(s);
}

遞迴:

function repeat(s, n) {
    return (n > 0) ? s.concat(repeat(s, --n)) : "";
}

實現釋出訂閱模式

簡介:

釋出訂閱者模式,一種物件間一對多的依賴關係,但一個物件的狀態發生改變時,所依賴它的物件都將得到狀態改變的通知。

主要的作用(優點):

  1. 廣泛應用於非同步程式設計中(替代了傳遞迴調函式)
  2. 物件之間鬆散耦合的編寫程式碼

缺點:

  • 建立訂閱者本身要消耗一定的時間和記憶體
  • 多個釋出者和訂閱者巢狀一起的時候,程式難以跟蹤維護

實現的思路:

  • 建立一個物件(快取列表)
  • on方法用來把回撥函式fn都加到快取列表中
  • emit 根據key值去執行對應快取列表中的函式
  • off方法可以根據key值取消訂閱
class EventEmiter {
  constructor() {
    // 事件物件,存放訂閱的名字和事件
    this._events = {}
  }
  // 訂閱事件的方法
  on(eventName,callback) {
    if(!this._events) {
      this._events = {}
    }
    // 合併之前訂閱的cb
    this._events[eventName] = [...(this._events[eventName] || []),callback]
  }
  // 觸發事件的方法
  emit(eventName, ...args) {
    if(!this._events[eventName]) {
      return
    }
    // 遍歷執行所有訂閱的事件
    this._events[eventName].forEach(fn=>fn(...args))
  }
  off(eventName,cb) {
    if(!this._events[eventName]) {
      return
    }
    // 刪除訂閱的事件
    this._events[eventName] = this._events[eventName].filter(fn=>fn != cb && fn.l != cb)
  }
  // 繫結一次 觸發後將繫結的移除掉 再次觸發掉
  once(eventName,callback) {
    const one = (...args)=>{
      // 等callback執行完畢在刪除
      callback(args)
      this.off(eventName,one)
    }
    one.l = callback // 自定義屬性
    this.on(eventName,one)
  }
}

測試用例

let event = new EventEmiter()

let login1 = function(...args) {
  console.log('login success1', args)
}
let login2 = function(...args) {
  console.log('login success2', args)
}
// event.on('login',login1)
event.once('login',login2)
event.off('login',login1) // 解除訂閱
event.emit('login', 1,2,3,4,5)
event.emit('login', 6,7,8,9)
event.emit('login', 10,11,12)  

釋出訂閱者模式和觀察者模式的區別?

  • 釋出/訂閱模式是觀察者模式的一種變形,兩者區別在於,釋出/訂閱模式在觀察者模式的基礎上,在目標和觀察者之間增加一個排程中心。
  • 觀察者模式是由具體目標排程,比如當事件觸發,Subject 就會去呼叫觀察者的方法,所以觀察者模式的訂閱者與釋出者之間是存在依賴的。
  • 釋出/訂閱模式由統一排程中心呼叫,因此釋出者和訂閱者不需要知道對方的存在。

使用ES5和ES6求函式引數的和

ES5:

function sum() {
    let sum = 0
    Array.prototype.forEach.call(arguments, function(item) {
        sum += item * 1
    })
    return sum
}

ES6:

function sum(...nums) {
    let sum = 0
    nums.forEach(function(item) {
        sum += item * 1
    })
    return sum
}

實現斐波那契數列

// 遞迴
function fn (n){
    if(n==0) return 0
    if(n==1) return 1
    return fn(n-2)+fn(n-1)
}
// 最佳化
function fibonacci2(n) {
    const arr = [1, 1, 2];
    const arrLen = arr.length;

    if (n <= arrLen) {
        return arr[n];
    }

    for (let i = arrLen; i < n; i++) {
        arr.push(arr[i - 1] + arr[ i - 2]);
    }

    return arr[arr.length - 1];
}
// 非遞迴
function fn(n) {
    let pre1 = 1;
    let pre2 = 1;
    let current = 2;

    if (n <= 2) {
        return current;
    }

    for (let i = 2; i < n; i++) {
        pre1 = pre2;
        pre2 = current;
        current = pre1 + pre2;
    }

    return current;
}

手寫深度比較isEqual

思路:深度比較兩個物件,就是要深度比較物件的每一個元素。=> 遞迴
  • 遞迴退出條件:

    • 被比較的是兩個值型別變數,直接用“===”判斷
    • 被比較的兩個變數之一為null,直接判斷另一個元素是否也為null
  • 提前結束遞推:

    • 兩個變數keys數量不同
    • 傳入的兩個引數是同一個變數
  • 遞推工作:深度比較每一個key
function isEqual(obj1, obj2){
    //其中一個為值型別或null
    if(!isObject(obj1) || !isObject(obj2)){
        return obj1 === obj2;
    }

    //判斷是否兩個引數是同一個變數
    if(obj1 === obj2){
        return true;
    }

    //判斷keys數是否相等
    const obj1Keys = Object.keys(obj1);
    const obj2Keys = Object.keys(obj2);
    if(obj1Keys.length !== obj2Keys.length){
        return false;
    }

    //深度比較每一個key
    for(let key in obj1){
        if(!isEqual(obj1[key], obj2[key])){
            return false;
        }
    }

    return true;
}

樹形結構轉成列表(處理選單)

[
    {
        id: 1,
        text: '節點1',
        parentId: 0,
        children: [
            {
                id:2,
                text: '節點1_1',
                parentId:1
            }
        ]
    }
]
轉成
[
    {
        id: 1,
        text: '節點1',
        parentId: 0 //這裡用0表示為頂級節點
    },
    {
        id: 2,
        text: '節點1_1',
        parentId: 1 //透過這個欄位來確定子父級
    }
    ...
]

實現程式碼如下:

function treeToList(data) {
  let res = [];
  const dfs = (tree) => {
    tree.forEach((item) => {
      if (item.children) {
        dfs(item.children);
        delete item.children;
      }
      res.push(item);
    });
  };
  dfs(data);
  return res;
}

相關文章