8道經典JavaScript面試題解析,你真的掌握JavaScript了嗎?

JavaScript是前端開發中很是重要的一門語言,瀏覽器是他主要運行的地方。JavaScript是一個很是有意思的語言,可是他有不少一些概念,你們常常都會忽略。好比說,原型,閉包,原型鏈,事件循環等等這些概念,不少JS開發人員都研究很少。前端

因此今天,就來和你們看看下面幾個問題,你們能夠先思考一下,嘗試做答。面試

八道面試題

問題1:下面這段代碼,瀏覽器控制檯上會打印什麼?
15205數組

問題2:若是咱們使用 let 或 const 代替 var,輸出是否相同
15208瀏覽器

問題3:「newArray」中有哪些元素?
15213閉包

問題4:若是咱們在瀏覽器控制檯中運行'foo'函數,是否會致使堆棧溢出錯誤?併發

15217
問題5: 若是在控制檯中運行如下函數,頁面(選項卡) 是否會有響應
15221異步

問題6: 咱們可否以某種方式爲下面的語句使用展開運算而不致使類型錯誤
15224函數

問題7:運行如下代碼片斷時,控制檯上會打印什麼?
15228
問題8:xGetter() 會打印什麼值?
15232oop

答案

前面的問題咱們都舉例出來了,接下來咱們會從頭至尾,一個個來分析咱們這些問題的答案,給你們一些學習的思路學習

問題1:
使用var關鍵字聲明的變量在JavaScript中會被提高,並在內存中開闢空間,因爲沒有賦值,沒法定義數值類型,因此分配默認值undefined。var聲明的變量,真正的數值初始化,是發生在你肯定賦值的位置。同時,咱們要知道,var聲明的變量是函數做用域的,也就是咱們須要區分局部變量和全局變量,而let和const是塊做用域的。因此咱們這道題的運行過程是這樣的:

var a = 10; // 全局做用域,全局變量。a=10
function foo() {
// var a 
//的聲明將被提高到到函數的頂部。
// 好比:var a

console.log(a); // 打印 undefined

// 實際初始化值20只發生在這裏
   var a = 20; // local scope
}

圖解在下面,好理解一點
15257
因此問題1的答案是:undefined

問題 2:
let和const聲明可讓變量在其做用域上受限於它所在的塊、語句或表達式中。和var不一樣的地方在於,這兩個聲明的變量,不會被提高。而且咱們會有一個稱爲暫時死區(TDZ)。若是訪問TDZ中的變量的話,就會報ReferenceError,由於他們的的做用域是在他們聲明的位置的,不會有提高。因此必須在執行到聲明的位置才能訪問。

var a = 10; // 全局使用域
function foo() { // TDZ 開始

// 建立了未初始化的'a'
    console.log(a); // ReferenceError

// TDZ結束,'a'僅在此處初始化,值爲20
    let a = 20;
}

圖解:
15287
問題2答案:ReferenceError: a is not defined

問題3:

這個問題,是循環結構會給你們帶來一種塊級做用域的誤區,在for的循環的頭部使用var聲明的變量,就是單個聲明的變量綁定(單個存儲空間)。在循環過程當中,這個var聲明的i變量是會隨循環變化的。可是在循環中執行的數組push方法,最後其實是push了i最終循環結束的3這個值。因此最後push進去的全都是3。

// 誤解做用域:認爲存在塊級做用域
var array = [];
for (var i = 0; i < 3; i++) {
    // 三個箭頭函數體中的每一個'i'都指向相同的綁定,
    // 這就是爲何它們在循環結束時返回相同的值'3'。
    array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [3, 3, 3]

圖解:
15306
若是想記錄每一次循環的值下來,可使用let聲明一個具備塊級做用域的變量,這樣爲每一個循環迭代建立一個新的綁定。

// 使用ES6塊級做用域
var array = [];
for (let i = 0; i < 3; i++) {
    // 這一次,每一個'i'指的是一個新的的綁定,並保留當前的值。
    // 所以,每一個箭頭函數返回一個不一樣的值。
    array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]

還有解決這個問題的另一種解決方案就是使用閉包就行了。

let array = [];
for (var i = 0; i < 3; i++) {
    array[i] = (function(x) {
     return function() {
           return x;
          };
    })(i);
}
const newArray = array.map(el => el());
console.log(newArray); // [0, 1, 2]

問題3答案:3,3,3

問題4
JavaScript的併發模式基於咱們常說的」事件循環「。
瀏覽器是提供運行時環境來給咱們執行JS代碼的。瀏覽器的主要組成包括有調用堆棧,事件循環,任務隊列和WEB API。像什麼經常使用的定時器setTimeout,setInterval這些全局函數就不是JavaScript的一部分,而是WEB API給咱們提供的。
15335
JS調用棧是後進先出(LIFO)的。引擎每次從堆棧中取出一個函數,而後從上到下依次運行代碼。每當它遇到一些異步代碼,如setTimeout,它就把它交給Web API(箭頭1)。所以,每當事件被觸發時,callback 都會被髮送到任務隊列(箭頭2)。
事件循環(Event loop)不斷地監視任務隊列(Task Queue),並按它們排隊的順序一次處理一個回調。每當調用堆棧(call stack)爲空時,Event loop獲取回調並將其放入堆棧(stack )(箭頭3)中進行處理。請記住,若是調用堆棧不是空的,則事件循環不會將任何回調推入堆棧。

好了,如今有了前面這些知識,咱們能夠看一下這道題的講解過程:
實現步驟:

  1. 調用 foo()會將foo函數放入調用堆棧(call stack)。
  2. 在處理內部代碼時,JS引擎遇到setTimeout。
  3. 而後將foo回調函數傳遞給WebAPIs(箭頭1)並從函數返回,調用堆棧再次爲空
  4. 計時器被設置爲0,所以foo將被髮送到任務隊列(箭頭2)。
  5. 因爲調用堆棧是空的,事件循環將選擇foo回調並將其推入調用堆棧進行處理。
  6. 進程再次重複,堆棧不會溢出。

問題4答案:堆棧不會溢出。

問題5:
在不少時候,不少作前端開發的同窗都是認爲循環事件圖中就只會有一個任務列表。但事實上不是這樣的,咱們是能夠有多個任務列表的。由瀏覽器選擇其中一個隊列並在該隊列進行處理回調。
從底層來看,JavaScript中是能夠有宏認爲和微任務的,好比說setTimeout回調是宏任務,而Promise回調是微任務。

他們有什麼區別呢?
主要的區別在於他們的執行方式。宏任務在單個循環週期中一次一個低堆入堆棧,可是微任務隊列老是在執行後返回到事件以前清空。因此,若是你以處理條目的速度向這個隊列添加條目,那麼你就永遠在處理微任務。只有當微任務隊列爲空時,事件循環纔會從新渲染頁面。

而後咱們再回到咱們前面講的問題5中:

function foo() {
  return Promise.resolve().then(foo);
};

咱們這段代碼,每次咱們去調用【foo】的時候,都會在微任務隊列上加另外一個【foo】的回調,所以事件循環沒辦法繼續去處理其餘的事件了(好比說滾動,點擊事件等等),直到該隊列徹底清空位置。所以,不會執行渲染,會被阻止。

問題5答案:不會響應。

問題6:
在咱們作面試題的時候,展開語法和for-of語句去遍歷iterable對象定義要遍歷的數據。其中咱們要使用迭代器的時候,Array和Map都是有默認迭代操做的內置迭代器的。
可是,對象是不可迭代的,也就是咱們這道題裏的,這是一個對象的集合。可是咱們可使用iterable和iterator協議來把它變成能夠迭代的。
在咱們研究對象的時候,若是一個對象他實現了@@iterator方法,那麼它就是能夠迭代的。這意味着這個對象(在他的原型鏈上的一個對象)必須是又@@iterator鍵的屬性的,而後咱們就能夠利用這個鍵,經過常量Symbol.iterator得到。
下面是這道題的舉例寫法:

var obj = { x: 1, y: 2, z: 3 };
obj[Symbol.iterator] = function() {
    // iterator 是一個具備 next 方法的對象,
    // 它的返回至少有一個對象
    // 兩個屬性:value&done。
    // 返回一個 iterator 對象
    return {
        next: function() {
            if (this._countDown === 3) {
               const lastValue = this._countDown;
               return { value: this._countDown, done: true };
              }
            this._countDown = this._countDown + 1;
            return { value: this._countDown, done: false };
        },
        _countDown: 0
    };
};
[...obj]; // 打印 [1, 2, 3]

問題6答案:如上是一種方案,能夠避免TypeError異常。

問題7:
在看這個問題的時候,咱們要先理解for-in循環遍歷自己的可枚舉屬性和對象從原來的原型繼承來的屬性。可枚舉屬性是能夠在for-in循環期間能夠訪問的屬性。
當咱們知道這個知識點前提了以後,咱們在看這道題,你就知道這道題打印的其實就是隻能打印這些特定的屬性。

var obj = { a: 1, b: 2 }; //a,b 都是可枚舉屬性

// 將{c:3}設置爲'obj'的原型,
// 而且咱們知道for-in 循環也迭代 obj 繼承的屬性
// 從它的原型,'c'也能夠被訪問。
Object.setPrototypeOf(obj, { c: 3 });

// 咱們在'obj'中定義了另一個屬性'd',
// 可是將'enumerable'可枚舉設置爲false。 這意味着'd'將被忽略。
Object.defineProperty(obj, "d", { value: 4, enumerable: false });
//因此最後使用for-in遍歷這個對象集合,那就是隻能遍歷出可枚舉屬性
for (let prop in obj) {
    console.log(prop);
}

// 也就是隻能打印
// a
// b
// c

圖解
15436
問題7答案:a、b、c

問題8:
首先咱們能夠看到var x是一個全局遍歷,在不是嚴格模式下,這個X就直接是window對象的屬性了。在這段代碼裏,咱們最重要是要理解this的對象指向問題,this始終是指向調用方法的對象的。因此,在foo,xGetter()的狀況下,this指向的是foo對象,返回的就是在foo中的屬性x,值就是90。可是在xGetter()的狀況下,他是直接調用的foo的getx()方法,可是其中this的指向是在xGetter的做用域,就是指向的window對象中,這時指向的就是全局變量x了,值也就是10。

var x = 10; // 全局變量
var foo = {
    x: 90,//foo對象的內部屬性
    getX: function() {
         return this.x;
    }
};
foo.getX(); // 此時是指向的foo對象,
//因此打印的是X屬性 值就是90
let xGetter = foo.getX;//xGetter是在全局做用域,
//這裏的this就是指向window對象
xGetter(); // 打印 10

15466
問題8答案:10

最後

ok,咱們的8道問題都解決了,若是你前面寫的答案所有都正確,那麼你很是棒!去面試前端工做起碼12k起步了。就算作不出來或者作錯了也沒有關係,咱們都是不斷經過犯錯來學習的,一步步的理解錯誤,理解背後的緣由,才能進步。

更多技術好文,前端開發學習教程,歡迎關注公衆號【前端研究所】看更多前端技術文章!
圖片描述

相關文章
相關標籤/搜索