放在前面,本文原文的標題是 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
解釋: let和const容許你聲明一個變量被限制在一個塊級做用域,或語句或表達式中。不像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的博客文章)。
答案 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環境的可視化表示以下所示:
JS調用堆棧是後進先出(LIFO)。引擎一次從堆棧中獲取一個函數,並從上到下依次運行代碼。每次遇到一些異步代碼(如setTimeout)時,它都會將其交給Web API(箭頭1)。所以,每當觸發事件時,callback都會被髮送到任務隊列(箭頭2)。Event Loop不斷監視任務隊列,並按照排隊順序一次處理一個callback。每當調用堆棧爲空時,循環檢索回調並將其放入堆棧(箭頭3)進行處理。請記住,若是調用堆棧不爲空,則事件循環不會將任何callbacks推送到堆棧。
有關Event Loop如何在JavaScript中工做的更詳細說明,我強烈建議您觀看Philip Roberts的視頻。此外,你還能夠經過這個很是棒的工具可視化和理解調用堆棧。來吧,在那裏運行'foo'函數,看看會發生什麼!
如今,有了這些知識,讓咱們試着回答上述問題:
筆:其實這個答案裏的連接和下面答案的連接很給力了。
不過也能夠看看其餘的
答案 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。對象不是可迭代的,但你可使用iterable和iterator協議使它們可迭代。
在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]
複製代碼
筆:對這個不熟悉的能夠看下一些例子:
答案 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);
}
複製代碼
筆:對這個不瞭解的能夠看文章瞭解一下
答案 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.bind將 this 的值綁定到foo對象來建立新函數。
let getFooX = foo.getX.bind(foo);
getFooX(); // prints 90
複製代碼
筆:這個說的主要就是this了,不瞭解的能夠看下
就是這樣!若是你全部的答案都正確,那就作得好。錯了不可怕,由於咱們都從錯誤中學習。關鍵是要知道背後的「緣由」。
你都對了嗎老兄。
原文:
Kyle simpson: You don't know js :smile: