前端面試大全:JS 基礎知識點及常考面試題(一)
(內容同步自小鄒的頭條號:滬漂程式設計師的生活史)
原始(Primitive)型別
涉及面試題:原始型別有哪幾種?null 是物件嘛?
在 JS 中,存在著 6 種原始值,分別是:
- boolean
- null
- undefined
- number
- string
- symbol
首先原始型別儲存的都是值,是沒有函式可以呼叫的,比如 undefined.toString()
此時你肯定會有疑問,這不對呀,明明 '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 分配了一個物件時就出現了分歧,請看下圖
所以最後 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 中型別轉換隻有三種情況,分別是:
- 轉換為布林值
- 轉換為數字
- 轉換為字串
我們先來看一個型別轉換表格,然後再進入正題
轉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
比較運算子
- 如果是物件,就通過 toPrimitive 轉換物件
- 如果是字串,就通過 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 基礎知識點的第一部分內容了。這一小節中涉及到的知識點在我們日常的開發中經常可以看到,並且很多容易出現的坑 也出自於這些知識點,相信認真讀完的你一定會在日後的開發中少踩很多坑。