面試 - 2024 版
誒好,事情是這樣的,雖說公司大裁員沒裁到我,但如履薄冰的感覺還是頗令人不安,所以我出去逛了一圈蒐集了一些面試問題😅。
以前唸書時有句話說:「自己的考古題自己拿」,我覺得這句話也好適用在前端工程師的面試上喔!
總而言之啦,現在我又上岸了 (是說原本也沒掉下海啦),就稍微整理一下這些問題囉。
HTML/CSS
Block element 與 Inline element
請問 <a href="..." style="width: 100px;" />
為何樣式沒有作用?
因為 <a>
tag 預設是 inline elements,對於 inline elements 來說,設定 width 是不起作用的,真要設定的話可以把它改成 block element。
px
、em
、rem
px
:像素(Pixel),是絕對單位,直接指定元素的大小。em
:相對單位,基於父元素的字體大小(font-size)。例如,父元素的字體大小(font-size)是16px
,1em
就等於16px
。rem
:相對單位,基於根元素(html
元素)的字體大小(font-size)。因此,1rem
等於根元素的字體大小。
使用 em
和 rem
的主要區別在於它們的基準不同,em
基於父元素字體大小,而 rem
始終基於根元素字體大小。這使得 rem
更加可控和一致性,特別適合用於整體版面和排版的調整。
JavaScript
作用域
var abc1 = '123'
function show(){
var abc2 = '456'
alert(abc1 + '&' + abc2)
}
show()
alert(abc1 + '&' + abc2)
答案會是:
show()
// 123&456
alert(abc1 + '&' + abc2)
// reference error
這題跟變數的作用域有關,abc1 為全域變數,abc2 則只被宣告在 show()
之中,所以當第二段 alert(abc1 + '&' + abc2)
執行時,其實是抓不到只在函式內部存活的 abc2,因此會報錯 reference error。
作用域與連續賦值
var b = 1
function print(){
var b = a = 2
console.log(a, b)
}
print()
console.log(a, b)
答案如下:
print()
// 2, 2
console.log(a, b)
// 2, 1
這題考變數作用域與連續賦值,主要關注點其實是在 print()
內部的 var b = a = 2
。
我們可以把 var b = a = 2
做的事拆解成下列步驟:
- 已知有一全域變數 b 被宣告為 1。
- 在
print()
中,a = 2
由於沒有使用var
、let
或const
等關鍵字,a 會成為全域變數。 - 在
print()
中宣告一區域變數 b (並不等同全域變數 b) 並令其值為 2。
所以在 print()
中,取到的 b 為函式變數的 b,故為 2。
在 console.log(a, b)
中取到的 b 則為全域變數 b,故為 1。
全域變數與函式內變數
呈上,如何讓結果出現 (2, 2)、(1, 1) 答案是:
var b = 1
var a = 1
function print(){
var b = 2
var a = 2
console.log(a, b)
}
print()
console.log(a, b)
其實就是各自宣告一個全域變數 a 以及一個函式內變數 a。
物件記憶體取值
var foo = {n: 1}
var bar = foo
foo.x = foo = {n: 2}
答案是:
console.log(foo)
// {n: 2}
console.log(bar)
// {n: 1, x: {n: 2}}
來解析一下這題:
var bar = foo
把變數 bar 的記憶位置指向 foo。foo.x = foo = {n: 2}
的步驟可以拆成:
- foo 的記憶體位置新增一個
x = {n: 2}
。 - 因為 bar 指向 foo 的記憶體位置,所以也有 bar.x 同樣為
{n: 2}
。 - 接著後半段
foo = {n: 2}
把 foo 指向一個新的記憶體位置使之為{n: 2}
。 本題算是陷阱題,考物件的記憶體取值以及連續賦值觀念。
bind
、apply
、call
(待 補充)
(待補充)
var
、const
、let
-
作用域:
- 函式作用域(Function Scope):
var
使用的是函式作用域,變數在函式內部聲明,函式外部無法訪問。- 若多次重複聲明變數,會在同一函式內產生變數覆蓋,容易導致變數污染。
- 區塊作用域(Block Scope):
const
和let
使用的是區塊作用域,變數在最近的區塊內有效,如{}
大括號內。- 區塊作用域變數僅在區塊內可見,不會影響外部範圍,避免了變數污染問題。
- 函式作用域(Function Scope):
-
變數提升:
var
會被提升到作用域頂部,但初始化在提升後進行,可能導致不直觀的行為。const
和let
也會被提升,但在初始化前會進入暫時性死區(TDZ),使用前必須先聲明。
-
重新賦值:
const
宣告後不可重新賦值,適用於常量。let
可以重新賦值,適用於需要改變的變數。
函式作用域:
- 在函式內部宣告的變數只能在該函式內部訪問,函式結束後,變數將被銷毀。
- 因為
var
使用的是函式作用域,同一函式內的變數重複聲明會覆蓋之前的值,導致變數污染。
區塊作用域:
- 在任意一對大括號
{}
內部宣告的變數只能在該區塊內部訪問,區塊結束後,變數將被銷毀。 const
和let
使用的是區塊作用域,即使在同一函式內部,不同區塊的變數也不會互相影響,避免了變數污染問題。
暫時性死區(Temporal Dead Zone, TDZ的簡單例子如下:
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 3;
簡單來說,暫時性死區是變數提升的一部分,但在變數實際聲明之前,不能對其進行讀取或寫入操作。
箭頭函式與宣告函式
基本上箭頭函式可以視作宣告函式的簡寫,但他們在特定情況使用上還是略有差別,最大的差別在於 this
的指向性:
- 宣告函式:
this
指向呼叫函式的作用域,因此this
的指向是會變動的。 - 箭頭函示:
this
指向一開始創建函式的父作用域,因此函式一旦創建,this
的指向就固定了。
window.name = 'global name';
const functions = {
name: 'functions name',
printName: function() {
console.log(this.name);
},
arrowPrintName: () => {
console.log(this);
console.log(this.name);
}
}
const obj = {
name: 'obj name',
objPrint: functions.printName,
objArrowPrint: functions.arrowPrintName
}
const obj2 = {
name: 'obj2 name',
objPrint: functions.printName,
objArrowPrint: functions.arrowPrintName
}
obj.objPrint(); // obj name
obj.objArrowPrint(); // global name
obj2.objPrint(); // obj2 name
obj2.objArrowPrint(); // global name
call by value 與 call by reference
call by value:傳入的參數是一個副本,函式對它的操作不會影響到原本的值。 call by reference:傳入的參數是值的本身,所以函式對參數的操作會直接影響到原先的 值。
// ex - call by value
const a = 1
const callByValue = a => a++
callByValue(a)
console.log(a) // 1
// ex - call by reference
const obj = { a: 1 }
const callByReference = obj => obj.a++
callByReference(obj)
console.log(obj.a) // 2
閉包(Closure)?
閉包是指定義一個函式時,如果在函式內部引用了外部的變量,那麼這個函式就是一個閉包。
onst sumNumber = (a => {
return b => a + b
})
const sum10 = sumNumber(10)
console.log(sum10(5)) // 15
具體來說上述在 sum10 = sumNumber(10)
時記憶了一個 a 為 10,之後在 sum10(5)
時傳入了一個 b 為 5。
async / await?
本質上 async/await 就是基於 promise 的語法糖,只是這種寫法可以讓我們用類似同步的寫法來撰寫非同步的內容。
具體來說 async/await 寫出看似同步的語法,是透過在函式一開始用 async 定義一個非同步函式,並在內部使用 await 等待 promise 執行完畢 (A函式),然後再去執行下一個動作 (B函式)。
另外在 async/await 中,我們可以透過 try 來處理 promise 操作成功的情況,然後靠catch 捕獲錯誤結果。
AJAX
AJAX 基本上可以說是最早串接 api 的方法,現在的 fetch 或 axios 基本都衍伸 自 AJAX 技術,是 AJAX 的出現才讓網頁開始可以實現非同步請求,讓網頁可以在不重新整理的情況向 server 請求資料。
使用時機基本就是向 server 請求資源時使用,比如點擊某按鈕時取得一筆資料來做渲染。
效能與安全性
XSS 攻擊
跨站腳本攻擊是指網站被惡意注入一段腳本,該腳本會在其他使用者操作該網站時被執行,繼而讓攻擊者有機會做出一些比如竊取資料等惡意操作。
XSS 攻擊主要有三種方式:
- stored XSS:比如透過部落格留言板去把腳本存到受攻擊方的後端資料庫中,當下一個使用者使用此受攻擊網站時便會因為網頁載入了這段腳本而遭受攻擊。
- DOM XSS:攻擊者透過 JavaScript 惡意操作網頁的 DOM。
- reflected XSS:比如有個網站網址 (http://example.com) 被惡意嵌入一段腳本 (
<script>alert('xss atack')</script>
) 變成類似這樣http://example.com?search=<script>alert('xss atack')</script>
,這個網址再被傳播出去,當其他使用者點擊時就會觸發這個腳本,以這個例子來說會彈出一個警告視窗。
XSS 的防範上主要體現在前後端是否對用戶輸入資料進行驗證管理,將不符合規範的資料剃除。
以前端來講可以透過正規表達式來檢驗使用者輸入的資料或將輸入 的資料統一轉成字串。
目前因為前端開發多會使用一些著名 library 或 framework,如 React、Vue 等,這些 library 或框架都自帶一定的 XSS 防範規則。
Browser rendering
- layout 階段:瀏覽器計算每個 DOM 元素的位置。因為涉及瀏覽器計算元素的位置與大小,此階段消耗最多效能。
- painting 階段:在 layout 確認好各元素的位置後,此階段開始將元素的樣式 (ex - 顏色、外框、文字樣式) 渲染到瀏覽器上。在三個階段中,消耗效能中等。
- compositing:將不同的渲染層合併在一起顯示在瀏覽器上。因為單純只是合併不同渲染層,所以效能消耗是最低的。
打個比方來說,現在有一幅立體畫,拆分成三個部分 (渲染層):大地、天空、山脈。
- 在 layout 階段就是確認大地上的樹要長多高、長哪些位置;天空中的白雲在天空哪個位置、大小多大。
- 之後在 painting 階段開始為大地上的樹木著色、幫天空的雲朵以及山脈中的高山繪製陰影。
- 最後在 compositing 階段,開始把這三個部分拼成完整的立體畫,決定大地之上是山脈,山脈之上是天空。