天天看點

26道JavaScript燒腦面試題彙總與解析

26道JavaScript燒腦面試題彙總與解析

今天為大家精選了26道稍微有點燒腦的JavaScript題,主要考察的是類型判斷、作用域、this指向、原型、事件循環等知識點,每道題都配有筆者詳細傻瓜式的解析,偏向于初學者,大佬請随意。

第1題

let a = 1
function b(a) {
  a = 2
  console.log(a)
}
b(a)
console.log(a)      

答案:

2、1

解析:

首先基本類型資料是按值傳遞的,是以執行b函數時,b的參數a接收的值為1,參數a相當于函數内部的變量,當本作用域有和上層作用域同名的變量時,無法通路到上層變量,是以函數内無論怎麼修改a,都不影響上層,是以函數内部列印的a是2,外面列印的仍是1。

第2題

function a (b = c, c = 1) {
  console.log(b, c)
}
a()      

答案:

報錯

解析:

給函數多個參數設定預設值實際上跟按順序定義變量一樣,是以會存在暫時性死區的問題,即前面定義的變量不能引用後面還未定義的變量,而後面的可以通路前面的。

第3題

let a = b = 10
;(function(){ 
  let a = b = 20 
})()
console.log(a)
console.log(b)      

答案:

10、20

解析:

連等操作是從右向左執行的,相當于b = 10、let a = b,很明顯b沒有聲明就直接指派了,是以會隐式建立為一個全局變量,函數内的也是一樣,并沒有聲明b,直接就對b指派了,因為作用域鍊,會一層一層向上查找,找了到全局的b,是以全局的b就被修改為20了,而函數内的a因為重新聲明了,是以隻是局部變量,不影響全局的a,是以a還是10。

第4題

var a = {n:1}
var b = a
a.x = a = {n:2}
console.log(a.x)
console.log(b.x)      

答案:

undefined、{n: 2}

解析:恕筆者不才,這道題筆者做一次錯一次。

反正按照網上大部分的解釋是因為.運算符優先級最高,是以會先執行a.x,此時a、b共同指向的{n: 1}變成了{n: 1, x: undefined},然後按照連等操作從右到左執行代碼,a = {n: 2},顯然,a現在指向了一個新對象,然後a.x = a,因為a.x最開始就執行過了,是以這裡其實等價于:({n: 1, x: undefined}).x = b.x = a = {n: 2}。

第5題

var arr = [0, 1, 2]
arr[10] = 10
console.log(arr.filter(function (x) {
  return x === undefined
}))      

答案:

[]

解析:

這題比較簡單,arr[10]=10,那麼索引3到9位置上都是undefined,arr[3]等列印出來也确實是undefined,但是,這裡其實涉及到ECMAScript版本不同對應方法行為不同的問題,ES6之前的周遊方法都會跳過數組未指派過的位置,也就是空位,但是ES6新增的for of方法就不會跳過。

第6題

var name = 'World'
;(function () {
  if (typeof name === 'undefined') {
    var name = "Jack"
    console.info('Goodbye ' + name)
  } else {
    console.info('Hello ' + name)
  }
})()      

答案:

Goodbye Jack

解析:

這道題考察的是變量提升的問題,var聲明變量時會把變量自動提升到目前作用域頂部,是以函數内的name雖然是在if分支裡聲明的,但是也會提升到外層,因為和全局的變量name重名,是以通路不到外層的name,最後因為已聲明未指派的變量的值都為undefined,導緻if的第一個分支滿足條件。

第7題

console.log(1 + NaN)
console.log("1" + 3)
console.log(1 + undefined)
console.log(1 + null)
console.log(1 + {})
console.log(1 + [])
console.log([] + {})      

答案:

NaN、13、NaN、1、1[object Object]、1、[object Object]

解析:

這道題考察的顯然是+号的行為:

1.如果有一個操作數是字元串,那麼把另一個操作數轉成字元串執行連接配接

2.如果有一個操作數是對象,那麼調用對象的valueOf方法轉成原始值,如果沒有該方法或調用後仍是非原始值,則調用toString方法

3.其他情況下,兩個操作數都會被轉成數字執行加法操作

第8題

var a={},
    b={key:'b'},
    c={key:'c'}
a[b]=123
a[c]=456
console.log(a[b])      

答案:

456

解析:

對象有兩種方法設定和引用屬性,obj.name和obj['name'],方括号裡可以字元串、數字和變量設定是表達式等,但是最終計算出來得是一個字元串,對于上面的b和c,它們兩個都是對象,是以會調用toString()方法轉成字元串,對象轉成字元串和數組不一樣,和内容無關,結果都是[object Obejct],是以a[b]=a[c]=a['[object Object]']。

第9題

var out = 25
var inner = {
  out: 20,
  func: function () {
    var out = 30
    return this.out
  }
};
console.log((inner.func, inner.func)())
console.log(inner.func())
console.log((inner.func)())
console.log((inner.func = inner.func)())      

答案:

25、20、20、25

解析:

這道題考察的是this指向問題:

1.逗号操作符會傳回表達式中的最後一個值,這裡為inner.func對應的函數,注意是函數本身,然後執行該函數,該函數并不是通過對象的方法調用,而是在全局環境下調用,是以this指向window,列印出來的當然是window下的out

2.這個顯然是以對象的方法調用,那麼this指向該對象

3.加了個括号,看起來有點迷惑人,但實際上(inner.func)和inner.func是完全相等的,是以還是作為對象的方法調用

4.指派表達式和逗号表達式相似,都是傳回的值本身,是以也相對于在全局環境下調用函數

第10題

let {a,b,c} = { c:3, b:2, a:1 }
console.log(a, b, c)      

答案:

1、2、3

解析:

這題考察的是變量解構指派的問題,數組解構指派是按位置對應的,而對象隻要變量與屬性同名,順序随意。

第11題

console.log(Object.assign([1, 2, 3], [4, 5]))      

答案:

[4, 5, 3]

解析:

是不是從來沒有用assign方法合并過數組?assign方法可以用于處理數組,不過會把數組視為對象,比如這裡會把目标數組視為是屬性為0、1、2的對象,是以源數組的0、1屬性的值覆寫了目标對象的值。

第12題

var x=1
switch(x++)
{
  case 0: ++x
  case 1: ++x
  case 2: ++x
}
console.log(x)      

答案:

4

解析:

這題考查的是自增運算符的字首版和字尾版,以及switch的文法,字尾版的自增運算符會在語句被求值後才發生。

是以x會仍以1的值去比對case分支,那麼顯然比對到為1的分支,此時,x++生效,x變成2,再執行++x,變成3,因為沒有break語句。

是以會進入目前case後面的分支,是以再次++x,最終變成4。

第13題

console.log(typeof undefined == typeof NULL)
console.log(typeof function () {} == typeof class {})      

答案:

true、true

解析:

1.首先不要把NULL看成是null,js的關鍵字是區分大小寫的,是以這就是一個普通的變量,而且沒有聲明,typeof對沒有聲明的變量使用是不會報錯的,傳回'undefined',typeof對undefined使用也是'undefined',是以兩者相等

第14題

var count = 0
console.log(typeof count === "number")
console.log(!!typeof count === "number")      

答案:

true、false

解析:

1.沒啥好說的,typeof對數字類型傳回'number'。

2.這題考查的是運算符優先級的問題,邏輯非!的優先級比全等===高,是以先執行!!typeof count,結果為true,然後執行true === 'number',結果當然為false

第15題

"use strict"
a = 1
var a = 2
console.log(window.a)
console.log(a)      

答案:

2、2

解析:

var聲明會把變量提升到目前作用域頂部,是以a=1并不會報錯,另外在全局作用域下使用var聲明變量,該變量會變成window的一個屬性,以上兩點都和是否在嚴格模式下無關。

第16題

var i = 1
function b() {
  console.log(i)
}
function a() {
  var i = 2
  b()
}
a()      

答案:

1

解析:

