天天看點

JS 常見問題

JavaScript 是一種有趣的語言,我們都喜歡它,因為它的性質。浏覽器是JavaScript的主要運作的地方,兩者在我們的服務中協同工作。JS有一些概念,人們往往會對它掉以輕心,有時可能會忽略不計。原型、閉包和事件循環等概念仍然是大多數JS開發人員繞道而行的晦澀領域之一。正如我們所知,無知是一件危險的事情,它可能會導緻錯誤。

接下來,來看看幾個問題,你也可以試試想想,然後作答。

問題1:浏覽器控制台上會列印什麼?

問題2:如果我們使用 let 或 const 代替 var,輸出是否相同

問題3:"newArray"中有哪些元素?

問題4:如果我們在浏覽器控制台中運作'foo'函數,是否會導緻堆棧溢出錯誤?

問題5: 如果在控制台中運作以下函數,頁面(頁籤)的 UI 是否仍然響應

問題6: 我們能否以某種方式為下面的語句使用展開運算而不導緻類型錯誤

問題7:運作以下代碼片段時,控制台上會列印什麼?

問題8:xGetter() 會列印什麼值?

var x = 10;

var foo = {

  x: 90,

  getX: function() {

    return this.x;

  }

};

foo.getX(); // prints 90

var xGetter = foo.getX;

xGetter(); // prints ??

答案

現在,讓我們從頭到尾回答每個問題。我将給您一個簡短的解釋,同時試圖揭開這些行為的神秘面紗,并提供一些參考資料。

使用<code>var</code>關鍵字聲明的變量在JavaScript中會被提升,并在記憶體中配置設定值<code>undefined</code>。但初始化恰發生在你給變量指派的地方。另外,<code>var</code>聲明的變量是函數作用域的,而<code>let</code>和<code>const</code>是塊作用域的。是以,這就是這個過程的樣子:

<code>let</code>和<code>const</code>聲明可以讓變量在其作用域上受限于它所使用的塊、語句或表達式。與<code>var</code>不同的是,這些變量沒有被提升,并且有一個所謂的暫時死區(TDZ)。試圖通路TDZ中的這些變量将引發<code>ReferenceError</code>,因為隻有在執行到達聲明時才能通路它們。

下表概述了與JavaScript中使用的不同關鍵字聲明的變量對應的提升行為和使用域:

JS 常見問題

在<code>for</code>循環的頭部聲明帶有<code>var</code>關鍵字的變量會為該變量建立單個綁定(存儲空間)。閱讀更多關于閉包的資訊。讓我們再看一次for循環。

如果使用 <code>let</code> 聲明一個具有塊級作用域的變量,則為每個循環疊代建立一個新的綁定。

解決這個問題的另一種方法是使用閉包。

問題4 : 不會溢出

JavaScript并發模型基于“事件循環”。當我們說“浏覽器是 JS 的家”時我真正的意思是浏覽器提供運作時環境來執行我們的JS代碼。

浏覽器的主要元件包括調用堆棧,事件循環,任務隊列和Web API。像<code>setTimeout</code>,<code>setInterval</code>和<code>Promise</code>這樣的全局函數不是JavaScript的一部分,而是 Web API 的一部分。JavaScript 環境的可視化形式如下所示:

JS 常見問題

JS調用棧是後進先出(LIFO)的。引擎每次從堆棧中取出一個函數,然後從上到下依次運作代碼。每當它遇到一些異步代碼,如<code>setTimeout</code>,它就把它交給<code>Web API</code>(箭頭1)。是以,每當事件被觸發時,<code>callback</code> 都會被發送到任務隊列(箭頭2)。

事件循環(Event loop)不斷地監視任務隊列(Task Queue),并按它們排隊的順序一次處理一個回調。每當調用堆棧(call stack)為空時,Event loop擷取回調并将其放入堆棧(stack )(箭頭3)中進行處理。請記住,如果調用堆棧不是空的,則事件循環不會将任何回調推入堆棧。

現在,有了這些知識,讓我們來回答前面提到的問題:

步驟

調用 <code>foo()</code>會将<code>foo</code>函數放入調用堆棧(call stack)。

在處理内部代碼時,JS引擎遇到<code>setTimeout</code>。

然後将<code>foo</code>回調函數傳遞給WebAPIs(箭頭1)并從函數傳回,調用堆棧再次為空

計時器被設定為0,是以<code>foo</code>将被發送到任務隊列&lt;Task Queue&gt;(箭頭2)。

由于調用堆棧是空的,事件循環将選擇<code>foo</code>回調并将其推入調用堆棧進行處理。

程序再次重複,堆棧不會溢出。

運作示意圖如下所示:

JS 常見問題

大多數時候,開發人員假設在事件循環&lt;event loop&gt;圖中隻有一個任務隊列。但事實并非如此,我們可以有多個任務隊列。由浏覽器選擇其中的一個隊列并在該隊列中處理回調&lt;callbacks&gt;。

在底層來看,JavaScript中有宏任務和微任務。<code>setTimeout</code>回調是宏任務,而<code>Promise</code>回調是微任務。

主要的差別在于他們的執行方式。宏任務在單個循環周期中一次一個地推入堆棧,但是微任務隊列總是在執行後傳回到事件循環之前清空。是以,如果你以處理條目的速度向這個隊列添加條目,那麼你就永遠在處理微任務。隻有當微任務隊列為空時,事件循環才會重新渲染頁面、

現在,當你在控制台中運作以下代碼段

展開文法 和 for-of 語句周遊<code>iterable</code>對象定義要周遊的資料。<code>Array</code> 或<code>Map</code> 是具有預設疊代行為的内置疊代器。對象不是可疊代的,但是可以通過使用iterable和iterator協定使它們可疊代。

在Mozilla文檔中,如果一個對象實作了<code>@@iterator</code>方法,那麼它就是可疊代的,這意味着這個對象(或者它原型鍊上的一個對象)必須有一個帶有<code>@@iterator</code>鍵的屬性,這個鍵可以通過常量<code>Symbol.iterator</code>獲得。

上述語句可能看起來有點冗長,但是下面的示例将更有意義:

還可以使用 generator 函數來定制對象的疊代行為:

問題7 : a, b, c

<code>for-in</code>循環周遊對象本身的可枚舉屬性以及對象從其原型繼承的屬性。可枚舉屬性是可以在<code>for-in</code>循環期間包含和通路的屬性。

問題8 : 10

在全局範圍内初始化<code>x</code>時,它成為window對象的屬性(不是嚴格的模式)。看看下面的代碼:

咱們可以斷言:

<code> </code>

<code>this</code> 始終指向調用方法的對象。是以,在<code>foo.getx()</code>的例子中,它指向<code>foo</code>對象,傳回<code>90</code>的值。而在<code>xGetter()</code>的情況下,<code>this</code>指向 window對象, 傳回 window 中的<code>x</code>的值,即<code>10</code>。

要擷取 <code>foo.x</code>的值,可以通過使用<code>Function.prototype.bind</code>将<code>this</code>的值綁定到<code>foo</code>對象來建立新函數。

就這樣!如果你的所有答案都正确,那麼幹得漂亮。咱們都是通過犯錯來學習的。這一切都是為了了解背後的“原因”。