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
物件上。