1. 程式人生 > >前端面試大全:JS 基礎知識點及常考面試題(一)

前端面試大全:JS 基礎知識點及常考面試題(一)

(內容同步自小鄒的頭條號:滬漂程式設計師的生活史)

å端é¢è¯å¤§å¨ï¼JS åºç¡ç¥è¯ç¹å常èé¢è¯é¢ï¼ä¸ï¼

原始(Primitive)型別

涉及面試題:原始型別有哪幾種?null 是物件嘛?

在 JS 中,存在著 6 種原始值,分別是:

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol

首先原始型別儲存的都是值,是沒有函式可以呼叫的,比如 undefined.toString()

前端面試大全:JS 基礎知識點及常考面試題(一)

此時你肯定會有疑問,這不對呀,明明 '1'.toString() 是可以使用的。其實在這種情況下,'1' 已經不是原始型別了,而是被強制轉換成了 String 型別也就是物件型別,所以可以呼叫 toString 函式。

除了會在必要的情況下強轉型別以外,原始型別還有一些坑。

其中 JS 的 number 型別是浮點型別的,在使用中會遇到某些 Bug,比如 0.1 + 0.2 !== 0.3,但是這一塊的內容會在進階部分講到。string 型別是不可變的,無論你在 string 型別上呼叫何種方法,都不會對值有改變。

另外對於 null 來說,很多人會認為他是個物件型別,其實這是錯誤的。雖然 typeof null 會輸出 object,但是這只是 JS 存在的一個悠久 Bug。在 JS 的最初版本中使用的是 32 位系統,為了效能考慮使用低位儲存變數的型別資訊,000 開頭代表是物件,然而 null 表示為全零,所以將它錯誤的判斷為 object 。雖然現在的內部型別判斷程式碼已經改變了,但是對於這個 Bug 卻是一直流傳下來。

物件(Object)型別

涉及面試題:物件型別和原始型別的不同之處?函式引數是物件會發生什麼問題?

在 JS 中,除了原始型別那麼其他的都是物件型別了。物件型別和原始型別不同的是,原始型別儲存的是值,物件型別儲存的是地址(指標)。當你建立了一個物件型別的時候,計算機會在記憶體中幫我們開闢一個空間來存放值,但是我們需要找到這個空間,這個空間會擁有一個地址(指標)。

const a = []

對於常量 a 來說,假設記憶體地址(指標)為 #001,那麼在地址 #001 的位置存放了值 [],常量 a存放了地址(指標) #001,再看以下程式碼

const a = []
const b = a
b.push(1)

當我們將變數賦值給另外一個變數時,複製的是原本變數的地址(指標),也就是說當前變數 b 存放的地址(指標)也是 #001,當我們進行資料修改的時候,就會修改存放在地址(指標) #001 上的值,也就導致了兩個變數的值都發生了改變。

接下來我們來看函式引數是物件的情況

