[譯] 你真的懂JavaScript嗎?

放在前面,本文原文的標題是 So you think you know JavaScript?javascript

在下感受有些標題黨了,不過看了下文章的連接仍是很不錯的。html

原文做者是由幾個問題展開了說明。java

問題 1: 瀏覽器的console裏會打印出什麼?git

var a = 10;
function foo() {
    console.log(a); // ??
    var a = 20;
}
foo();
複製代碼

問題2: 若是是有const或let代替var,輸出是否同樣?es6

var a = 10;
function foo() {
    console.log(a); // ??
    let a = 20;
}
foo();
複製代碼

問題3: "newArray"中的元素是什麼?github

var array = [];
for(var i = 0; i <3; i++) {
 array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // ??
複製代碼

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

function foo() {
  setTimeout(foo, 0); // will there by any stack overflow error?
};
複製代碼

問題5:若是咱們在控制檯中運行如下函數,頁面的UI(tab頁)是否仍然響應?瀏覽器

function foo() {
  return Promise.resolve().then(foo);
};
複製代碼

問題6:咱們能夠在不引發TypeError的狀況下以某種方式使用如下語句的擴展語法嗎?閉包

var obj = { x: 1, y: 2, z: 3 };
[...obj]; // TypeError
複製代碼

問題7:運行如下代碼片斷時,控制檯上會打印什麼?併發

var obj = { a: 1, b: 2 };
Object.setPrototypeOf(obj, {c: 3});
Object.defineProperty(obj, 'd', { value: 4, enumerable: false });

// what properties will be printed when we run the for-in loop?
for(let prop in obj) {
    console.log(prop);
}
複製代碼

問題8:xGetter()將輸出什麼值?

var x = 10;
var foo = {
  x: 90,
  getX: function() {
    return this.x;
  }
};
foo.getX(); // prints 90
var xGetter = foo.getX;
xGetter(); // prints ??
複製代碼

解答

如今,讓咱們從頭至尾回答上面的每一個問題。我將給一個簡短的解釋,同時試圖揭開這些行爲的神祕面紗,並提供一些參考資料。

答案 1: undefined

解釋: 使用var關鍵字聲明的變量被提高並在內存中爲其賦值爲undefined。可是初始化剛好發生在你在代碼中寫入它們的地方。另外,var聲明的變量是函數做用域的,而let和const是塊做用域的。因此,這就是這個過程的樣子:

var a = 10; // 全局做用域
function foo() {
// 使用var聲明的會被提高到函數做用域內頂部.
// 就像: var a;

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

// 實際初始化值20只發生在這裏
   var a = 20; // 本地 scope
}
複製代碼

筆:對這個不瞭解的,能夠看下這篇文章瞭解一番

答案 2: ReferenceError: a is not defined

解釋: letconst容許你聲明一個變量被限制在一個塊級做用域,或語句或表達式中。不像var,這些變量不會被提高,而且具備所謂的temporal dead zone(TDZ)。嘗試在TDZ中訪問這些變量將拋出一個ReferenceError,由於它們只能在執行到達聲明纔可被訪問。能夠閱讀詞法做用域執行上下文棧

var a = 10; // 全局做用域
function foo() { // 進入新的做用域, TDZ開始

// 沒有初始綁定的'a'被建立
    console.log(a); // ReferenceError

// TDZ 結束, 'a'只是在這裏被初始化了一個值20
    let a = 20;
}
複製代碼

下表概述了與JavaScript中使用的不一樣關鍵字相關的提高行爲和範圍(主要摘選:Axel Rauschmayer的博客文章)。

image-20190810210348073

答案 3: [3, 3, 3].

解釋:for loop的頭部聲明一個帶有var關鍵字的變量,爲該變量建立一個綁定(存儲空間)。閱讀關於閉包的更多信息。讓咱們再看一次for循環。

// 誤解做用域:認爲存在塊級做用域
var array = [];
for (var i = 0; i < 3; i++) {
  // 三個箭頭函數中的每一個都引用同一個綁定,
  // 這就是爲何循環結束以後返回一樣的數字3
  array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [3, 3, 3]
複製代碼

若是你聲明一個具備塊級做用域的變量,則會爲每一個循環迭代建立一個新綁定。

// 使用ES6塊級做用域綁定
var array = [];
for (let i = 0; i < 3; i++) {
  // 這一次,每一個「i」引用一個特定迭代的綁定,並保留當時的值。
  // 所以,每一個arrow函數返回一個不一樣的值。
  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]
複製代碼

爲啥let能夠,能夠參考這篇文章

答案 4: 不會

解釋: JavaScript併發模型基於「事件循環」。當我說「瀏覽器是JS的家(歸宿)」時,我真正的意思是瀏覽器提供運行時環境來執行咱們的JavaScript代碼。瀏覽器的主要組件包括 調用堆棧事件循環任務隊列Web API 。像setTimeout,setInterval和Promise這樣的全局函數不是JavaScript的一部分,而是Web API的一部分。JavaScript環境的可視化表示以下所示:

alt text

JS調用堆棧是後進先出(LIFO)。引擎一次從堆棧中獲取一個函數,並從上到下依次運行代碼。每次遇到一些異步代碼(如setTimeout)時,它都會將其交給Web API(箭頭1)。所以,每當觸發事件時,callback都會被髮送到任務隊列(箭頭2)。Event Loop不斷監視任務隊列,並按照排隊順序一次處理一個callback。每當調用堆棧爲空時,循環檢索回調並將其放入堆棧(箭頭3)進行處理。請記住,若是調用堆棧不爲空,則事件循環不會將任何callbacks推送到堆棧。

有關Event Loop如何在JavaScript中工做的更詳細說明,我強烈建議您觀看Philip Roberts的視頻。此外,你還能夠經過這個很是棒的工具可視化和理解調用堆棧。來吧,在那裏運行'foo'函數,看看會發生什麼!

如今,有了這些知識,讓咱們試着回答上述問題:

步驟

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

筆:其實這個答案裏的連接和下面答案的連接很給力了。

不過也能夠看看其餘的

答案 5: 不會

解釋: 大多數時候,我看到開發人員假設在事件循環的藍圖中只有一個任務隊列(筆: 也叫task queue或event queue或callback queue )。但事實並不是如此。咱們能夠有多個任務隊列。由瀏覽器選擇任意的隊列並在其中處理callbacks

在高層次上來看,JavaScript中有宏任務和微任務。setTimeout回調是 macrotasks 而Promise回調是 microtasks 。主要的區別在於他們的執行儀式。宏任務在單個循環週期中一次一個地推入堆棧,可是微任務隊列老是在執行返回到event loop(包括任何額外排隊的項)以前清空。所以,若是你將這些項快速的添加到這個你正在處理的隊列,那麼你將永遠在處理微任務。有關更深刻的解釋,請觀看Jake Archibald視頻文章

在執行返回事件循環以前,微任務隊列老是被清空

如今,當你在控制檯中運行如下代碼段時:

function foo() {
  return Promise.resolve().then(foo);
};
複製代碼

每次調用'foo'都會繼續在微任務隊列上添加另外一個'foo'回調,所以事件循環沒法繼續處理其餘事件(scroll,click等),直到該隊列徹底清空爲止。所以,它會阻止渲染。

筆:Jake的此文絕對是精華,沒有讀過的能夠拜讀一番。

答案 6: 能夠, 經過是對象iterables

解釋: 拓展運算符for-of語句迭代iterable對象。數組或Map是具備默認迭代行爲的內置iterable。對象不是可迭代的,但你可使用iterableiterator協議使它們可迭代。

在Mozilla文檔中,若是一個對象實現了@@iterator方法,那麼它就是可迭代的,這意味着這個對象(或者它原型鏈上的一個對象)必須有一個帶有@@iterator鍵的屬性,這個鍵能夠經過常量Symbol.iterator得到。

上述陳述可能看起來有點冗長,但下面的例子會更有意義:

var obj = { x: 1, y: 2, z: 3 };
obj[Symbol.iterator] = function() {

  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]; // will print [1, 2, 3]
複製代碼

你還可使用generator函數來自定義對象的迭代行爲:

var obj = { x: 1, y: 2, z: 3 };
obj[Symbol.iterator] = function*() {
  yield 1;
  yield 2;
  yield 3;
};
[...obj]; // print [1, 2, 3]
複製代碼

筆:對這個不熟悉的能夠看下一些例子:

iterator&generator

答案 7: a, b, c

解釋: for-in循環遍歷對象自己的可枚舉屬性以及對象從其原型繼承的屬性。可枚舉屬性是能夠在for-in循環期間包含和訪問的屬性。

var obj = { a: 1, b: 2 };
var descriptor = Object.getOwnPropertyDescriptor(obj, "a");
console.log(descriptor.enumerable); // true
console.log(descriptor);
// { value: 1, writable: true, enumerable: true, configurable: true }
複製代碼

如今掌握了這些知識,應該很容易理解爲何咱們的代碼會打印出這些特定的屬性:

var obj = { a: 1, b: 2 }; // a, b are both enumerables properties


Object.setPrototypeOf(obj, { c: 3 });

Object.defineProperty(obj, "d", { value: 4, enumerable: false });

for (let prop in obj) {
  console.log(prop);
}
複製代碼

筆:對這個不瞭解的能夠看文章瞭解一下

property descriptors

object getPrototypeOf&setPrototypeOf

答案 8: 10

解釋: 當咱們將x初始化爲全局做用域時,它將成爲window對象的屬性(假設它是瀏覽器環境而不是嚴格模式)。看下面代碼:

var x = 10; // 全局做用域
var foo = {
  x: 90,
  getX: function() {
    return this.x;
  }
};
foo.getX(); // prints 90
let xGetter = foo.getX;
xGetter(); // prints 10
複製代碼

咱們能夠斷言:

window.x === 10; // true
複製代碼

this 將始終指向調用該方法的對象。所以,在foo.getX() 的狀況下,this 指向foo對象返回值90。而在xGetter()的狀況下,this 指向window 對象返回值10。

要檢索foo.x的值,咱們能夠經過使用Function.prototype.bindthis 的值綁定到foo對象來建立新函數。

let getFooX = foo.getX.bind(foo);
getFooX(); // prints 90
複製代碼

筆:這個說的主要就是this了,不瞭解的能夠看下

this All Makes Sense Now

就是這樣!若是你全部的答案都正確,那就作得好。錯了不可怕,由於咱們都從錯誤中學習。關鍵是要知道背後的「緣由」。

你都對了嗎老兄。

原文:

  1. So you think you know JavaScript?
  2. 你真的懂JavaScript嗎

Kyle simpson: You don't know js :smile: ​

相關文章
相關標籤/搜索