騰訊前端二面手寫面試題

hello_world_1024發表於2023-02-07

函式柯里化的實現

函式柯里化指的是一種將使用多個引數的一個函式轉換成一系列使用一個引數的函式的技術。

function curry(fn, args) {
  // 獲取函式需要的引數長度
  let length = fn.length;

  args = args || [];

  return function() {
    let subArgs = args.slice(0);

    // 拼接得到現有的所有引數
    for (let i = 0; i < arguments.length; i++) {
      subArgs.push(arguments[i]);
    }

    // 判斷引數的長度是否已經滿足函式所需引數的長度
    if (subArgs.length >= length) {
      // 如果滿足,執行函式
      return fn.apply(this, subArgs);
    } else {
      // 如果不滿足,遞迴返回科裡化的函式,等待引數的傳入
      return curry.call(this, fn, subArgs);
    }
  };
}

// es6 實現
function curry(fn, ...args) {
  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}

實現map方法

  • 回撥函式的引數有哪些,返回值如何處理
  • 不修改原來的陣列
Array.prototype.myMap = function(callback, context){
  // 轉換類陣列
  var arr = Array.prototype.slice.call(this),//由於是ES5所以就不用...展開符了
      mappedArr = [], 
      i = 0;

  for (; i < arr.length; i++ ){
    // 把當前值、索引、當前陣列返回去。呼叫的時候傳到函式引數中 [1,2,3,4].map((curr,index,arr))
    mappedArr.push(callback.call(context, arr[i], i, this));
  }
  return mappedArr;
}

實現簡單路由

// hash路由
class Route{
  constructor(){
    // 路由儲存物件
    this.routes = {}
    // 當前hash
    this.currentHash = ''
    // 繫結this,避免監聽時this指向改變
    this.freshRoute = this.freshRoute.bind(this)
    // 監聽
    window.addEventListener('load', this.freshRoute, false)
    window.addEventListener('hashchange', this.freshRoute, false)
  }
  // 儲存
  storeRoute (path, cb) {
    this.routes[path] = cb || function () {}
  }
  // 更新
  freshRoute () {
    this.currentHash = location.hash.slice(1) || '/'
    this.routes[this.currentHash]()
  }
}

交換a,b的值,不能用臨時變數

巧妙的利用兩個數的和、差:

a = a + b
b = a - b
a = a - b

迴圈列印紅黃綠

下面來看一道比較典型的問題,透過這個問題來對比幾種非同步程式設計方法:紅燈 3s 亮一次,綠燈 1s 亮一次,黃燈 2s 亮一次;如何讓三個燈不斷交替重複亮燈?

三個亮燈函式:

function red() {
    console.log('red');
}
function green() {
    console.log('green');
}
function yellow() {
    console.log('yellow');
}

這道題複雜的地方在於需要“交替重複”亮燈,而不是“亮完一次”就結束了。

(1)用 callback 實現

const task = (timer, light, callback) => {
    setTimeout(() => {
        if (light === 'red') {
            red()
        }
        else if (light === 'green') {
            green()
        }
        else if (light === 'yellow') {
            yellow()
        }
        callback()
    }, timer)
}
task(3000, 'red', () => {
    task(2000, 'green', () => {
        task(1000, 'yellow', Function.prototype)
    })
})

這裡存在一個 bug:程式碼只是完成了一次流程,執行後紅黃綠燈分別只亮一次。該如何讓它交替重複進行呢?

上面提到過遞迴,可以遞迴亮燈的一個週期:

const step = () => {
    task(3000, 'red', () => {
        task(2000, 'green', () => {
            task(1000, 'yellow', step)
        })
    })
}
step()

注意看黃燈亮的回撥裡又再次呼叫了 step 方法 以完成迴圈亮燈。

(2)用 promise 實現

const task = (timer, light) => 
    new Promise((resolve, reject) => {
        setTimeout(() => {
            if (light === 'red') {
                red()
            }
            else if (light === 'green') {
                green()
            }
            else if (light === 'yellow') {
                yellow()
            }
            resolve()
        }, timer)
    })
