1. 可變性
在 JavaScript 中有七種基本數據類型(string、number、boolean、undefined、symbol、bigint 和 null),這些都是不可變的。這意味着一旦分配了一個值,我們就無法修改它們,我們可以做的是將它重新分配給一個不同的值(不同的內存指針)。另一方面,其他數據類型(如 Object 和 Function)是可變的,這意味着我們可以修改同一內存指針中的值。
// Q1
let text = 'abcde'
text[1] = 'z'
console.log(text) // abcde
字符串是不可變的,因此一旦分配給一個值,就不能將其更改為不同的值,您可以做的是重新分配它。請記住,更改值和重新分配給另一個值是不同的。
// Q2
const arr = [1, 2, 3]
arr.length = 0
console.log(arr) // []
分配 arr.length 為 0 與重置或清除數組相同,因此此時數組將變為空數組。
// Q3
const arr = [1, 2, 3, 4]
arr[100] = undefined
console.log(arr, arr.length) // [1, 2, 3, 4, empty × 96, undefined] 101
因為數組佔用的是連續的內存位置,所以當我們將索引 100 賦給一個值(包括 undefined)時,JavaScript 會保留索引 0 到 索引 100 的內存,這意味着現在的數組長度為 101。
2. var 和 提升
// Q4
var variable = 10;
(() => {
variable2 = 100
console.log(variable)
console.log(variable2)
variable = 20
var variable2 = 50
console.log(variable)
})();
console.log(variable)
var variable = 30
console.log(variable2)
// 10
// 100
// 20
// 20
// ReferenceError: variable2 is not defined
var 是函數作用域變量,而 let 和 const 是塊級作用域變量,只有 var 能被提升,這意味着變量聲明總是被移動到頂部。由於提升,您甚至可以在使用 var 關鍵字聲明變量之前分配、調用或使用該變量。
let 和const 不能被提升,因為它啓用了 TDZ(臨時性死區),這意味着變量在聲明之前是不可訪問的。
在上面的示例中,variable2 在函數內部聲明,var 關鍵字使該變量僅在函數範圍內可用。所以當函數外的任何東西想要使用或者調用該變量時,referenceError 就會被拋出。
// Q5
test() // 不報錯
function test() {
cconsole.log('test')
}
test2() // 報錯
var test2 = () => console.log('test2')
function 關鍵字聲明的函數可以提升函數語句,但是不能提升箭頭函數,即使它是使用 var 進行變量聲明的。
3. 偶然性全局變量
// Q6
function foo() {
let a = b = 0;
a++;
return a;
}
foo();
typeof b; // number
typeof a; // undefined
console.log(a) // error: ReferenceError: a is not defined
var 是函數作用域,let 是塊級作用域變量。雖然看起來 a 和 b 都是使用 let 聲明的( let a = b = 0),但實際上變量 b 被聲明為全局變量並分配給 Window 對象。換句話説,它類似於:
function foo() {
window.b = 0;
let a = b;
a++;
}
4. 閉包
// Q7
const length = 4;
const fns = [];
const fns2 = [];
for(var i = 0; i < length; i++) {
fns.push(() => console.log(i));
}
for(let i = 0; i < length; i++) {
fns2.push(() => console.log(i));
}
fns.forEach(fn => fn()); // 4 4 4 4
fns2.forEach(fn => fn()); // 0 1 2 3
閉包是對變量環境的一種保護,即使變量已經更改或者已被垃圾回收。在上面的問題中,區別在於變量聲明,其中第一個循環使用的是 var,第二個循環使用的是 let。
var 是函數作用域變量,因此當它在 for 循環塊內聲明時,var 被視為全局變量而不是內部變量。另一方面,let 是塊級作用域的變量,類似於 Java 和 C++ 等其他語言中的變量聲明。
在這種情況下,閉包只發生在 let 變量中,推送到 fns2 數組的每個函數都會記住變量當前的值,無論變量將來是否更改。相反,fns 不記住變量的當前值,它使用全局變量的未來或最終值。
5. 對象
// Q8
var obj1 = { n: 1 }
var obj2 = obj1
obj2.n = 2
console.log(obj1) // { n: 2 }
// Q9
function foo(obj) {
obj.n = 3
obj.name = '測試'
}
foo(obj2)
console,log(obj1) // { n: 3, name: '測試' }
正如我們所知,對象變量僅包含該對象的內存位置指針,所以這裏 obj2 和 obj1 指向同一個對象。這意味着如果我們更改 obj2 的任何值,obj1 也會受到影響,因為本質上它們是同一個對象。同樣,當我們在函數中將對象作為參數傳遞時,傳遞的參數只包含對象指針。因此,函數可以直接修改對象而不返回任何內容,這種技術稱為通過引用傳遞。
// Q10
var foo = { n: 1 };
var bar = foo;
console.log(foo === bar); // true
foo.x = foo = { n: 2 };
console.log(foo) // { n: 2 }
console.log(bar) // { n: 1, x: { n: 2 } }
console.log(foo === bar) // false
因為對象變量只包含該對象內存位置的指針,所以當我們聲明 var bar = foo 時,foo 和 bar 都指向同一個對象。
在下一個邏輯中,foo = { n: 2 } 首先運行,其中 foo 被分配給不同對象,因此 foo 有一個指向不同對象的指針。同時,foo.x = foo 正在運行,這裏的 foo 仍然包含舊指針,所以邏輯類似於:
foo = { n: 2 }
bar.x = foo
所以 bar.x = { n: 2 },最後 foo 的值是 { n: 2 },而 bar 是 { n: 1, x: { n: 2 } }。
6. this
// Q11
const obj = {
name: "test",
prop: {
name: "prop name",
print: function(){
console.log(this.name)
},
},
print: function(){
console.log(this.name)
}
print2: () => console.log(this.name, this)
}
obj.print() // test
obj.prop.print() // prop name
obj.print2() // undefined, window global object
上面的例子展示了 this 關鍵字在一個對象中是如何工作的,this 引用執行函數中的執行上下文對象。但是,this 範圍僅在普通函數聲明中可用,在箭頭函數中不可用。
上面的例子展示了顯示綁定,例如在 object1.object2.object3.object4.print() 中,print 函數將使用最新的對象 object4 作為 this 上下文,如果 this 未綁定對象,它將回退到根對象,該對象是在調用 obj.print2() 時的 Window 全局對象。
另一方面,您還必須理解對象上下文之前已經綁定的隱式綁定,因此下一個函數執行始終使用該對象作為 this 上下文。例如:當我們使用 func.bind(<object>) 時,它將返回一個 <object> 用作新執行上下文的新函數。
7. 強制轉換
// Q12
console.log(1 + "2" + "2"); // 122
console.log(1 + +"2" + "2"); // 32
console.log(1 + -"1" + "2"); // 02
console.log(+"1" + "1" + "2"); // 112
console.log("A" - "B" + "2"); // NaN2
console.log("A" - "B" + 2); // NaN
"10,11" == [[[[10]], 11]] // true (10,11 == 10,11)
"[object Object]" == { name: "test" } true
強制轉換是最棘手的 JavaScript 問題之一。一般來説有兩條原則,第一條是,如果 2 個操作數與 + 操作符連接,則兩個操作數將首先使用 toString 方法轉變為字符串,然後連接。同時,其他運算符(如 -、* 或 /) 會將操作數更改為數字,如果它不能被強制轉換為一個數字,則返回 NAN。
如果操作數包含一個對象或數組,那就更棘手了。任何對象的 toString 方法返回的都是 "[object Object]",但在數組中,該 toString 方法將返回由逗號分隔的基礎值。
注意: == 表示允許強制轉換,而 === 不允許。
8. 異步
// Q13
console.log(1);
new Promise(resolve => {
console.log(2);
return setTimeout(() => {
console.log(3);
resolve();
}, 0)
})
setTimeout(function() { console.log(4) }, 1000);
setTimeout(function() { console.log(5) }, 0);
console.log(6);
// 1
// 2
// 6
// 3
// 5
// 4
在這裏,你需要知道事件循環、宏任務和微任務隊列是如何工作的。您可以在此處查看這篇文章,這裏深入探討了這些概念。一般情況下,異步函數在所用同步函數執行完後才執行。
// Q14
async function foo() {
return 10;
}
console.log(foo()) // Promise{ <fulfilled>: 10 }
一旦函數聲明為 async,它總是返回一個 Promise,無論內部邏輯是同步的還異步的。
// Q15
const delay = async (item) => new Promise(
resolve => setTimeout(() => {
console.log(item);
resolve(item);
}, Math.random() * 100)
)
console.log(1)
let arr = [3, 4, 5, 6]
arr.forEach(async item => await delay(item)))
console.log(2)
forEach 函數總是同步的,不管每個循環是同步的還是異步的,這意味着每個循環都不會等待另一個。如果要依次執行每個循環並相互等待,可以改用 for of。
9. 函數
// Q16
if(function f(){}) {
console.log(f)
}
// error: ReferenceError: f is not defined
在上面的例子中,if 條件被滿足,因為函數聲明被認為是一個真值。但是,內部塊無法訪問函數聲明,因為它們具有不同的塊作用域。
// Q17
function foo() {
return
{ name: 2 }
}
foo() // 返回 undefined
由於自動分號插入(ASI)機制,return 語句將以分號結束,並且分號下面的所有內容都不會運行。
// Q18
function foo(a, b, a) { return a + b }
console.log(foo(1, 2, 3)) // 3+2 = 5
function foo2(a, b, c = a) { return a + b + c }
console.log(foo(1, 2)) // 1+2+1 = 4
function foo3(a = b, b) { return a + b }
console.log(foo3(1, 2)) // 1+2 = 3
console.log(foo3(undefined, 2)) // 錯誤
前三次執行的很清楚,但是最後一個函數執行會報錯,因為 b 在聲明之前就被使用了,類似於這樣:
let a = b;
let b = 2;
10. 原型
// Q19
function Persion() {}
Persion.prototype.walk = function() {
return this
}
Persion.run = function() {
return this
}
let user = new Persion();
let walk = user.walk;
console.log(walk()) // window object
console.log(user.walk()) // user object
let run = Persion.run;
console.log(run()); // window object
console.log(user.run()); // TypeError: user.run is not a function
原型是存在於每個變量中的對象,用於從其父對象繼承特性。例如,當您聲明一個字符串變量時,該字符串變量具有一個繼承自 String.prototype 的原型,這就是為什麼您可以在字符串變量中調用字符串方法的原因,例如 string.replace(), string.substring() 等。
在上面的示例中,我們將 walk 函數分配給 Persion 函數的原型,並將 run 函數分配給函數對象。這是兩個不同的對象,函數使用 new 關鍵字創建的每個對象都將從函數原型而不是函數對象上繼承方法。但是請記住,如果我們將該函數分配給一個變量 ,如 let walk = user.walk,該函數將忘記使用 user 作為執行上下文,而是返回到 Window 對象上。
原文:https://medium.com/@andreassu...