當裸辭遇到面試難,這些面試題你需要了解一下

子君發表於2020-08-31
乘興裸辭心甚爽,面試工作屢遭難。
幸得每日一題伴,點選關注莫偷懶。

又要到金九銀十的跳槽季了,為了讓更多的小夥伴可以在面試的時候取的更好的offer,所以自上月起我每天都會在自己的公眾號【前端有的玩】裡面推送一到兩道面試題,方便找工作的小夥伴每日都會有新的收穫。本文就是小編將前期的一些比較經典的每日一題進行了梳理,歡迎大家一起來看看。本文內容首發於公眾號【前端有的玩】,關注 === 學會。

類陣列面試題

什麼是類陣列,類陣列就是 擁有length屬性,且其他屬性(索引)為非負整數的物件,且不具備陣列所用於的方法。比如我們常用的document.querySelector返回的NodeLists就是一個類陣列。這道題就是和類陣列相關的內容.

題目

請說出以下程式碼輸出的內容,需要區分nodejs,chrome以及chrome去掉splice之後的輸出內容

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

答案

這道題一共問了三種情況下面的輸出,下面依次說明答案

  1. node下面輸出

    { '2': 1,
      '3': 2,
      length: 4,
      splice: [Function: splice],
      push: [Function: push] }
  2. chrome下面輸出

    [empty × 2, 1, 2, splice: ƒ, push: ƒ]
  3. chrome去掉splice下面輸出

    {2: 1, 3: 2, length: 4, push: ƒ}

通過上面輸出的內容,可以看出相同的程式碼,不同情況輸出的內容是有所不同的,下面進行詳細分解。

題解

在解答題目之前,我們再看看這段程式碼

const arr = new Array(2)
// 輸出  2 [empty * 2]
console.log(arr.length, arr)
arr.push(1)
// 輸出  3 [empty * 2, 1]
console.log(arr.length, arr)

可以看到push方法會將陣列的length + 1, 然後將值放在索引為length - 1的位置,比如上面的程式碼,因為在初始化陣列的時候,已經將陣列長度指定為了2, 所以在push之後length就變成了3,然後arr[3 - 1] = 1

MDN上面對push的方法的解釋是:

push 方法具有通用性。該方法和 call()apply() 一起使用時,可應用在類似陣列的物件上。push 方法根據 length 屬性來決定從哪裡開始插入給定的值。如果 length 不能被轉成一個數值,則插入的元素索引為 0,包括 length 不存在時。當 length 不存在時,將會建立它。

根據MDN解釋,push既可以使用到陣列中,也可以使用到類陣列中。而根據前文中對類陣列的解釋,可以看到題目中的obj就是一個標準的類陣列,那就可以在obj上面使用陣列的push方法。

再看obj.push(1), 因為obj.length = 2, 所以會將length + 1就變成了3, 這時候 索引值時obj[3 - 1] = 1obj[2] = 1, 同理 obj.push(2) 也一樣的。因為在obj中已經有了屬性(索引)23,所以在push的時候會覆蓋掉23上面的預設值。

所以在nodejs中就會輸出

{ '2': 1,
  '3': 2,
  length: 4,
  splice: [Function: splice],
  push: [Function: push] }

但是在chrome控制檯中輸出

[empty × 2, 1, 2, splice: ƒ, push: ƒ]

很奇怪,為什麼會輸出這樣呢?這一塊有一個很特殊的陷阱,就是chrome控制檯是如何判斷列印的內容是陣列還是其他物件呢?對於這個,chrome就是通過判斷物件上面是否有splicelength這兩個屬性來判斷的,所以如果你將splice去掉之後,就會輸出以下內容

{2: 1, 3: 2, length: 4, push: ƒ}

你也可以試試下面的程式碼:

console.log({splice:function(){},length:1})
console.log({slice:function(){},length:1})

邏輯面試題之小鼠喝毒藥

小編當年畢業的時候面試就遇到過好幾次邏輯類的面試題,這道題就是一道邏輯類的面試題,一起來看看。

題目

有16瓶水,其中只有一瓶水有毒,小白鼠喝一滴之後一小時會死,請問最少用多少隻小白鼠,在1小時內一定可以找出有毒的水?

答案與題解

答案是至少需要4只小鼠,怎麼理解呢?我們可以用二進位制去推理一下:

假設有4只小鼠,分別是甲乙丙丁,使用二進位制來表示小鼠喝藥的順序,1代表喝藥,0代表不喝藥

甲: 1111 1111 0000 0000

乙: 1111 0000 1111 0000

丙: 1100 1100 1100 1100