const step = () => {
    task(3000, 'red')
        .then(() => task(2000, 'green'))
        .then(() => task(2100, 'yellow'))
        .then(step)
}
step()

這裡將回撥移除,在一次亮燈結束後,resolve 當前 promise,並依然使用遞迴進行。

(3)用 async/await 實現

const taskRunner =  async () => {
    await task(3000, 'red')
    await task(2000, 'green')
    await task(2100, 'yellow')
    taskRunner()
}
taskRunner()

字串出現的不重複最長長度

用一個滑動視窗裝沒有重複的字元,列舉字元記錄最大值即可。用 map 維護字元的索引,遇到相同的字元,把左邊界移動過去即可。挪動的過程中記錄最大長度:

var lengthOfLongestSubstring = function (s) {
    let map = new Map();
    let i = -1
    let res = 0
    let n = s.length
    for (let j = 0; j < n; j++) {
        if (map.has(s[j])) {
            i = Math.max(i, map.get(s[j]))
        }
        res = Math.max(res, j - i)
        map.set(s[j], j)
    }
    return res
};

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

將虛擬 Dom 轉化為真實 Dom

{
  tag: 'DIV',
  attrs:{
  id:'app'
  },
  children: [
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [
        { tag: 'A', children: [] },
        { tag: 'A', children: [] }
      ]
    }
  ]
}

把上面虛擬Dom轉化成下方真實Dom

<div id="app">
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

實現

// 真正的渲染函式
function _render(vnode) {
  // 如果是數字型別轉化為字串
  if (typeof vnode === "number") {
    vnode = String(vnode);
  }
  // 字串型別直接就是文位元組點
  if (typeof vnode === "string") {
    return document.createTextNode(vnode);
  }
  // 普通DOM
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // 遍歷屬性
    Object.keys(vnode.attrs).forEach((key) => {
      const value = vnode.attrs[key];
      dom.setAttribute(key, value);
    });
  }
  // 子陣列進行遞迴操作
  vnode.children.forEach((child) => dom.appendChild(_render(child)));
  return dom;
}

實現filter方法

Array.prototype.myFilter=function(callback, context=window){

  let len = this.length
      newArr = [],
      i=0

  for(; i < len; i++){
    if(callback.apply(context, [this[i], i , this])){
      newArr.push(this[i]);
    }
  }
  return newArr;
}

手寫節流函式

函式節流是指規定一個單位時間,在這個單位時間內,只能有一次觸發事件的回撥函式執行,如果在同一個單位時間內某事件被觸發多次,只有一次能生效。節流可以使用在 scroll 函式的事件監聽上,透過事件節流來降低事件呼叫的頻率。

// 函式節流的實現;
function throttle(fn, delay) {
  let curTime = Date.now();

  return function() {
    let context = this,
        args = arguments,
        nowTime = Date.now();

    // 如果兩次時間間隔超過了指定時間,則執行函式。
    if (nowTime - curTime >= delay) {
      curTime = Date.now();
      return fn.apply(context, args);
    }
  };
}

實現觀察者模式

觀察者模式(基於釋出訂閱模式) 有觀察者,也有被觀察者

觀察者需要放到被觀察者中,被觀察者的狀態變化需要通知觀察者 我變化了 內部也是基於釋出訂閱模式,收集觀察者,狀態變化後要主動通知觀察者

class Subject { // 被觀察者 學生
  constructor(name) {
    this.state = 'happy'
    this.observers = []; // 儲存所有的觀察者
  }
  // 收集所有的觀察者
  attach(o){ // Subject. prototype. attch
    this.observers.push(o)
  }
  // 更新被觀察者 狀態的方法
  setState(newState) {
    this.state = newState; // 更新狀態
    // this 指被觀察者 學生
    this.observers.forEach(o => o.update(this)) // 通知觀察者 更新它們的狀態
  }
}

class Observer{ // 觀察者 父母和老師
  constructor(name) {
    this.name = name
  }
  update(student) {
    console.log('當前' + this.name + '被通知了', '當前學生的狀態是' + student.state)
  }
}

let student = new Subject('學生'); 

let parent = new Observer('父母'); 
let teacher = new Observer('老師'); 

// 被觀察者儲存觀察者的前提,需要先接納觀察者
student. attach(parent); 
student. attach(teacher); 
student. setState('被欺負了');

實現陣列的push方法

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

debounce(防抖)

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

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

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

Promise.all

Promise.all是支援鏈式呼叫的,本質上就是返回了一個Promise例項,透過resolvereject來改變例項狀態。

Promise.myAll = function(promiseArr) {
  return new Promise((resolve, reject) => {
    const ans = [];
    let index = 0;
    for (let i = 0; i < promiseArr.length; i++) {
      promiseArr[i]
      .then(res => {
        ans[i] = res;
        index++;
        if (index === promiseArr.length) {
          resolve(ans);
        }
      })
      .catch(err => reject(err));
    }
  })
}

手寫 Object.create

思路:將傳入的物件作為原型

function create(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}

實現Vue reactive響應式

// Dep module
class Dep {
  static stack = []
  static target = null
  deps = null

  constructor() {
    this.deps = new Set()
  }

  depend() {
    if (Dep.target) {
      this.deps.add(Dep.target)
    }
  }

  notify() {
    this.deps.forEach(w => w.update())
  }

  static pushTarget(t) {
    if (this.target) {
      this.stack.push(this.target)
    }
    this.target = t
  }

  static popTarget() {
    this.target = this.stack.pop()
  }
}

// reactive
function reactive(o) {
  if (o && typeof o === 'object') {
    Object.keys(o).forEach(k => {
      defineReactive(o, k, o[k])
    })
  }
  return o
}

function defineReactive(obj, k, val) {
  let dep = new Dep()
  Object.defineProperty(obj, k, {
    get() {
      dep.depend()
      return val
    },
    set(newVal) {
      val = newVal
      dep.notify()
    }
  })
  if (val && typeof val === 'object') {
    reactive(val)
  }
}

// watcher
class Watcher {
  constructor(effect) {
    this.effect = effect
    this.update()
  }

  update() {
    Dep.pushTarget(this)
    this.value = this.effect()
    Dep.popTarget()
    return this.value
  }
}

// 測試程式碼
const data = reactive({
  msg: 'aaa'
})

new Watcher(() => {
  console.log('===> effect', data.msg);
})

setTimeout(() => {
  data.msg = 'hello'
}, 1000)

模擬Object.create

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

// 模擬 Object.create

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

  return new F();
}

實現類的繼承

實現類的繼承-簡版

類的繼承在幾年前是重點內容,有n種繼承方式各有優劣,es6普及後越來越不重要,那麼多種寫法有點『回字有四樣寫法』的意思,如果還想深入理解的去看紅寶書即可,我們目前只實現一種最理想的繼承方式。
// 寄生組合繼承
function Parent(name) {
  this.name = name
}
Parent.prototype.say = function() {
  console.log(this.name + ` say`);
}
Parent.prototype.play = function() {
  console.log(this.name + ` play`);
}

function Child(name, parent) {
  // 將父類的建構函式繫結在子類上
  Parent.call(this, parent)
  this.name = 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.name + ` say`);
}

// 注意記得把子類的構造指向子類本身
Child.prototype.constructor = Child;
// 測試
var parent = new Parent('parent');
parent.say() 

var child = new Child('child');
child.say() 
child.play(); // 繼承父類的方法

ES5實現繼承-詳細

第一種方式是藉助call實現繼承

function Parent1(){
    this.name = 'parent1';
}
function Child1(){
    Parent1.call(this);
    this.type = 'child1'    
}
console.log(new Child1);
這樣寫的時候子類雖然能夠拿到父類的屬性值,但是問題是父類中一旦存在方法那麼子類無法繼承。那麼引出下面的方法

第二種方式藉助原型鏈實現繼承:

function Parent2() {
    this.name = 'parent2';
    this.play = [1, 2, 3]
  }
  function Child2() {
    this.type = 'child2';
  }
  Child2.prototype = new Parent2();

  console.log(new Child2());

看似沒有問題,父類的方法和屬性都能夠訪問,但實際上有一個潛在的不足。舉個例子:

var s1 = new Child2();
  var s2 = new Child2();
  s1.play.push(4);
  console.log(s1.play, s2.play); // [1,2,3,4] [1,2,3,4]

明明我只改變了s1的play屬性,為什麼s2也跟著變了呢?很簡單,因為兩個例項使用的是同一個原型物件

第三種方式:將前兩種組合:

function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
  }
  function Child3() {
    Parent3.call(this);
    this.type = 'child3';
  }
  Child3.prototype = new Parent3();
  var s3 = new Child3();
  var s4 = new Child3();
  s3.play.push(4);
  console.log(s3.play, s4.play); // [1,2,3,4] [1,2,3]
之前的問題都得以解決。但是這裡又徒增了一個新問題,那就是Parent3的建構函式會多執行了一次(Child3.prototype = new Parent3();)。這是我們不願看到的。那麼如何解決這個問題?

第四種方式: 組合繼承的最佳化1

function Parent4 () {
    this.name = 'parent4';
    this.play = [1, 2, 3];
  }
  function Child4() {
    Parent4.call(this);
    this.type = 'child4';
  }
  Child4.prototype = Parent4.prototype;
這裡讓將父類原型物件直接給到子類,父類建構函式只執行一次,而且父類屬性和方法均能訪問,但是我們來測試一下
var s3 = new Child4();
  var s4 = new Child4();
  console.log(s3)
子類例項的建構函式是Parent4,顯然這是不對的,應該是Child4。

第五種方式(最推薦使用):最佳化2

function Parent5 () {
    this.name = 'parent5';
    this.play = [1, 2, 3];
  }
  function Child5() {
    Parent5.call(this);
    this.type = 'child5';
  }
  Child5.prototype = Object.create(Parent5.prototype);
  Child5.prototype.constructor = Child5;
這是最推薦的一種方式,接近完美的繼承。

轉化為駝峰命名

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

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

原型繼承

這裡只寫寄生組合繼承了,中間還有幾個演變過來的繼承但都有一些缺陷

function Parent() {
  this.name = 'parent';
}
function Child() {
  Parent.call(this);
  this.type = 'children';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

實現一個迷你版的vue

入口

// js/vue.js
class Vue {
  constructor (options) {
    // 1. 透過屬性儲存選項的資料
    this.$options = options || {}
    this.$data = options.data || {}
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el
    // 2. 把data中的成員轉換成getter和setter,注入到vue例項中
    this._proxyData(this.$data)
    // 3. 呼叫observer物件,監聽資料的變化
    new Observer(this.$data)
    // 4. 呼叫compiler物件,解析指令和差值表示式
    new Compiler(this)
  }
  _proxyData (data) {
    // 遍歷data中的所有屬性
    Object.keys(data).forEach(key => {
      // 把data的屬性注入到vue例項中
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get () {
          return data[key]
        },
        set (newValue) {
          if (newValue === data[key]) {
            return
          }
          data[key] = newValue
        }
      })
    })
  }
}

實現Dep

