JS常見面試題,看看你都會多少?

1. 如何在ES5環境下實現let

這個問題實質上是在回答let和var有什麼區別,對於這個問題,我們可以直接查看babel轉換前後的結果,看一下在循環中通過let定義的變量是如何解決變量提升的問題

babel在let定義的變量前加了道下劃線,避免在塊級作用域外訪問到該變量,除了對變量名的轉換,我們也可以通過自執行函數來模擬塊級作用域

(function(){
  for(var i = 0; i < 5; i ++){
    console.log(i)  // 0 1 2 3 4
  }
})();

console.log(i)      // Uncaught ReferenceError: i is not defined

不過這個問題並沒有結束,我們回到varlet/const的區別上:

  • var聲明的變量會掛到window上,而let和const不會
  • var聲明的變量存在變量提升,而let和const不會
  • let和const聲明形成塊作用域,只能在塊作用域裏訪問,不能跨塊訪問,也不能跨函數訪問
  • 同一作用域下let和const不能聲明同名變量,而var可以
  • 暫時性死區,let和const聲明的變量不能在聲明前被使用

babel的轉化,其實只實現了第2、3、5點

2. 如何在ES5環境下實現const

實現const的關鍵在於Object.defineProperty()這個API,這個API用於在一個對象上增加或修改屬性。通過配置屬性描述符,可以精確地控制屬性行爲。Object.defineProperty() 接收三個參數:Object.defineProperty(obj, prop, desc)

參數 說明
obj 要在其上定義屬性的對象
prop 要定義或修改的屬性的名稱
descriptor 將被定義或修改的屬性描述符
屬性描述符 說明 默認值
value 該屬性對應的值。可以是任何有效的 JavaScript 值(數值,對象,函數等)。默認爲 undefined undefined
get 一個給屬性提供 getter 的方法,如果沒有 getter 則爲 undefined undefined
set 一個給屬性提供 setter 的方法,如果沒有 setter 則爲 undefined。當屬性值修改時,觸發執行該方法 undefined
writable 當且僅當該屬性的writable爲true時,value才能被賦值運算符改變。默認爲 false false
enumerable enumerable定義了對象的屬性是否可以在 for...in 循環和 Object.keys() 中被枚舉 false
Configurable configurable特性表示對象的屬性是否可以被刪除,以及除value和writable特性外的其他特性是否可以被修改 false

對於const不可修改的特性,我們通過設置writable屬性來實現

function _const(key, value) {    
    const desc = {        
        value,        
        writable: false    
    }    
    Object.defineProperty(window, key, desc)
}
    
_const('obj', {a: 1})   //定義obj
obj.b = 2               //可以正常給obj的屬性賦值
obj = {}                //無法賦值新對象
3. 手寫call()
call()` 方法使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數 語法:`function.call(thisArg, arg1, arg2, ...)

call()的原理比較簡單,由於函數的this指向它的直接調用者,我們變更調用者即完成this指向的變更:

//變更函數調用者示例
function foo() {
    console.log(this.name)
}

// 測試
const obj = {
    name: '前端腦洞'
}
obj.foo = foo   // 變更foo的調用者
obj.foo()       // '前端腦洞'

基於以上原理, 我們兩句代碼就能實現call()

Function.prototype.myCall = function(thisArg, ...args) {
    thisArg.fn = this              // this指向調用call的對象,即我們要改變this指向的函數
    return thisArg.fn(...args)     // 執行函數並return其執行結果
}

但是我們有一些細節需要處理:

Function.prototype.myCall = function(thisArg, ...args) {
    const fn = Symbol('fn')        // 聲明一個獨有的Symbol屬性, 防止fn覆蓋已有屬性
    thisArg = thisArg || window    // 若沒有傳入this, 默認綁定window對象
    thisArg[fn] = this              // this指向調用call的對象,即我們要改變this指向的函數
    const result = thisArg[fn](...args)  // 執行當前函數
    delete thisArg[fn]              // 刪除我們聲明的fn屬性
    return result                  // 返回函數執行結果
}

//測試
foo.myCall(obj)     // 輸出'前端腦洞'
4. 手寫apply()

apply() 方法調用一個具有給定this值的函數,以及作爲一個數組(或類似數組對象)提供的參數。 語法:func.apply(thisArg, [argsArray])

apply()call()類似,區別在於call()接收參數列表,而apply()接收一個參數數組,所以我們在call()的實現上簡單改一下入參形式即可

Function.prototype.myApply = function(thisArg, args) {
    const fn = Symbol('fn')        // 聲明一個獨有的Symbol屬性, 防止fn覆蓋已有屬性
    thisArg = thisArg || window    // 若沒有傳入this, 默認綁定window對象
    thisArg[fn] = this              // this指向調用call的對象,即我們要改變this指向的函數
    const result = thisArg[fn](...args)  // 執行當前函數(此處說明一下:雖然apply()接收的是一個數組,但在調用原函數時,依然要展開參數數組。可以對照原生apply(),原函數接收到展開的參數數組)
    delete thisArg[fn]              // 刪除我們聲明的fn屬性
    return result                  // 返回函數執行結果
}