這道題考察的是作用域的問題,作用域其實就是一套變量的查找規則,每個函數在執行時都會建立一個執行上下文,其中會關聯一個變量對象,也就是它的作用域,上面儲存着該函數能通路的所有變量,另外上下文中的代碼在執行時還會建立一個作用域鍊,如果某個辨別符在目前作用域中沒有找到,會沿着外層作用域繼續查找,直到最頂端的全局作用域,因為js是詞法作用域,在寫代碼階段就作用域就已經确定了,換句話說,是在函數定義的時候确定的,而不是執行的時候,是以a函數是在全局作用域中定義的,雖然在b函數内調用,但是它隻能通路到全局的作用域而不能通路到b函數的作用域。

第17題

var obj = {
  name: 'abc',
  fn: () => {
    console.log(this.name)
  }
};
obj.name = 'bcd'
obj.fn()      

答案:

undefined

解析:

這道題考察的是this的指向問題,箭頭函數執行的時候上下文是不會綁定this的,是以它裡面的this取決于外層的this,這裡函數執行的時候外層是全局作用域,是以this指向window,window對象下沒有name屬性,是以是undefined。

第18題

const obj = {
  a: {
    a: 1
  }
};
const obj1 = {
  a: {
    b: 1
  }
};
console.log(Object.assign(obj, obj1))      

答案:

{a: {b: 1}}

解析:

這道題很簡單,因為assign方法執行的是淺拷貝,是以源對象的a屬性會直接覆寫目标對象的a屬性。

第19題

console.log(a)
var a = 1
var getNum = function() {
  a = 2
}
function getNum() {
  a = 3
}
console.log(a)
getNum()
console.log(a)      

答案:

undefined、1、2

解析:

首先因為var聲明的變量提升作用,是以a變量被提升到頂部,未指派,是以第一個列印出來的是undefined。

接下來是函數聲明和函數表達式的差別,函數聲明會有提升作用,在代碼執行前就把函數提升到頂部,在執行上下文上中生成函數定義,是以第二個getNum會被最先提升到頂部。

然後是var聲明getNum的提升,但是因為getNum函數已經被聲明了,是以就不需要再聲明一個同名變量,接下來開始執行代碼,執行到var getNum = fun...時,雖然聲明被提前了,但是指派操作還是留在這裡。

是以getNum被指派為了一個函數,下面的函數聲明直接跳過,最後,getNum函數執行前a列印出來還是1,執行後,a被修改成了2,是以最後列印出來的2。

第20題

var scope = 'global scope'
function a(){
  function b(){ 
    console.log(scope)
  }
  return b
  var scope = 'local scope'
}
a()()      

答案:

undefined

解析:

這題考查的還是變量提升和作用域的問題,雖然var聲明是在return語句後面,但還是會提升到a函數作用域的頂部。

然後又因為作用域是在函數定義的時候确定的,與調用位置無關,是以b的上層作用域是a函數,scope在b自身的作用域裡沒有找到,向上查找找到了自動提升的并且未指派的scope變量,是以列印出undefined。

第21題

function fn (){ 
  console.log(this) 
}
var arr = [fn]
arr[0]()      

答案:

列印出arr數組本身

解析:

函數作為某個對象的方法調用,this指向該對象,數組顯然也是對象,隻不過我們都習慣了對象引用屬性的方法:obj.fn,但是實際上obj['fn']引用也是可以的。

第22題

var a = 1
function a(){}
console.log(a)


var b
function b(){}
console.log(b)


function b(){}
var b
console.log(b)      

答案:

1、b函數本身、b函數本身

解析:

這三小題都涉及到函數聲明和var聲明,這兩者都會發生提升,但是函數會優先提升,是以如果變量和函數同名的話,變量的提升就忽略了。

1.提升完後,執行到指派代碼,a被指派成了1,函數因為已經聲明提升了,是以跳過,最後列印a就是1。

2.和第一題類似,隻是b沒有指派操作,那麼執行到這兩行相當于都沒有操作,b當然是函數。

3.和第二題類似,隻是先後順序換了一下,但是并不影響兩者的提升順序,仍是函數優先,同名的var聲明提升忽略,是以列印出b還是函數。

第23題

function Foo() {
  getName = function () { console.log(1) }
  return this
}
Foo.getName = function () { console.log(2) }
Foo.prototype.getName = function () { console.log(3) }
var getName = function () { console.log(4) }
function getName() { console.log(5) }


//請寫出以下輸出結果:
Foo.getName()
getName()
Foo().getName()
getName()
new Foo.getName()
new Foo().getName()
new new Foo().getName()      

答案:

2、4、1、1、2、3、3

解析:

這是一道綜合性題目,首先getName函數聲明會先提升,然後getName函數表達式提升,但是因為函數聲明提升線上,是以忽略函數表達式的提升,然後開始執行代碼,執行到var getName= ...時,修改了getName的值,指派成了列印4的新函數。

1.執行Foo函數的靜态方法,列印出2。

2.執行getName,目前getName是列印出4的那個函數。

3.執行Foo函數,修改了全局變量getName,指派成了列印1的函數,然後傳回this,因為是在全局環境下執行,是以this指向window,因為getName已經被修改了,是以列印出1。

4.因為getName沒有被重新指派,是以再執行仍然列印出1。

5.new操作符是用來調用函數的,是以new Foo.getName()相當于new (Foo.getName)(),是以new的是Foo的靜态方法getName,列印出2。

6.因為點運算符(.)的優先級和new是一樣高的,是以從左往右執行,相當于(new Foo()).getName(),對Foo使用new調用會傳回一個新建立的對象,然後執行該對象的getName方法,該對象本身并沒有該方法,是以會從Foo的原型對象上查找,找到了,是以列印出3。

7.和上題一樣,點運算符(.)的優先級和new一樣高,另外new是用來調用函數的,是以new new Foo().getName()相當于new ((new Foo()).getName)(),括号裡面的就是上一題。是以最後找到的是Foo原型上的方法,無論是直接調用,還是通過new調用,都會執行該方法,是以列印出3。

第24題

const person = {
  address: {
    country:"china",
    city:"hangzhou"
  },
  say: function () {
    console.log(`it's ${this.name}, from ${this.address.country}`)
  },
  setCountry:function (country) {
    this.address.country=country
  }
}


const p1 = Object.create(person)
const p2 = Object.create(person)


p1.name = "Matthew"
p1.setCountry("American")


p2.name = "Bob"
p2.setCountry("England")


p1.say()
p2.say()      

答案:

it's Matthew, from England

it's Bob, from England

解析:

Object.create方法會建立一個對象,并且将該對象的__proto__屬性指向傳入的對象,是以p1和p2兩個對象的原型對象指向了同一個對象,接着給p1添加了一個name屬性,然後調用了p1的setCountry方法,p1本身是沒有這個方法的。

是以會沿着原型鍊進行查找,在它的原型上,也就是person對象上找到了這個方法,執行這個方法會給address對象的country屬性設定傳入的值,p1本身也是沒有address屬性的,但是和name屬性不一樣,address屬性在原型對象上找到了,并且因為是個引用值。

是以會成功修改它的country屬性,接着對p2的操作也是一樣,然後因為原型中存在引用值會在所有執行個體中共享。

是以p1和p2它們引用的address也是同一個對象,一個執行個體修改了,會反映到所有執行個體上,是以p2的修改會覆寫p1的修改,最終country的值為England。

第25題

setTimeout(function() {
  console.log(1)
}, 0)
new Promise(function(resolve) {
  console.log(2)
  for( var i=0 ; i<10000 ; i++ ) {
    i == 9999 && resolve()
  }
  console.log(3)
}).then(function() {
  console.log(4)
})
console.log(5)      

答案:

2、3、5、4、1

解析:

這道題顯然考察的是事件循環的知識點。

js是一門單線程的語言,但是為了執行一些異步任務時不阻塞代碼,以及避免等待期間的資源浪費,js存在事件循環的機制,單線程指的是執行js的線程,稱作主線程。

