騰訊前端一面經典手寫面試題合集

hello_world_1024發表於2023-02-26

查詢字串中出現最多的字元和個數

例: 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}次`);

手寫型別判斷函式

function getType(value) {
  // 判斷資料是 null 的情況
  if (value === null) {
    return value + "";
  }
  // 判斷資料是引用型別的情況
  if (typeof value === "object") {
    let valueClass = Object.prototype.toString.call(value),
      type = valueClass.split(" ")[1].split("");
    type.pop();
    return type.join("").toLowerCase();
  } else {
    // 判斷資料是基本資料型別的情況和函式的情況
    return typeof value;
  }
}

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

手寫 Promise.race

該方法的引數是 Promise 例項陣列, 然後其 then 註冊的回撥方法是陣列中的某一個 Promise 的狀態變為 fulfilled 的時候就執行. 因為 Promise 的狀態只能改變一次, 那麼我們只需要把 Promise.race 中產生的 Promise 物件的 resolve 方法, 注入到陣列中的每一個 Promise 例項中的回撥函式中即可.

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

將數字每千分位用逗號隔開

數字有小數版本:

let format = n => {
    let num = n.toString() // 轉成字串
    let decimals = ''
        // 判斷是否有小數
    num.indexOf('.') > -1 ? decimals = num.split('.')[1] : decimals
    let len = num.length
    if (len <= 3) {
        return num
    } else {
        let temp = ''
        let remainder = len % 3
        decimals ? temp = '.' + decimals : temp
        if (remainder > 0) { // 不是3的整數倍
            return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') + temp
        } else { // 是3的整數倍
            return num.slice(0, len).match(/\d{3}/g).join(',') + temp 
        }
    }
}
format(12323.33)  // '12,323.33'

數字無小數版本:

let format = n => {
    let num = n.toString() 
    let len = num.length
    if (len <= 3) {
        return num
    } else {
        let remainder = len % 3
        if (remainder > 0) { // 不是3的整數倍
            return num.slice(0, remainder) + ',' + num.slice(remainder, len).match(/\d{3}/g).join(',') 
        } else { // 是3的整數倍
            return num.slice(0, len).match(/\d{3}/g).join(',') 
        }
    }
}
format(1232323)  // '1,232,323'

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

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

手寫 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]
})

手寫 apply 函式

apply 函式的實現步驟:

  1. 判斷呼叫物件是否為函式,即使我們是定義在函式的原型上的,但是可能出現使用 call 等方式呼叫的情況。
  2. 判斷傳入上下文物件是否存在,如果不存在,則設定為 window 。
  3. 將函式作為上下文物件的一個屬性。
  4. 判斷引數值是否傳入
  5. 使用上下文物件來呼叫這個方法,並儲存返回結果。
  6. 刪除剛才新增的屬性
  7. 返回結果
// apply 函式實現
Function.prototype.myApply = function(context) {
  // 判斷呼叫物件是否為函式
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判斷 context 是否存在,如果未傳入則為 window
  context = context || window;
  // 將函式設為物件的方法
  context.fn = this;
  // 呼叫方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 將屬性刪除
  delete context.fn;
  return result;
};

實現字串翻轉

在字串的原型鏈上新增一個方法,實現字串翻轉:

String.prototype._reverse = function(a){
    return a.split("").reverse().join("");
}
var obj = new String();
var res = obj._reverse ('hello');
console.log(res);    // olleh

需要注意的是,必須透過例項化物件之後再去呼叫定義的方法,不然找不到該方法。

轉化為駝峰命名

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

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

二叉樹層次遍歷

// 二叉樹層次遍歷

class Node {
  constructor(element, parent) {
    this.parent = parent // 父節點 
    this.element = element // 當前儲存內容
    this.left = null // 左子樹
    this.right = null // 右子樹
  }
}

class BST {
  constructor(compare) {
    this.root = null // 樹根
    this.size = 0 // 樹中的節點個數

    this.compare = compare || this.compare
  }
  compare(a,b) {
    return a - b
  }
  add(element) {
    if(this.root === null) {
      this.root = new Node(element, null)
      this.size++
      return
    }
    // 獲取根節點 用當前新增的進行判斷 放左邊還是放右邊
    let currentNode = this.root 
    let compare
    let parent = null 
    while (currentNode) {
      compare = this.compare(element, currentNode.element)
      parent = currentNode // 先將父親儲存起來
      // currentNode要不停的變化
      if(compare > 0) {
        currentNode = currentNode.right
      } else if(compare < 0) {
        currentNode = currentNode.left
      } else {
        currentNode.element = element // 相等時 先覆蓋後續處理
      }
    }

    let newNode = new Node(element, parent)
    if(compare > 0) {
      parent.right = newNode
    } else if(compare < 0) {
      parent.left = newNode
    }

    this.size++
  }
  // 層次遍歷 佇列
  levelOrderTraversal(visitor) {
    if(this.root == null) {
      return
    }
    let stack = [this.root]
    let index = 0 // 指標 指向0
    let currentNode 
    while (currentNode = stack[index++]) {
      // 反轉二叉樹
      let tmp = currentNode.left
      currentNode.left = currentNode.right
      currentNode.right = tmp
      visitor.visit(currentNode.element)
      if(currentNode.left) {
        stack.push(currentNode.left)
      }
      if(currentNode.right) {
        stack.push(currentNode.right)
      }
    }
  }
}
// 測試
var bst = new BST((a,b)=>a.age-b.age) // 模擬sort方法

// ![](http://img-repo.poetries.top/images/20210522203619.png)
// ![](http://img-repo.poetries.top/images/20210522211809.png)
bst.add({age: 10})
bst.add({age: 8})
bst.add({age:19})
bst.add({age:6})
bst.add({age: 15})
bst.add({age: 22})
bst.add({age: 20})

// 使用訪問者模式
class Visitor {
  constructor() {
    this.visit = function (elem) {
      elem.age = elem.age*2
    }
  }
}

// ![](http://img-repo.poetries.top/images/20210523095515.png)
console.log(bst.levelOrderTraversal(new Visitor()))

實現一個拖拽

<style>
  html, body {
    margin: 0;
    height: 100%;
  }
  #box {
    width: 100px;
    height: 100px;
    background-color: red;
    position: absolute;
    top: 100px;
    left: 100px;
  }
</style>
<div id="box"></div>
window.onload = function () {
  var box = document.getElementById('box');
  box.onmousedown = function (ev) {
    var oEvent = ev || window.event; // 相容火狐,火狐下沒有window.event
    var distanceX = oEvent.clientX - box.offsetLeft; // 滑鼠到可視區左邊的距離 - box到頁面左邊的距離
    var distanceY = oEvent.clientY - box.offsetTop;
    document.onmousemove = function (ev) {
      var oEvent = ev || window.event;
      var left = oEvent.clientX - distanceX;
      var top = oEvent.clientY - distanceY;
      if (left <= 0) {
        left = 0;
      } else if (left >= document.documentElement.clientWidth - box.offsetWidth) {
        left = document.documentElement.clientWidth - box.offsetWidth;
      }
      if (top <= 0) {
        top = 0;
      } else if (top >= document.documentElement.clientHeight - box.offsetHeight) {
        top = document.documentElement.clientHeight - box.offsetHeight;
      }
      box.style.left = left + 'px';
      box.style.top = top + 'px';
    }
    box.onmouseup = function () {
      document.onmousemove = null;
      box.onmouseup = null;
    }
  }
}

判斷括號字串是否有效(小米)

題目描述

給定一個只包括 '(',')','{','}','[',']' 的字串 s ,判斷字串是否有效。

有效字串需滿足:
- 左括號必須用相同型別的右括號閉合。
- 左括號必須以正確的順序閉合。

示例 1:

輸入:s = "()"
輸出:true

示例 2:

輸入:s = "()[]{}"
輸出:true

示例 3:

輸入:s = "(]"
輸出:false

答案

const isValid = function (s) {
  if (s.length % 2 === 1) {
    return false;
  }
  const regObj = {
    "{": "}",
    "(": ")",
    "[": "]",
  };
  let stack = [];
  for (let i = 0; i < s.length; i++) {
    if (s[i] === "{" || s[i] === "(" || s[i] === "[") {
      stack.push(s[i]);
    } else {
      const cur = stack.pop();
      if (s[i] !== regObj[cur]) {
        return false;
      }
    }
  }

  if (stack.length) {
    return false;
  }

  return true;
};

非同步併發數限制

/**
 * 關鍵點
 * 1. new promise 一經建立,立即執行
 * 2. 使用 Promise.resolve().then 可以把任務加到微任務佇列,防止立即執行迭代方法
 * 3. 微任務處理過程中,產生的新的微任務,會在同一事件迴圈內,追加到微任務佇列裡
 * 4. 使用 race 在某個任務完成時,繼續新增任務,保持任務按照最大併發數進行執行
 * 5. 任務完成後,需要從 doingTasks 中移出
 */
function limit(count, array, iterateFunc) {
  const tasks = []
  const doingTasks = []
  let i = 0
  const enqueue = () => {
    if (i === array.length) {
      return Promise.resolve()
    }
    const task = Promise.resolve().then(() => iterateFunc(array[i++]))
    tasks.push(task)
    const doing = task.then(() => doingTasks.splice(doingTasks.indexOf(doing), 1))
    doingTasks.push(doing)
    const res = doingTasks.length >= count ? Promise.race(doingTasks) : Promise.resolve()
    return res.then(enqueue)
  };
  return enqueue().then(() => Promise.all(tasks))
}

// test
const timeout = i => new Promise(resolve => setTimeout(() => resolve(i), i))
limit(2, [1000, 1000, 1000, 1000], timeout).then((res) => {
  console.log(res)
})

實現日期格式化函式

輸入:

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
}

實現陣列的flat方法

function _flat(arr, depth) {
  if(!Array.isArray(arr) || depth <= 0) {
    return arr;
  }
  return arr.reduce((prev, cur) => {
    if (Array.isArray(cur)) {
      return prev.concat(_flat(cur, depth - 1))
    } else {
      return prev.concat(cur);
    }
  }, []);
}

字串最長的不重複子串

題目描述

給定一個字串 s ,請你找出其中不含有重複字元的 最長子串 的長度。


示例 1:

輸入: s = "abcabcbb"
輸出: 3
解釋: 因為無重複字元的最長子串是 "abc",所以其長度為 3。

示例 2:

輸入: s = "bbbbb"
輸出: 1
解釋: 因為無重複字元的最長子串是 "b",所以其長度為 1。

示例 3:

輸入: s = "pwwkew"
輸出: 3
解釋: 因為無重複字元的最長子串是 "wke",所以其長度為 3。
     請注意,你的答案必須是 子串 的長度,"pwke" 是一個子序列,不是子串。

示例 4:

輸入: s = ""
輸出: 0

答案

const lengthOfLongestSubstring = function (s) {
  if (s.length === 0) {
    return 0;
  }

  let left = 0;
  let right = 1;
  let max = 0;
  while (right <= s.length) {
    let lr = s.slice(left, right);
    const index = lr.indexOf(s[right]);

    if (index > -1) {
      left = index + left + 1;
    } else {
      lr = s.slice(left, right + 1);
      max = Math.max(max, lr.length);
    }
    right++;
  }
  return max;
};

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

[
    {
        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;
}

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

迴圈引用物件本來沒有什麼問題,但是序列化的時候就會發生問題,比如呼叫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
}

實現call方法

call做了什麼:

  • 將函式設為物件的屬性
  • 執行和刪除這個函式
  • 指定this到函式並傳入給定引數執行函式
  • 如果不傳入引數,預設指向為 window
// 模擬 call bar.mycall(null);
//實現一個call方法:
// 原理:利用 context.xxx = self obj.xx = func-->obj.xx()
Function.prototype.myCall = function(context = window, ...args) {
  if (typeof this !== "function") {
    throw new Error('type error')
  }
  // this-->func  context--> obj  args--> 傳遞過來的引數

  // 在context上加一個唯一值不影響context上的屬性
  let key = Symbol('key')
  context[key] = this; // context為呼叫的上下文,this此處為函式,將這個函式作為context的方法
  // let args = [...arguments].slice(1)   //第一個引數為obj所以刪除,偽陣列轉為陣列

  // 繫結引數 並執行函式
  let result = context[key](...args);
  // 清除定義的this 不刪除會導致context屬性越來越多
  delete context[key];

  // 返回結果 
  return result;
};
//用法:f.call(obj,arg1)
function f(a,b){
 console.log(a+b)
 console.log(this.name)
}
let obj={
 name:1
}
f.myCall(obj,1,2) //否則this指向window

相關文章