問題1:瀏覽器控制臺上會打印什么萍丐?
var a = 10;
function foo() {
console.log(a); // ??
var a = 20;
}
foo();
問題2:如果我們使用 let 或 const 代替 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:如果我們在瀏覽器控制臺中運(yùn)行'foo'函數(shù),是否會導(dǎo)致堆棧溢出錯誤?
function foo() {
setTimeout(foo, 0); // 是否存在堆棧溢出錯誤?
};
問題5: 如果在控制臺中運(yùn)行以下函數(shù)裳扯,頁面(選項(xiàng)卡)的 UI 是否仍然響應(yīng)
function foo() {
return Promise.resolve().then(foo);
};
問題6: 我們能否以某種方式為下面的語句使用展開運(yùn)算而不導(dǎo)致類型錯誤
var obj = { x: 1, y: 2, z: 3 };
[...obj]; // TypeError
問題7:運(yùn)行以下代碼片段時(shí),控制臺上會打印什么谤职?
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:瀏覽器控制臺上會打印什么?
var a = 10;
function foo() {
console.log(a); // ??
var a = 20;
}
foo();
答案: undefined
解析:
使用var
關(guān)鍵字聲明的變量在JavaScript中會被提升允蜈,并在內(nèi)存中分配值undefined
冤吨。 但初始化恰發(fā)生在你給變量賦值的地方。 另外饶套,var
聲明的變量是函數(shù)作用域的漩蟆,而let
和const
是塊作用域的。 所以妓蛮,這就是這個過程的樣子:
var a = 10; // 全局使用域
function foo() {
// var a 的聲明將被提升到到函數(shù)的頂部怠李。
// 比如:var a
console.log(a); // 打印 undefined
// 實(shí)際初始化值20只發(fā)生在這里
var a = 20; // local scope
}
問題2:如果我們使用 let 或 const 代替 var,輸出是否相同?
var a = 10;
function foo() {
console.log(a); // ??
let a = 20;
}
foo();
答案:ReferenceError:a undefined
蛤克。
解析:
let
和const
聲明可以讓變量在其作用域上受限于它所使用的塊扔仓、語句或表達(dá)式。與var
不同的是咖耘,這些變量沒有被提升,并且有一個所謂的暫時(shí)死區(qū)(TDZ)撬码。試圖訪問TDZ中的這些變量將引發(fā)ReferenceError
儿倒,因?yàn)橹挥性趫?zhí)行到達(dá)聲明時(shí)才能訪問它們。
var a = 10; // 全局使用域
function foo() { // TDZ 開始
// 創(chuàng)建了未初始化的'a'
console.log(a); // ReferenceError
// TDZ結(jié)束,'a'僅在此處初始化夫否,值為20
let a = 20;
}
下表概述了與JavaScript中使用的不同關(guān)鍵字聲明的變量對應(yīng)的提升行為和使用域:
問題3:“newArray”中有哪些元素彻犁?
var array = [];
for(var i = 0; i <3; i++) {
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // ??
答案: [3, 3, 3]
解析:
在for
循環(huán)的頭部聲明帶有var
關(guān)鍵字的變量會為該變量創(chuàng)建單個綁定(存儲空間)。 閱讀更多關(guān)于閉包的信息凰慈。 讓我們再看一次for循環(huán)汞幢。
// 誤解作用域:認(rèn)為存在塊級作用域
var array = [];
for (var i = 0; i < 3; i++) {
// 三個箭頭函數(shù)體中的每個`'i'`都指向相同的綁定,
// 這就是為什么它們在循環(huán)結(jié)束時(shí)返回相同的值'3'微谓。
array.push(() => i);
}
var newArray = array.map(el => el());
console.log(newArray); // [3, 3, 3]
如果使用let
聲明一個具有塊級作用域的變量森篷,則為每個循環(huán)迭代創(chuàng)建一個新的綁定。
// 使用ES6塊級作用域
var array = [];
for (let i = 0; i < 3; i++) {
// 這一次豺型,每個'i'指的是一個新的的綁定仲智,并保留當(dāng)前的值。
// 因此姻氨,每個箭頭函數(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]
問題4:如果我們在瀏覽器控制臺中運(yùn)行'foo'函數(shù)肴焊,是否會導(dǎo)致堆棧溢出錯誤前联?
function foo() {
setTimeout(foo, 0); // 是否存在堆棧溢出錯誤?
};
答案 : 不會溢出
解析:
JavaScript并發(fā)模型基于“事件循環(huán)”。 當(dāng)我們說“瀏覽器是 JS 的家”時(shí)我真正的意思是瀏覽器提供運(yùn)行時(shí)環(huán)境來執(zhí)行我們的JS代碼娶眷。
瀏覽器的主要組件包括調(diào)用堆棧似嗤,事件循環(huán)****,任務(wù)隊(duì)列和Web API茂浮。 像setTimeout
揍移,setInterval
和Promise
這樣的全局函數(shù)不是JavaScript的一部分,而是 Web API 的一部分由缆。 JavaScript 環(huán)境的可視化形式如下所示:
JS調(diào)用棧是后進(jìn)先出(LIFO)的矩欠。引擎每次從堆棧中取出一個函數(shù),然后從上到下依次運(yùn)行代碼幌羞。每當(dāng)它遇到一些異步代碼寸谜,如setTimeout
,它就把它交給Web API
(箭頭1)属桦。因此熊痴,每當(dāng)事件被觸發(fā)時(shí),callback
都會被發(fā)送到任務(wù)隊(duì)列(箭頭2)聂宾。
事件循環(huán)(Event loop)不斷地監(jiān)視任務(wù)隊(duì)列(Task Queue)果善,并按它們排隊(duì)的順序一次處理一個回調(diào)。每當(dāng)調(diào)用堆棧(call stack)為空時(shí)系谐,Event loop獲取回調(diào)并將其放入堆棧(stack )(箭頭3)中進(jìn)行處理巾陕。請記住讨跟,如果調(diào)用堆棧不是空的,則事件循環(huán)不會將任何回調(diào)推入堆棧鄙煤。
現(xiàn)在晾匠,有了這些知識,讓我們來回答前面提到的問題:
步驟
1梯刚、調(diào)用foo()
會將foo
函數(shù)放入調(diào)用堆棧(call stack)凉馆。
2、在處理內(nèi)部代碼時(shí)亡资,JS引擎遇到setTimeout
澜共。
3、然后將foo
回調(diào)函數(shù)傳遞給WebAPIs(箭頭1)并從函數(shù)返回沟于,調(diào)用堆棧再次為空
4咳胃、計(jì)時(shí)器被設(shè)置為0,因此foo
將被發(fā)送到任務(wù)隊(duì)列(箭頭2)旷太。
5展懈、由于調(diào)用堆棧是空的,事件循環(huán)將選擇foo
回調(diào)并將其推入調(diào)用堆棧進(jìn)行處理供璧。
6存崖、進(jìn)程再次重復(fù),堆棧不會溢出睡毒。
運(yùn)行示意圖如下所示:
問題5: 如果在控制臺中運(yùn)行以下函數(shù)来惧,頁面(選項(xiàng)卡)的 UI 是否仍然響應(yīng)?
function foo() {
return Promise.resolve().then(foo);
};
答案:不會響應(yīng)
解析:
大多數(shù)時(shí)候,開發(fā)人員假設(shè)在事件循環(huán)圖中只有一個任務(wù)隊(duì)列演顾。但事實(shí)并非如此供搀,我們可以有多個任務(wù)隊(duì)列。由瀏覽器選擇其中的一個隊(duì)列并在該隊(duì)列中處理回調(diào)钠至。
在底層來看葛虐,JavaScript中有宏任務(wù)和微任務(wù)。setTimeout
回調(diào)是宏任務(wù)棉钧,而Promise
回調(diào)是微任務(wù)屿脐。
主要的區(qū)別在于他們的執(zhí)行方式。宏任務(wù)在單個循環(huán)周期中一次一個地推入堆棧宪卿,但是微任務(wù)隊(duì)列總是在執(zhí)行后返回到事件循環(huán)之前清空的诵。因此,如果你以處理?xiàng)l目的速度向這個隊(duì)列添加條目佑钾,那么你就永遠(yuǎn)在處理微任務(wù)西疤。只有當(dāng)微任務(wù)隊(duì)列為空時(shí),事件循環(huán)才會重新渲染頁面休溶。
現(xiàn)在代赁,當(dāng)你在控制臺中運(yùn)行以下代碼段
function foo() {
return Promise.resolve().then(foo);
};
每次調(diào)用'foo
'都會繼續(xù)在微任務(wù)隊(duì)列上添加另一個'foo
'回調(diào)撒遣,因此事件循環(huán)無法繼續(xù)處理其他事件(滾動,單擊等)管跺,直到該隊(duì)列完全清空為止。 因此禾进,它會阻止渲染豁跑。
問題6: 我們能否以某種方式為下面的語句使用展開運(yùn)算而不導(dǎo)致類型錯誤?
var obj = { x: 1, y: 2, z: 3 };
[...obj]; // TypeError
答案:會導(dǎo)致TypeError錯誤
解析:
展開語法 和 for-of 語句遍歷iterable
對象定義要遍歷的數(shù)據(jù)泻云。Array
或Map
是具有默認(rèn)迭代行為的內(nèi)置迭代器艇拍。對象不是可迭代的,但是可以通過使用iterable和iterator協(xié)議使它們可迭代宠纯。
在Mozilla文檔中卸夕,如果一個對象實(shí)現(xiàn)了@@iterator
方法,那么它就是可迭代的婆瓜,這意味著這個對象(或者它原型鏈上的一個對象)必須有一個帶有@@iterator
鍵的屬性快集,這個鍵可以通過常量Symbol.iterator
獲得。
上述語句可能看起來有點(diǎn)冗長廉白,但是下面的示例將更有意義:
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]
還可以使用generator函數(shù)來定制對象的迭代行為:
var obj = {x:1, y:2, z: 3}
obj[Symbol.iterator] = function*() {
yield 1;
yield 2;
yield 3;
}
[...obj]; // 打印 [1, 2, 3]
問題7:運(yùn)行以下代碼片段時(shí)猴蹂,控制臺上會打印什么院溺?
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);
}
答案: 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)在你已經(jīng)掌握了這些知識珍逸,應(yīng)該很容易理解為什么我們的代碼要打印這些特定的屬性
var obj = { a: 1, b: 2 }; //a,b 都是 enumerables 屬性
// 將{c:3}設(shè)置為'obj'的原型聋溜,并且我們知道
// for-in 循環(huán)也迭代 obj 繼承的屬性
// 從它的原型谆膳,'c'也可以被訪問。
Object.setPrototypeOf(obj, { c: 3 });
// 我們在'obj'中定義了另外一個屬性'd'勤婚,但是
// 將'enumerable'設(shè)置為false摹量。 這意味著'd'將被忽略。
Object.defineProperty(obj, "d", { value: 4, enumerable: false });
for (let prop in obj) {
console.log(prop);
}
// 打印
// a
// b
// c
問題8:xGetter() 會打印什么值馒胆?
var x = 10;
var foo = {
x: 90,
getX: function() {
return this.x;
}
};
foo.getX(); // prints 90
var xGetter = foo.getX;
xGetter(); // prints ??
答案 : 10
解析:
在全局范圍內(nèi)初始化x
時(shí)缨称,它成為window對象的屬性(不是嚴(yán)格的模式)∽S兀看看下面的代碼:
var x = 10; // global scope
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
始終指向調(diào)用方法的對象睦尽。因此,在foo.getx()
的例子中型雳,它指向foo
對象当凡,返回90
的值山害。而在xGetter()
的情況下,this
指向 window對象, 返回window中的x
的值沿量,即10
浪慌。
要獲取foo.x
的值,可以通過使用Function.prototype.bind
將this
的值綁定到foo
對象來創(chuàng)建新函數(shù)朴则。
let getFooX = foo.getX.bind(foo);
getFooX(); // 90
就這樣权纤! 如果你的所有答案都正確,那么干漂亮乌妒。 咱們都是通過犯錯來學(xué)習(xí)的汹想。 這一切都是為了了解背后的“原因”。