其他還有一些比如網絡請求的線程、定時器的線程,主線程在運作時會産生執行棧,棧中的代碼如果調用了異步api的話則會把事件添加到事件隊列裡,隻要該異步任務有了結果便會把對應的回調放到【任務隊列】裡,當執行棧中的代碼執行完畢後會去讀取任務隊列裡的任務,放到主線程執行,當執行棧空了又會去檢查,如此往複,也就是所謂的事件循環。

異步任務又分為【宏任務】(比如setTimeout、setInterval)和【微任務】(比如promise),它們分别會進入不同的隊列,執行棧為空完後會優先檢查微任務隊列,如果有微任務的話會一次性執行完所有的微任務,然後去宏任務隊列裡檢查,如果有則取出一個任務到主線程執行,執行完後又會去檢查微任務隊列,如此循環。

回到這題,首先整體代碼作為一個宏任務開始執行,遇到setTimeout,相應回調會進入宏任務隊列,然後是promise,promise的回調是同步代碼,是以會列印出2,for循環結束後調用了resolve。

是以then的回調會被放入微任務隊列,然後列印出3,最後列印出5,到這裡目前的執行棧就空了,那麼先檢查微任務隊列,發現有一個任務,那麼取出來放到主線程執行,列印出4,最後檢查宏任務隊列,把定時器的回調放入主線程執行,列印出1。

第26題

console.log('1');


setTimeout(function() {
  console.log('2');
  process.nextTick(function() {
    console.log('3');
  });
  new Promise(function(resolve) {
    console.log('4');
    resolve();
  }).then(function() {
    console.log('5');
  });
}); 


process.nextTick(function() {
  console.log('6');
});


new Promise(function(resolve) {
  console.log('7');
  resolve();
}).then(function() {
  console.log('8');
});


setTimeout(function() {
  console.log('9');
  process.nextTick(function() {
    console.log('10');
  }) 
  new Promise(function(resolve) {
    console.log('11');
    resolve();
  }).then(function() {
    console.log('12')
  });
})      

答案:

1、7、6、8、2、4、9、11、3、10、5、12

解析:

這道題和上一題差不多,但是出現了process.nextTick,是以顯然是在node環境下,node也存在事件循環的概念,但是和浏覽器的有點不一樣,nodejs中的宏任務被分成了幾種不同的階段,兩個定時器屬于timers階段,setImmediate屬于check階段,socket的關閉事件屬于close callbacks階段,其他所有的宏任務都屬于poll階段。

除此之外,隻要執行到前面說的某個階段,那麼會執行完該階段所有的任務,這一點和浏覽器不一樣,浏覽器是每次取一個宏任務出來執行,執行完後就跑去檢查微任務隊列了。

但是nodejs是來都來了,一次全部執行完該階段的任務好了,那麼process.nextTick和微任務在什麼階段執行呢,在前面說的每個階段的後面都會執行,但是process.nextTick會優先于微任務。

一圖勝千言:

26道JavaScript燒腦面試題彙總與解析

了解了以後再來分析這道題就很簡單了,首先執行整體代碼,先列印出1,setTimeout回調扔進timers隊列,nextTick的扔進nextTick的隊列,promise的回調是同步代碼,執行後列印出7,then回調扔進微任務隊列。

然後又是一個setTimeout回調扔進timers隊列,到這裡目前節點就結束了,檢查nextTick和微任務隊列,nextTick隊列有任務,執行後列印出6,微任務隊列也有,列印出8。

接下來按順序檢查各個階段,check隊列、close callbacks隊列都沒有任務,到了timers階段,發現有兩個任務,先執行第一個,列印出2。

然後nextTick的扔進nextTick的隊列,執行promise列印出4,then回調扔進微任務隊列,再執行第二個setTimeout的回調,列印出9。

然後和剛才一樣,nextTick的扔進nextTick的隊列,執行promise列印出11,then回調扔進微任務隊列,到這裡timers階段也結束了,執行nextTick隊列的任務,發現又兩個任務,依次執行,列印出3和10。

然後檢查微任務隊列,也是兩個任務,依次執行,列印出5和12,到這裡是有隊列都清空了。

學習更多技能

請點選下方公衆号