將會學習到一些原理相關的知識,不會解釋涉及到的知識點的作用及用法,如果大家對於這些內容還不怎么熟悉,推薦先去學習相關的知識點內容再來學習原理知識。
手寫 call、apply 及 bind 函數
涉及面試題:call、apply 及 bind 函數內部實現是怎么樣的?
首先從以下幾點來考慮如何實現這幾個函數
- 不傳入第一個參數,那么上下文默認為
window
- 改變了
this
指向,讓新的對象可以執行該函數,並能接受參數
那么我們先來實現 call
Function.prototype.myCall = function(context) { if (typeof this !== 'function') { throw new TypeError('Error') } context = context || window context.fn = this const args = [...arguments].slice(1) const result = context.fn(...args) delete context.fn return result }
以下是對實現的分析:
- 首先
context
為可選參數,如果不傳的話默認上下文為window
- 接下來給
context
創建一個fn
屬性,並將值設置為需要調用的函數 - 因為
call
可以傳入多個參數作為調用函數的參數,所以需要將參數剝離出來 - 然后調用函數並將對象上的函數刪除
以上就是實現 call
的思路,apply
的實現也類似,區別在於對參數的處理,所以就不一一分析思路了
Function.prototype.myApply = function(context) { if (typeof this !== 'function') { throw new TypeError('Error') } context = context || window context.fn = this let result // 處理參數和 call 有區別 if (arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result }
bind
的實現對比其他兩個函數略微地復雜了一點,因為 bind
需要返回一個函數,需要判斷一些邊界問題,以下是 bind
的實現
Function.prototype.myBind = function (context) { if (typeof this !== 'function') { throw new TypeError('Error') } const _this = this const args = [...arguments].slice(1) // 返回一個函數 return function F() { // 因為返回了一個函數,我們可以 new F(),所以需要判斷 if (this instanceof F) { return new _this(...args, ...arguments) } return _this.apply(context, args.concat(...arguments)) } }
以下是對實現的分析:
- 前幾步和之前的實現大相徑庭,就不贅述了
bind
返回了一個函數,對於函數來說有兩種方式調用,一種是直接調用,一種是通過new
的方式,我們先來說直接調用的方式- 對於直接調用來說,這里選擇了
apply
的方式實現,但是對於參數需要注意以下情況:因為bind
可以實現類似這樣的代碼f.bind(obj, 1)(2)
,所以我們需要將兩邊的參數拼接起來,於是就有了這樣的實現args.concat(...arguments)
- 最后來說通過
new
的方式,在之前的章節中我們學習過如何判斷this
,對於new
的情況來說,不會被任何方式改變this
,所以對於這種情況我們需要忽略傳入的this
new
涉及面試題:new 的原理是什么?通過 new 的方式創建對象和通過字面量創建有什么區別?
在調用 new
的過程中會發生以上四件事情:
- 新生成了一個對象
- 鏈接到原型
- 綁定 this
- 返回新對象
根據以上幾個過程,我們也可以試着來自己實現一個 new
function create() { let obj = {} let Con = [].shift.call(arguments) obj.__proto__ = Con.prototype let result = Con.apply(obj, arguments) return result instanceof Object ? result : obj }
以下是對實現的分析:
- 創建一個空對象
- 獲取構造函數
- 設置空對象的原型
- 綁定
this
並執行構造函數 - 確保返回值為對象
對於對象來說,其實都是通過 new
產生的,無論是 function Foo()
還是 let a = { b : 1 }
。
對於創建一個對象來說,更推薦使用字面量的方式創建對象(無論性能上還是可讀性)。因為你使用 new Object()
的方式創建對象需要通過作用域鏈一層層找到 Object
,但是你使用字面量的方式就沒這個問題。
function Foo() {} // function 就是個語法糖 // 內部等同於 new Function() let a = { b: 1 } // 這個字面量內部也是使用了 new Object()
instanceof 的原理
涉及面試題:instanceof 的原理是什么?
instanceof
可以正確的判斷對象的類型,因為內部機制是通過判斷對象的原型鏈中是不是能找到類型的 prototype
。
我們也可以試着實現一下 instanceof
function myInstanceof(left, right) { let prototype = right.prototype left = left.__proto__ while (true) { if (left === null || left === undefined) return false if (prototype === left) return true left = left.__proto__ } }
以下是對實現的分析:
- 首先獲取類型的原型
- 然后獲得對象的原型
- 然后一直循環判斷對象的原型是否等於類型的原型,直到對象原型為
null
,因為原型鏈最終為null
為什么 0.1 + 0.2 != 0.3
涉及面試題:為什么 0.1 + 0.2 != 0.3?如何解決這個問題?
先說原因,因為 JS 采用 IEEE 754 雙精度版本(64位),並且只要采用 IEEE 754 的語言都有該問題。
我們都知道計算機是通過二進制來存儲東西的,那么 0.1
在二進制中會表示為
// (0011) 表示循環 0.1 = 2^-4 * 1.10011(0011)
我們可以發現,0.1
在二進制中是無限循環的一些數字,其實不只是 0.1
,其實很多十進制小數用二進制表示都是無限循環的。這樣其實沒什么問題,但是 JS 采用的浮點數標准卻會裁剪掉我們的數字。
IEEE 754 雙精度版本(64位)將 64 位分為了三段
- 第一位用來表示符號
- 接下去的 11 位用來表示指數
- 其他的位數用來表示有效位,也就是用二進制表示
0.1
中的10011(0011)
那么這些循環的數字被裁剪了,就會出現精度丟失的問題,也就造成了 0.1
不再是 0.1
了,而是變成了 0.100000000000000002
0.100000000000000002 === 0.1 // true
那么同樣的,0.2
在二進制也是無限循環的,被裁剪后也失去了精度變成了 0.200000000000000002
0.200000000000000002 === 0.2 // true
所以這兩者相加不等於 0.3
而是 0.300000000000000004
0.1 + 0.2 === 0.30000000000000004 // true
那么可能你又會有一個疑問,既然 0.1
不是 0.1
,那為什么 console.log(0.1)
卻是正確的呢?
因為在輸入內容的時候,二進制被轉換為了十進制,十進制又被轉換為了字符串,在這個轉換的過程中發生了取近似值的過程,所以打印出來的其實是一個近似值,你也可以通過以下代碼來驗證
console.log(0.100000000000000002) // 0.1
那么說完了為什么,最后來說說怎么解決這個問題吧。其實解決的辦法有很多,這里我們選用原生提供的方式來最簡單的解決問題
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true
垃圾回收機制
涉及面試題:V8 下的垃圾回收機制是怎么樣的?
V8 實現了准確式 GC,GC 算法采用了分代式垃圾回收機制。因此,V8 將內存(堆)分為新生代和老生代兩部分。
新生代算法
新生代中的對象一般存活時間較短,使用 Scavenge GC 算法。
在新生代空間中,內存空間分為兩部分,分別為 From 空間和 To 空間。在這兩個空間中,必定有一個空間是使用的,另一個空間是空閑的。新分配的對象會被放入 From 空間中,當 From 空間被占滿時,新生代 GC 就會啟動了。算法會檢查 From 空間中存活的對象並復制到 To 空間中,如果有失活的對象就會銷毀。當復制完成后將 From 空間和 To 空間互換,這樣 GC 就結束了。
老生代算法
老生代中的對象一般存活時間較長且數量也多,使用了兩個算法,分別是標記清除算法和標記壓縮算法。
在講算法前,先來說下什么情況下對象會出現在老生代空間中:
- 新生代中的對象是否已經經歷過一次 Scavenge 算法,如果經歷過的話,會將對象從新生代空間移到老生代空間中。
- To 空間的對象占比大小超過 25 %。在這種情況下,為了不影響到內存分配,會將對象從新生代空間移到老生代空間中。
老生代中的空間很復雜,有如下幾個空間
enum AllocationSpace { // TODO(v8:7464): Actually map this space's memory as read-only. RO_SPACE, // 不變的對象空間 NEW_SPACE, // 新生代用於 GC 復制算法的空間 OLD_SPACE, // 老生代常駐對象空間 CODE_SPACE, // 老生代代碼對象空間 MAP_SPACE, // 老生代 map 對象 LO_SPACE, // 老生代大空間對象 NEW_LO_SPACE, // 新生代大空間對象 FIRST_SPACE = RO_SPACE, LAST_SPACE = NEW_LO_SPACE, FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE, LAST_GROWABLE_PAGED_SPACE = MAP_SPACE };
在老生代中,以下情況會先啟動標記清除算法:
- 某一個空間沒有分塊的時候
- 空間中被對象超過一定限制
- 空間不能保證新生代中的對象移動到老生代中
在這個階段中,會遍歷堆中所有的對象,然后標記活的對象,在標記完成后,銷毀所有沒有被標記的對象。在標記大型對內存時,可能需要幾百毫秒才能完成一次標記。這就會導致一些性能上的問題。為了解決這個問題,2011 年,V8 從 stop-the-world 標記切換到增量標志。在增量標記期間,GC 將標記工作分解為更小的模塊,可以讓 JS 應用邏輯在模塊間隙執行一會,從而不至於讓應用出現停頓情況。但在 2018 年,GC 技術又有了一個重大突破,這項技術名為並發標記。該技術可以讓 GC 掃描和標記對象時,同時允許 JS 運行,你可以點擊 該博客 詳細閱讀。
清除對象后會造成堆內存出現碎片的情況,當碎片超過一定限制后會啟動壓縮算法。在壓縮過程中,將活的對象像一端移動,直到所有對象都移動完成然后清理掉不需要的內存。
小結
以上就是 JS 進階知識點的內容了,這部分的知識相比於之前的內容更加深入也更加的理論,也是在面試中能夠於別的候選者拉開差距的一塊內容。如果大家對於這個章節的內容存在疑問,歡迎在評論區與我互動。