function test(person) {
 person.age = 26
 person = {
 name: 'yyy',
 age: 30
 }
 return person
}
const p1 = {
 name: 'yck',
 age: 25
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?

對於以上程式碼,你是否能正確的寫出結果呢?接下來讓我為了解析一番:

  • 首先,函式傳參是傳遞物件指標的副本
  • 到函式內部修改引數的屬性這步,我相信大家都知道,當前 p1 的值也被修改了
  • 但是當我們重新為了 person 分配了一個物件時就出現了分歧,請看下圖

前端面試大全:JS 基礎知識點及常考面試題(一)

所以最後 person 擁有了一個新的地址(指標),也就和 p1 沒有任何關係了,導致了最終兩個變數的值是不相同的。

typeof vs instanceof

涉及面試題:typeof 是否能正確判斷型別?instanceof 能正確判斷物件的原理是什麼?

typeof 對於原始型別來說,除了 null 都可以顯示正確的型別

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

typeof 對於物件來說,除了函式都會顯示 object,所以說 typeof 並不能準確判斷變數到底是什麼型別

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

如果我們想判斷一個物件的正確型別,這時候可以考慮使用 instanceof,因為內部機制是通過原型鏈來判斷的,在後面的章節中我們也會自己去實現一個 instanceof。

const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true

對於原始型別來說,你想直接通過 instanceof 來判斷型別是不行的,當然我們還是有辦法讓 instanceof 判斷原始型別的

class PrimitiveString {
 static [Symbol.hasInstance](x) {
 return typeof x === 'string'
 }
}
console.log('hello world' instanceof PrimitiveString) // true

你可能不知道 Symbol.hasInstance 是什麼東西,其實就是一個能讓我們自定義 instanceof 行為的東西,以上程式碼等同於 typeof 'hello world' === 'string',所以結果自然是 true 了。這其實也側面反映了一個問題, instanceof 也不是百分之百可信的。

型別轉換

涉及面試題:該知識點常在筆試題中見到,熟悉了轉換規則就不懼怕此類題目了。

首先我們要知道,在 JS 中型別轉換隻有三種情況,分別是:

  • 轉換為布林值
  • 轉換為數字
  • 轉換為字串

我們先來看一個型別轉換表格,然後再進入正題

前端面試大全:JS 基礎知識點及常考面試題(一)

 

轉Boolean

在條件判斷時,除了 undefined, null, false, NaN, '', 0, -0,其他所有值都轉為 true,包括所有物件。

物件轉原始型別

物件在轉換型別的時候,會呼叫內建的 [[ToPrimitive]] 函式,對於該函式來說,演算法邏輯一般來說如下:

  • 如果已經是原始型別了,那就不需要轉換了
  • 呼叫 x.valueOf(),如果轉換為基礎型別,就返回轉換的值
  • 呼叫 x.toString(),如果轉換為基礎型別,就返回轉換的值
  • 如果都沒有返回原始型別,就會報錯

當然你也可以重寫 Symbol.toPrimitive ,該方法在轉原始型別時呼叫優先順序最高。

let a = {
 valueOf() {
 return 0
 },
 toString() {
 return '1'
 },
 [Symbol.toPrimitive]() {
 return 2
 }
}
1 + a // => 3

四則運算子

加法運算子不同於其他幾個運算子,它有以下幾個特點:

  • 運算中其中一方為字串,那麼就會把另一方也轉換為字串
  • 如果一方不是字串或者數字,那麼會將它轉換為數字或者字串
1 + '1' // '11'
true + true // 2
4 + [1,2,3] // "41,2,3"

如果你對於答案有疑問的話,請看解析:

  • 對於第一行程式碼來說,觸發特點一,所以將數字 1 轉換為字串,得到結果 '11'
  • 對於第二行程式碼來說,觸發特點二,所以將 true 轉為數字 1
  • 對於第三行程式碼來說,觸發特點二,所以將陣列通過 toString 轉為字串 1,2,3,得到結果 41,2,3

另外對於加法還需要注意這個表示式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"

因為 + 'b' 等於 NaN,所以結果為 "aNaN",你可能也會在一些程式碼中看到過 + '1' 的形式來快速獲取 number 型別。

那麼對於除了加法的運算子來說,只要其中一方是數字,那麼另一方就會被轉為數字

4 * '3' // 12
4 * [] // 0
4 * [1, 2] // NaN

比較運算子

  1. 如果是物件,就通過 toPrimitive 轉換物件
  2. 如果是字串,就通過 unicode 字元索引來比較
let a = {
 valueOf() {
 return 0
 },
 toString() {
 return '1'
 }
}
a > -1 // true

在以上程式碼中,因為 a 是物件,所以會通過 valueOf 轉換為原始型別再比較值。

this

涉及面試題:如何正確判斷 this?箭頭函式的 this 是什麼?

this 是很多人會混淆的概念,但是其實它一點都不難,只是網上很多文章把簡單的東西說複雜了。在這一小節中,你一定會徹底明白 this 這個概念的。

我們先來看幾個函式呼叫的場景

function foo() {
 console.log(this.a)
}
var a = 1
foo()
const obj = {
 a: 2,
 foo: foo
}
obj.foo()
const c = new foo()

接下來我們一個個分析上面幾個場景

  • 對於直接呼叫 foo 來說,不管 foo 函式被放在了什麼地方,this 一定是 window
  • 對於 obj.foo() 來說,我們只需要記住,誰呼叫了函式,誰就是 this,所以在這個場景下 foo函式中的 this 就是 obj 物件
  • 對於 new 的方式來說,this 被永遠繫結在了 c 上面,不會被任何方式改變 this

說完了以上幾種情況,其實很多程式碼中的 this 應該就沒什麼問題了,下面讓我們看看箭頭函式中的 this

function a() {
 return () => {
 return () => {
 console.log(this)
 }
 }
}
console.log(a()()())

首先箭頭函式其實是沒有 this 的,箭頭函式中的 this 只取決包裹箭頭函式的第一個普通函式的 this。在這個例子中,因為包裹箭頭函式的第一個普通函式是 a,所以此時的 this 是 window。另外對箭頭函式使用 bind 這類函式是無效的。

最後種情況也就是 bind 這些改變上下文的 API 了,對於這些函式來說,this 取決於第一個引數,如果第一個引數為空,那麼就是 window。

那麼說到 bind,不知道大家是否考慮過,如果對一個函式進行多次 bind,那麼上下文會是什麼呢?

let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?

如果你認為輸出結果是 a,那麼你就錯了,其實我們可以把上述程式碼轉換成另一種形式

// fn.bind().bind(a) 等於
let fn2 = function fn1() {
 return function() {
 return fn.apply()
 }.apply(a)
}
fn2()

可以從上述程式碼中發現,不管我們給函式 bind 幾次,fn 中的 this 永遠由第一次 bind 決定,所以結果永遠是 window。

let a = { name: 'yck' }
function foo() {
 console.log(this.name)
}
foo.bind(a)() // => 'yck'

以上就是 this 的規則了,但是可能會發生多個規則同時出現的情況,這時候不同的規則之間會根據優先順序最高的來決定 this 最終指向哪裡。

首先,new 的方式優先順序最高,接下來是 bind 這些函式,然後是 obj.foo() 這種呼叫方式,最後是 foo 這種呼叫方式,同時,箭頭函式的 this 一旦被繫結,就不會再被任何方式所改變。

如果你還是覺得有點繞,那麼就看以下的這張流程圖吧,圖中的流程只針對於單個規則。

前端面試大全:JS 基礎知識點及常考面試題(一)

 

小結

以上就是我們 JS 基礎知識點的第一部分內容了。這一小節中涉及到的知識點在我們日常的開發中經常可以看到,並且很多容易出現的坑 也出自於這些知識點,相信認真讀完的你一定會在日後的開發中少踩很多坑。