放在前面,本文原文的標題是 So you think you know JavaScript?
在下感覺有些標題黨了,不過看了下文章的鏈接還是很不錯的。
原文作者是由幾個問題展開了說明。
問題 1: 瀏覽器的console里會打印出什么?
var a = 10;
function foo() {
console.log(a); // ??
var a = 20;
}
foo();
問題2: 如果是有const或let代替var,輸出是否一樣?
var a = 10;
function foo() {
console.log(a); // ??
let a = 20;
}
foo();
問題3: "newArray"中的元素是什么?
var array = [];
for(var i = 0; i <3; i++) {
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // ??
問題4:如果我們在瀏覽器控制臺中運行'foo'函數(shù),是否會導致堆棧溢出錯誤?
function foo() {
setTimeout(foo, 0); // will there by any stack overflow error?
};
問題5:如果我們在控制臺中運行以下函數(shù),頁面的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 ??
解答
現(xiàn)在,讓我們從頭到尾回答上面的每個問題。我將給一個簡短的解釋,同時試圖揭開這些行為的神秘面紗,并提供一些參考資料。
答案 1: undefined
解釋: 使用var關鍵字聲明的變量被提升并在內存中為其賦值為undefined。但是初始化恰好發(fā)生在你在代碼中寫入它們的地方。另外,var聲明的變量是函數(shù)作用域的,而let和const是塊作用域的。所以,這就是這個過程的樣子:
var a = 10; // 全局作用域
function foo() {
// 使用var聲明的會被提升到函數(shù)作用域內頂部.
// 就像: var a;
console.log(a); // 打印 undefined
// 實際初始化值20只發(fā)生在這里
var a = 20; // 本地 scope
}
筆:對這個不了解的,可以看下這篇文章了解一番。
答案 2: ReferenceError: a is not defined
解釋: let和const允許你聲明一個變量被限制在一個塊級作用域,或語句或表達式中。不像var,這些變量不會被提升,并且具有所謂的temporal dead zone(TDZ)。嘗試在TDZ中訪問這些變量將拋出一個ReferenceError,因為它們只能在執(zhí)行到達聲明才可被訪問。可以閱讀詞法作用域和執(zhí)行上下文棧。
var a = 10; // 全局作用域
function foo() { // 進入新的作用域, TDZ開始
// 沒有初始綁定的'a'被創(chuàng)建
console.log(a); // ReferenceError
// TDZ 結束, 'a'只是在這里被初始化了一個值20
let a = 20;
}
下表概述了與JavaScript中使用的不同關鍵字相關的提升行為和范圍(主要摘選:Axel Rauschmayer的博客文章)。
[圖片上傳失敗...(image-73310c-1565572678275)]
答案 3: [3, 3, 3].
解釋: 在for loop的頭部聲明一個帶有var關鍵字的變量,為該變量創(chuàng)建一個綁定(存儲空間)。閱讀關于閉包的更多信息。讓我們再看一次for循環(huán)。
// 誤解作用域:認為存在塊級作用域
var array = [];
for (var i = 0; i < 3; i++) {
// 三個箭頭函數(shù)中的每個都引用同一個綁定,
// 這就是為什么循環(huán)結束之后返回同樣的數(shù)字3
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [3, 3, 3]
如果你聲明一個具有塊級作用域的變量,則會為每個循環(huán)迭代創(chuàng)建一個新綁定。
// 使用ES6塊級作用域綁定
var array = [];
for (let i = 0; i < 3; i++) {
// 這一次,每個“i”引用一個特定迭代的綁定,并保留當時的值。
// 因此,每個arrow函數(shù)返回一個不同的值。
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并發(fā)模型基于“事件循環(huán)”。當我說“瀏覽器是JS的家(歸宿)”時,我真正的意思是瀏覽器提供運行時環(huán)境來執(zhí)行我們的JavaScript代碼。瀏覽器的主要組件包括 調用堆棧 ,事件循環(huán) ,任務隊列 和 Web API 。像setTimeout,setInterval和Promise這樣的全局函數(shù)不是JavaScript的一部分,而是Web API的一部分。JavaScript環(huán)境的可視化表示如下所示:
JS調用堆棧是后進先出(LIFO)。引擎一次從堆棧中獲取一個函數(shù),并從上到下依次運行代碼。每次遇到一些異步代碼(如setTimeout)時,它都會將其交給Web API(箭頭1)。因此,每當觸發(fā)事件時,callback都會被發(fā)送到任務隊列(箭頭2)。Event Loop不斷監(jiān)視任務隊列,并按照排隊順序一次處理一個callback。每當調用堆棧為空時,循環(huán)檢索回調并將其放入堆棧(箭頭3)進行處理。請記住,如果調用堆棧不為空,則事件循環(huán)不會將任何callbacks推送到堆棧。
有關Event Loop如何在JavaScript中工作的更詳細說明,我強烈建議您觀看Philip Roberts的視頻。此外,你還可以通過這個非常棒的工具可視化和理解調用堆棧。來吧,在那里運行'foo'函數(shù),看看會發(fā)生什么!
現(xiàn)在,有了這些知識,讓我們試著回答上述問題:
步驟
- 調用foo()將把foo函數(shù)放進調用棧。
- 在處理內部代碼時,JS引擎遇到setTimeout。
- 然后它將foo回調移交給 WebAPI (箭頭1)并從函數(shù)返回。調用堆棧再次為空。
- 計時器設置為0,因此foo將被發(fā)送到 任務隊列 (箭頭2)。
- 因為,我們的調用堆棧是空的,事件循環(huán)將選擇foo回調并將其推送到調用堆棧進行處理。
- 進程再次重復,堆棧不會溢出 。
筆:其實這個答案里的鏈接和下面答案的鏈接很給力了。
不過也可以看看其他的
答案 5: 不會
解釋: 大多數(shù)時候,我看到開發(fā)人員假設在事件循環(huán)的藍圖中只有一個任務隊列(筆: 也叫task queue或event queue或callback queue )。但事實并非如此。我們可以有多個任務隊列。由瀏覽器選擇任意的隊列并在其中處理callbacks。
在高層次上來看,JavaScript中有宏任務和微任務。setTimeout回調是 macrotasks 而Promise回調是 microtasks 。主要的區(qū)別在于他們的執(zhí)行儀式。宏任務在單個循環(huán)周期中一次一個地推入堆棧,但是微任務隊列總是在執(zhí)行返回到event loop(包括任何額外排隊的項)之前清空。因此,如果你將這些項快速的添加到這個你正在處理的隊列,那么你將永遠在處理微任務。有關更深入的解釋,請觀看Jake Archibald的視頻或文章。
在執(zhí)行返回事件循環(huán)之前,微任務隊列總是被清空
現(xiàn)在,當你在控制臺中運行以下代碼段時:
function foo() {
return Promise.resolve().then(foo);
};
每次調用'foo'都會繼續(xù)在微任務隊列上添加另一個'foo'回調,因此事件循環(huán)無法繼續(xù)處理其他事件(scroll,click等),直到該隊列完全清空為止。因此,它會阻止渲染。
筆:Jake的此文絕對是精華,沒有讀過的可以拜讀一番。
答案 6: 可以, 通過是對象iterables
解釋: 拓展運算符和for-of語句迭代iterable對象。數(shù)組或Map是具有默認迭代行為的內置iterable。對象不是可迭代的,但你可以使用iterable和iterator協(xié)議使它們可迭代。
在Mozilla文檔中,如果一個對象實現(xiàn)了@@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函數(shù)來自定義對象的迭代行為:
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循環(huán)遍歷對象本身的可枚舉屬性以及對象從其原型繼承的屬性。可枚舉屬性是可以在for-in循環(huán)期間包含和訪問的屬性。
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 }
現(xiàn)在掌握了這些知識,應該很容易理解為什么我們的代碼會打印出這些特定的屬性:
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對象的屬性(假設它是瀏覽器環(huán)境而不是嚴格模式)。看下面代碼:
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對象來創(chuàng)建新函數(shù)。
let getFooX = foo.getX.bind(foo);
getFooX(); // prints 90
筆:這個說的主要就是this了,不了解的可以看下
就是這樣!如果你所有的答案都正確,那就做得好。錯了不可怕,因為我們都從錯誤中學習。關鍵是要知道背后的“原因”。
你都對了嗎老兄。
Kyle simpson: You don't know js