丁: 1010 1010 1010 1010

那麼我們就可以這樣去判斷:

  1. 甲乙丙丁都死了,說明第一瓶有毒
  2. 甲乙丙死了,說明第二瓶有毒
  3. 甲乙丁死了,說明第三瓶有毒
  4. 甲乙死了,說明第四瓶有毒
  5. 甲丙丁死了,說明第五瓶有毒
  6. 。。。 依次類推

其實對於這道題,可以使用2n次方來判斷,比如有32瓶水,那麼就是25次方,所以就需要5只小鼠。

arguments 面試題

ES6中,我們如果一個函式引數個數不確定,我們一般會使用擴充套件運算子即function(...rest){},得到一個引數陣列rest,但是在ES6之前,我們是不能使用擴充套件運算子的,這時候就需要考慮使用arguments

題目

請說出以下程式輸出的內容(chrome輸出內容)

let obj = {
  arg: 18,
  foo: function(func) {
    func()
    arguments[0]()
  }
}

var age = 10
function fn() {
  console.log(this.age)
}

obj.foo(fn)

答案

本題的答案是:

// 第一個輸出 10
func()
// 第一個輸出 undefined
arguments[0]()

有點出乎意料了嗎?

先來解釋一下第一個,為什麼不是輸出18呢,雖然func()是在foo函式裡面呼叫的,但是並沒有顯式指明作用域,這時候會使用預設作用域window,而對於瀏覽器來說,在全域性通過var宣告的變數會自動掛載到window上面,所以var age = 10相當於window.age = 10, 而第一個func()裡面的this.age相當於window.age

第二個可能許多人有點蒙,為啥是undefined,先看一下下面的程式碼

const arr = [function() {console.log(this[1])}, '我是子君']
// 輸出 我是子君
console.log(arr[0]())

我們通過arr[0]獲取到函式,這時候函式的作用域就是這個陣列,所以再呼叫的時候,this就是arr, 所以this[1]就是陣列第二項。

這時候回過頭來看arguments,這個其實是一個類陣列,裡面存的是函式傳入的引數,第一項就是傳入的函式,和上面例子一樣,arguments[0]的作用域就是arguments,而arguments上面並沒有age屬性,所以是undefined

this指向問題

this指向問題一直是比較混亂的,在箭頭函式出現之前,this的指向與程式碼在哪裡定義並沒有關係,而是取決於是被誰執行的,正因為此,所以許多開發人員經常會搞不清楚this到底是誰。下面的兩道題都是和this指向相關的問題。

題目一(青銅)

請說出以下程式碼輸出的內容

let num = 1;
let obj = {
    num: 2,
    add: function() {
        this.num = 3;
        (function() {
            console.log(this.num);
            this.num = 4;
        })();
        console.log(this.num);
    },
    sub: function() {
        console.log(this.num)
    }
}
obj.add();
console.log(obj.num);
console.log(num);
const sub = obj.sub;
sub();

題目二(黃金)

請說出以下程式碼輸出的內容

var num = 10
const obj = {num: 20}
obj.fn = (function (num) {
  this.num = num * 3
  num++
  return function (n) {
    this.num += n
    num++
    console.log(num)
  }
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)

答案

題目一

輸出結果: 1,3,3,4,4, 你答對了嗎?下面我們來看看程式碼解析

var num = 1;
let obj = {
    num: 2,
    add: function() {
        this.num = 3;
          // 這裡的立即指向函式,因為我們沒有手動去指定它的this指向,所以都會指向window
        (function() {
            // 所有這個 this.num 就等於 window.num
            console.log(this.num);
            this.num = 4;
        })();
        console.log(this.num);
    },
    sub: function() {
        console.log(this.num)
    }
}
// 下面逐行說明列印的內容

/**
 * 在通過obj.add 呼叫add 函式時,函式的this指向的是obj,這時候第一個this.num=3
 * 相當於 obj.num = 3 但是裡面的立即指向函式this依然是window,
 * 所以 立即執行函式裡面console.log(this.num)輸出1,同時 window.num = 4
 *立即執行函式之後,再輸出`this.num`,這時候`this`是`obj`,所以輸出3
 */
obj.add() // 輸出 1 3

// 通過上面`obj.add`的執行,obj.name 已經變成了3
console.log(obj.num) // 輸出3
// 這個num是 window.num
console.log(num) // 輸出4
// 如果將obj.sub 賦值給一個新的變數,那麼這個函式的作用域將會變成新變數的作用域
const sub = obj.sub
// 作用域變成了window window.num 是 4
sub() // 輸出4
題目二

輸出結果為: 22 23 65 30, 你答對了嗎? 下面我們解析一下

var num = 10
const obj = {num: 20}
obj.fn = (function (num) {
  this.num = num * 3
  num++
  return function (n) {
    this.num += n
    num++
    console.log(num)
  }
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)

我們把上面的程式碼分為以下幾步進行分析

  1. 先看第三行程式碼,是一個賦值操作,我們知道賦值操作是從右向左的,而=號右邊是一個立即執行函式,所以會優先執行立即執行函式,立即執行函式沒有手動指定this,這時候this = window,而立即函式的引數num是傳進來的obj.num,所以num引數預設值是 20
  2. 第四行相當於window.num = 20 * 3
  3. 第五行為傳入的引數加一,所以 num = 20 + 1
  4. 第六行return了一個函式,而這個函式就是obj.fn的值, 但是因為return的函式引用了立即執行函式裡面的num,所以形成了閉包。這時候

    obj.fn = function(n) {
      this.num += n
      // 這個num是立即執行函式裡面的num
      num++
      console.log(num)
    }
  5. var fn = obj.fn, 將obj.fn賦值給新的變數,而這個變數的作用域是window
  6. 在呼叫fn(5)的時候, 在第二步,window.num的值已經變成了60, 然後因為這時候fnthiswindow, this.num += n相當於window.num += n, 即window.num = 65
  7. num++, 因為閉包的原因,第三步num21,所以這一步 num變成了22, 同時輸出22
  8. 然後obj.fn(10),這時候fnthisobj,obj.num預設值是20, this.num += n相當於 obj.num += 10
  9. 和第七步一樣, num + 1 輸出 23
  10. console.log(num, obj.num)相當於 console.log(window.num, obj.num),從上面幾步可知, window.num = 65, obj.num = 30
擴充套件題

如果將上面兩道題的 var改成 let, 又會輸出什麼結果呢?

資料型別轉換問題

雖然在日常開發中,我們隱氏型別轉換用的比較少(不一定),但是這個還是面試常問問題,掌握還是要掌握的,一起來看看這道題目吧.

題目(王炸/青銅,我也不知道)

請說出以下程式碼輸出的內容

console.log([] + [])
console.log({} + [])
console.log([] == ![])
console.log(true + false)

答案

一起來看看答案吧

  1. 第一行程式碼
// 輸出 "" 空字串
console.log([] + [])

這行程式碼輸出的是空字串"", 包裝型別在運算的時候,會先呼叫valueOf方法,如果valueOf返回的還是包裝型別,那麼再呼叫toString方法

// 還是 陣列
const val = [].valueOf()
// 陣列 toString 預設會將陣列各項使用逗號 "," 隔開, 比如 [1,2,3].toSting 變成了"1,2,3",空陣列 toString 就是空字串
const val1 = val.toString() // val1 是空字串

所以上面的程式碼相當於

console.log("" + "")
  1. 第二行程式碼

    // 輸出 "[object Object]"
    console.log({} + [])

    和第一題道理一樣,物件 {}隱氏轉換成了[object Object],然後與""相加

  2. 第三行程式碼

    // 輸出 true
    console.log([] == ![])

    對於===, 會嚴格比較兩者的值,但是對於==就不一樣了

    1. 比如 null == undefined
    2. 如果非numbernumber比較,會將其轉換為number
    3. 如果比較的雙方中由一方是boolean,那麼會先將boolean轉換為number

所以對於上面的程式碼,看下面一步一步分析

// 這個輸出 false
console.log(![])
// 套用上面第三條 將 false 轉換為 數值
// 這個輸出 0
console.log(Number(false))
// 包裝型別與 基本型別 == 先將包裝型別通過 valueOf toString 轉換為基本型別 
// 輸出 ""
console.log([].toString())
// 套用第2條, 將空字串轉換為數值、
// 輸出 0
console.log(Number(""))
// 所以
console.log(0 == 0)
  1. 第四行程式碼

    // 輸出 1
    console.log(true + false)

    兩個基本型別相加,如果其中一方是字元,則將其他的轉換為字元相加,否則將型別轉換為Number,然後相加, Number(true)1, Number(false)0, 所以結果是 1

總結

面試造火箭,工作擰螺絲。雖然我只想擰螺絲,但是我卻需要通過造火箭來找到擰螺絲的工作,每日一題,每天都有新的面試題目,歡迎關注公眾號【前端有的玩】,拉你進入前端技術交流群,每日一題等著你來一起答題。

結語

不要吹滅你的靈感和你的想象力; 不要成為你的模型的奴隸。 ——文森特・梵高

相關文章