面試小冊:面試官經常問的十個棘手的 JavaScript 問題

破曉L發表於2023-02-08

image-20230207094547683.png

1. 可變性

在 JavaScript 中有七種基本資料型別(stringnumberbooleanundefinedsymbolbigintnull),這些都是不可變的。這意味著一旦分配了一個值,我們就無法修改它們,我們可以做的是將它重新分配給一個不同的值(不同的記憶體指標)。另一方面,其他資料型別(如 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 是函式作用域變數,而 letconst 是塊級作用域變數,只有 var 能被提升,這意味著變數宣告總是被移動到頂部。由於提升,您甚至可以在使用 var 關鍵字宣告變數之前分配、呼叫或使用該變數。

letconst 不能被提升,因為它啟用了 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 是塊級作用域變數。雖然看起來 ab 都是使用 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: '測試' }

正如我們所知,物件變數僅包含該物件的記憶體位置指標,所以這裡 obj2obj1 指向同一個物件。這意味著如果我們更改 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 時,foobar 都指向同一個物件。

在下一個邏輯中,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...

相關文章