class Dep {
  constructor () {
    // 儲存所有的觀察者
    this.subs = []
  }
  // 新增觀察者
  addSub (sub) {
    if (sub && sub.update) {
      this.subs.push(sub)
    }
  }
  // 傳送通知
  notify () {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}

實現watcher

class Watcher {
  constructor (vm, key, cb) {
    this.vm = vm
    // data中的屬性名稱
    this.key = key
    // 回撥函式負責更新檢視
    this.cb = cb

    // 把watcher物件記錄到Dep類的靜態屬性target
    Dep.target = this
    // 觸發get方法,在get方法中會呼叫addSub
    this.oldValue = vm[key]
    Dep.target = null
  }
  // 當資料發生變化的時候更新檢視
  update () {
    let newValue = this.vm[this.key]
    if (this.oldValue === newValue) {
      return
    }
    this.cb(newValue)
  }
}

實現compiler

class Compiler {
  constructor (vm) {
    this.el = vm.$el
    this.vm = vm
    this.compile(this.el)
  }
  // 編譯模板,處理文位元組點和元素節點
  compile (el) {
    let childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      // 處理文位元組點
      if (this.isTextNode(node)) {
        this.compileText(node)
      } else if (this.isElementNode(node)) {
        // 處理元素節點
        this.compileElement(node)
      }

      // 判斷node節點,是否有子節點,如果有子節點,要遞迴呼叫compile
      if (node.childNodes && node.childNodes.length) {
        this.compile(node)
      }
    })
  }
  // 編譯元素節點,處理指令
  compileElement (node) {
    // console.log(node.attributes)
    // 遍歷所有的屬性節點
    Array.from(node.attributes).forEach(attr => {
      // 判斷是否是指令
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        // v-text --> text
        attrName = attrName.substr(2)
        let key = attr.value
        this.update(node, key, attrName)
      }
    })
  }

  update (node, key, attrName) {
    let updateFn = this[attrName + 'Updater']
    updateFn && updateFn.call(this, node, this.vm[key], key)
  }

  // 處理 v-text 指令
  textUpdater (node, value, key) {
    node.textContent = value
    new Watcher(this.vm, key, (newValue) => {
      node.textContent = newValue
    })
  }
  // v-model
  modelUpdater (node, value, key) {
    node.value = value
    new Watcher(this.vm, key, (newValue) => {
      node.value = newValue
    })
    // 雙向繫結
    node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }

  // 編譯文位元組點,處理差值表示式
  compileText (node) {
    // console.dir(node)
    // {{  msg }}
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if (reg.test(value)) {
      let key = RegExp.$1.trim()
      node.textContent = value.replace(reg, this.vm[key])

      // 建立watcher物件,當資料改變更新檢視
      new Watcher(this.vm, key, (newValue) => {
        node.textContent = newValue
      })
    }
  }
  // 判斷元素屬性是否是指令
  isDirective (attrName) {
    return attrName.startsWith('v-')
  }
  // 判斷節點是否是文位元組點
  isTextNode (node) {
    return node.nodeType === 3
  }
  // 判斷節點是否是元素節點
  isElementNode (node) {
    return node.nodeType === 1
  }
}

實現Observer

class Observer {
  constructor (data) {
    this.walk(data)
  }
  walk (data) {
    // 1. 判斷data是否是物件
    if (!data || typeof data !== 'object') {
      return
    }
    // 2. 遍歷data物件的所有屬性
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }
  defineReactive (obj, key, val) {
    let that = this
    // 負責收集依賴,併傳送通知
    let dep = new Dep()
    // 如果val是物件,把val內部的屬性轉換成響應式資料
    this.walk(val)
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get () {
        // 收集依賴
        Dep.target && dep.addSub(Dep.target)
        return val
      },
      set (newValue) {
        if (newValue === val) {
          return
        }
        val = newValue
        that.walk(newValue)
        // 傳送通知
        dep.notify()
      }
    })
  }
}

使用

<!DOCTYPE html>
<html lang="cn">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Mini Vue</title>
</head>
<body>
  <div id="app">
    <h1>差值表示式</h1>
    <h3>{{ msg }}</h3>
    <h3>{{ count }}</h3>
    <h1>v-text</h1>
    <div v-text="msg"></div>
    <h1>v-model</h1>
    <input type="text" v-model="msg">
    <input type="text" v-model="count">
  </div>
  <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script>
  <script src="./js/vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg: 'Hello Vue',
        count: 100,
        person: { name: 'zs' }
      }
    })
    console.log(vm.msg)
    // vm.msg = { test: 'Hello' }
    vm.test = 'abc'
  </script>
</body>
</html>

相關文章