//測試
foo.myApply(obj, [])     // 輸出'前端腦洞'
5. 手寫bind()
bind()` 方法創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其餘參數將作爲新函數的參數,供調用時使用。 語法: `function.bind(thisArg, arg1, arg2, ...)

從用法上看,似乎給call/apply包一層function就實現了bind():

Function.prototype.myBind = function(thisArg, ...args) {
    return () => {
        this.apply(thisArg, args)
    }
}

但我們忽略了三點:

  • bind()除了this還接收其他參數,bind()返回的函數也接收參數,這兩部分的參數都要傳給返回的函數
  • new會改變this指向:如果bind綁定後的函數被new了,那麼this指向會發生改變,指向當前函數的實例
  • 沒有保留原函數在原型鏈上的屬性和方法
Function.prototype.myBind = function (thisArg, ...args) {
    var self = this
    // new優先級
    var fbound = function () {
        self.apply(this instanceof self ? this : thisArg, args.concat(Array.prototype.slice.call(arguments)))
    }
    // 繼承原型上的屬性和方法
    fbound.prototype = Object.create(self.prototype);

    return fbound;
}

//測試
const obj = { name: '前端腦洞' }
function foo() {
    console.log(this.name)
    console.log(arguments)
}

foo.myBind(obj, 'a', 'b', 'c')()    //輸出前端腦洞 ['a', 'b', 'c']
6. 手寫一個防抖函數

防抖和節流的概念都比較簡單,所以我們就不在“防抖節流是什麼”這個問題上浪費過多篇幅了,簡單點一下: 防抖,即短時間內大量觸發同一事件,只會執行一次函數,實現原理爲設置一個定時器,約定在xx毫秒後再觸發事件處理,每次觸發事件都會重新設置計時器,直到xx毫秒內無第二次操作,防抖常用於搜索框/滾動條的監聽事件處理,如果不做防抖,每輸入一個字/滾動屏幕,都會觸發事件處理,造成性能浪費。

function debounce(func, wait) {
    let timeout = null
    return function() {
        let context = this
        let args = arguments
        if (timeout) clearTimeout(timeout)
        timeout = setTimeout(() => {
            func.apply(context, args)
        }, wait)
    }
}
7. 手寫一個節流函數

防抖是延遲執行,而節流是間隔執行,函數節流即每隔一段時間就執行一次,實現原理爲設置一個定時器,約定xx毫秒後執行事件,如果時間到了,那麼執行函數並重置定時器,和防抖的區別在於,防抖每次觸發事件都重置定時器,而節流在定時器到時間後再清空定時器

function throttle(func, wait) {
    let timeout = null
    return function() {
        let context = this
        let args = arguments
        if (!timeout) {
            timeout = setTimeout(() => {
                timeout = null
                func.apply(context, args)
            }, wait)
        }

    }
}

實現方式2:使用兩個時間戳prev舊時間戳和now新時間戳,每次觸發事件都判斷二者的時間差,如果到達規定時間,執行函數並重置舊時間戳

function throttle(func, wait) {
    var prev = 0;
    return function() {
        let now = Date.now();
        let context = this;
        let args = arguments;
        if (now - prev > wait) {
            func.apply(context, args);
            prev = now;
        }
    }
}
8. 數組扁平化

對於[1, [1,2], [1,2,3]]這樣多層嵌套的數組,我們如何將其扁平化爲[1, 1, 2, 1, 2, 3]這樣的一維數組呢:

1.ES6的flat()

const arr = [1, [1,2], [1,2,3]]
arr.flat(Infinity)  // [1, 1, 2, 1, 2, 3]

2.序列化後正則

const arr = [1, [1,2], [1,2,3]]
const str = `[${JSON.stringify(arr).replace(/(\[|\])/g, '')}]`
JSON.parse(str)   // [1, 1, 2, 1, 2, 3]

3.遞歸 對於樹狀結構的數據,最直接的處理方式就是遞歸

const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
  let result = []
  for (const item of arr) {
    item instanceof Array ? result = result.concat(flat(item)) : result.push(item)
  }
  return result
}

flat(arr) // [1, 1, 2, 1, 2, 3]

4.reduce()遞歸

const arr = [1, [1,2], [1,2,3]]
function flat(arr) {
  return arr.reduce((prev, cur) => {
    return prev.concat(cur instanceof Array ? flat(cur) : cur)
  }, [])
}

flat(arr)  // [1, 1, 2, 1, 2, 3]

5.迭代+展開運算符

// 每次while都會合並一層的元素,這裏第一次合併結果爲[1, 1, 2, 1, 2, 3, [4,4,4]]
// 然後arr.some判定數組中是否存在數組,因爲存在[4,4,4],繼續進入第二次循環進行合併
let arr = [1, [1,2], [1,2,3,[4,4,4]]]
while (arr.some(Array.isArray)) {
  arr = [].concat(...arr);
}

console.log(arr)  // [1, 1, 2, 1, 2, 3, 4, 4, 4]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章