1.數據類型
1.1概念篇
7種原始數據類型
- boolean
- string
- number
- undefined
- null
- Symbol
- bigInt
引用類型
- 對象Object
- 普通對象-Object
- 數組對象-Array
- 正則對象-RegExp
- 日期對象-Date
- 數學函數-Math
- 函數對象-Function
- 基本包裝類型 Boolean String Number
null是對象嗎?為什么?
- 結論: null不是對象
- 解釋: 雖然 typeof null 會輸出 object,但是這只是 JS 存在的一個悠久 Bug。JS在運行之前編譯成二進制形式,在 JS 的最初版本中使用的是 32 位系統,為了性能考慮使用低位存儲變量的類型信息,000 開頭代表是對象然而 null 表示為全零,所以將它錯誤的判斷為 object 。
1 .toString()或者(1).toString為什么可以調用?
- 數字后面的第一個點會被解釋為小數點,而不是點調用。只不過不推薦這種使用方法,而且這樣做也沒什么意義
- 基本包裝類型:為什么基本類型卻可以直接調用引用類型的方法呢?其實是js引擎在解析上面的語句的時候,會把這三種基本類型解析為包裝對象(就是下面的new String()),而包裝對象是引用類型可以調用Object.prototype上的方法。大概過程如下:
0.1+0.2為什么不等于0.3?
- 在JS中數字采用的IEEE 754的雙精度標準進行存儲,到這里我們都理解只要采取IEEE 754 FP的浮點數編碼的語言均會出現上述問題,只是它們的標準類庫已經為我們提供了解決方案而已
- 而對于像0.1這樣的數值用二進制表示你就會發現無法整除,最后算下來會是 0.000110011….由于存儲空間有限(雙精度是64位存儲空間),最后根據IEEE 754的規則會舍棄后面的數值,所以我們最后就只能得到一個,此時就已經出現了精度的損失
- 簡單理解 0.1和02不能被二進制浮點數精確表示
- 在0.1 + 0.2這個式子中,0.1和0.2都是近似表示的,在他們相加的時候,兩個近似值進行了計算,導致最后得到的值是0.30000000000000004,此時對于JS來說,其不夠近似于0.3),于是就出現了0.1 + 0.2 != 0.3 這個現象
既然十進制0.1不能被二進制浮點數精確存儲,那么為什么console.log(0.1)打印出來的確確實實是0.1這個精確的值?
- 實際IEEE 754標準就是采用一套規則去近視于0。1 雖然無法精確存儲,但是可以用一個近視值去表示,比如:0.100000000000000002 ==
0.100000000000000010 // true - 當64bit的存儲空間無法存儲完整的無限循環小數,而IEEE 754 Floating-point采用round to nearest, tie to even的舍入模式,因此0.1實際存儲時的位模式是0-01111111011-1001100110011001100110011001100110011001100110011010;
解決浮點數運算精度
- 換算成整數進行運算,整數運算就不存在精度缺失問題,??我們可以把需要計算的數字升級(乘以10的n次冪)成計算機能夠精確識別的整數,等計算完成后再進行降級(除以10的n次冪),這是大部分編程語言處理精度問題常用的方法。例如:
- 但是換算也是浮點數運算的操作,同樣也會存在問題,所以解決的方法就是采用字符串形式進行換算 比如:3.14===> {times: 100, num: 314} ===>有點類似大數相加的原理
- 利用第三方庫:Math.js,decimal.js
解題思路:
二進制換算后(不會出現循環被截斷,這是前提條件,這樣換算后值落在這個區間就保證了精度無誤,所以我們想辦法使運算的數字落在這個區間 這個就是解搭問題的關鍵)由于僅位于Number.MIN_SAFE_INTEGER和Number.MAX_SAFE_INTEGER間的整數才能被精準地表示,也就是只要保證運算過程的操作數和結果均落在這個閥值內,那么運算結果就是精準無誤的
問題的關鍵落在如何將小數和極大數轉換或拆分為Number.MIN_SAFE_INTEGER至Number.MAX_SAFE_INTEGER閥值間的數了
小數轉換為整數,自然就是通過科學計數法表示,并通過右移小數點,減小冪的方式處理;(如0.000123 等價于 123 * 10-6)
Number.EPSILON實際上是 JavaScript 能夠表示的最小精度。誤差如果小于這個值,就可以認為已經沒有意義了,即不存在誤差了。
可以這樣判斷:0.1+0.2-0.3<Number.EPSILON
1.2 檢測篇
typeof 是否能正確判斷類型?
- 對于原始類型來說,除了 null 都可以調用typeof顯示正確的類型。
- 但對于引用數據類型,除了函數之外,都會顯示"object"。
instanceof能判斷基本數據類型
Object.is和===的區別?
- Object在嚴格等于的基礎上修復了一些特殊情況下的失誤,具體來說就是+0和-0(false),NaN和NaN(true)。
Object.prototype.toString
1.3 轉換篇
JS中類型轉換有哪幾種?
- 轉換成數字
- 轉換成布爾值
- 轉換成字符串
== 和 ===有什么區別?
===叫做嚴格相等,是指:左右兩邊不僅值要相等,類型也要相等,例如'1'===1的結果是false,因為一邊是string,另一邊是number。
-
==不像===那樣嚴格,對于一般情況,只要值相等,就返回true,但==還涉及一些類型轉換,它的轉換規則如下:(比較最終都是轉化為數字的)
- 兩邊的類型是否相同,相同的話就比較值的大小,例如1==2,返回false - 判斷的是否是null和undefined,是的話就返回true - 判斷的類型是否是String和Number,是的話,把String類型轉換成Number,再進行比較 - 判斷其中一方是否是Boolean,是的話就把Boolean轉換成Number,再進行比較 - 如果其中一方為Object,且另一方為String、Number或者Symbol,會將Object轉換成字符串,再進行比較
對象轉原始類型是根據什么流程運行的
- 如果存在Symbol.toPrimitive()方法,優先調用再返回
- 調用valueOf(),如果轉換為原始類型,則返回
- 調用toString(),如果轉換為原始類型,則返回
- 如果都沒有返回原始類型,會報錯
如何讓if(a == 1 && a == 2)條件成立?
- 其實就是上一個問題的應用。
var a = {
value: 0,
valueOf: function() {
this.value++;
return this.value;
}
};
console.log(a == 1 && a == 2);// true
2.拷貝
2.1 淺拷貝:shallowClone
- 一個新的對象直接拷貝已存在的對象的對象屬性的引用,即淺拷貝。(對象的屬性)
- Object.assign
- ...展開運算符
- concat淺拷貝數組
- slice淺拷貝
2.2 深拷貝:deepClone
- 深拷貝會另外拷貝一份一個一模一樣的對象,從堆內存中開辟一個新的區域存放新對象,新對象跟原對象不共享內存,修改新對象不會改到原對象。
JSON.parse(JSON.stringify());
- 無法解決循環引用的問題。舉個例子:
- 無法拷貝一寫特殊的對象,諸如 RegExp, Date, Set, Map等
- 無法拷貝函數(劃重點)
動手實現一個深拷貝
-
普通類型
- 直接返回就行
-
引用類型
- 循環引用
-
解決循環引用問題,我們可以額外開辟一個存儲空間,來存儲當前對象和拷貝對象的對應關系,當需要拷貝當前對象時,先去存儲空間中找,有沒有拷貝過這個對象,如果有的話直接返回,如果沒有的話繼續拷貝,這樣就巧妙化解的循環引用的問題。
-
可遍歷類型
- Set - Map - Array - Object
-
不可遍歷類型(考慮基本包裝類型(引用類型))
- Boolean - Number - String
- Date
- Unix時間戳:value
一個 Unix 時間戳(Unix Time Stamp),它是一個整數值,表示自1970年1月1日00:00:00 UTC(the Unix epoch)以來的毫秒數,忽略了閏秒。請注意大多數 Unix 時間戳功能僅精確到最接近的秒。
- 重新生成一個Date實例,參數傳入一個Unix
- Unix時間戳:value
- Date
Error
-
Symbol
- Object(Symbol('foo'))
- es6過后就不提倡用new 直接類似Symbol(xxx)這樣執行就行
-
WeakMap、WeakSet、ArrayBuffer對象、TypedArray視圖和DataView視圖、Float32Array、Float64Array、Int8Array
Blob、File、FileList、ImageData
拷貝函數
- lodash對函數的處理 因為拷貝函數沒有啥意義
- 函數(prototype來區分下箭頭函數和普通函數,箭頭函數是沒有prototype)
- 箭頭函數
- 我們可以直接使用eval和函數字符串來重新生成一個箭頭函數,注意這種方法是不適用于普通函數的。
- 非箭頭函數
- 分別使用正則取出函數體和函數參數,然后使用new Function ([arg1[, arg2[, ...argN]],] functionBody)構造函數重新構造一個新的函數
賦值
基本數據類型:賦值,賦值之后兩個變量互不影響
引用數據類型:賦址,兩個變量具有相同的引用,指向同一個對象,相互之間有影響
-
為什么需要淺拷貝和深拷貝?
- 對引用類型進行賦址操作,兩個變量指向同一個對象,改變變量 a 之后會影響變量 b,哪怕改變的只是對象 a 中的基本類型數據
- 通常在開發中并不希望改變變量 a 之后會影響到變量 b,這時就需要用到淺拷貝和深拷貝。
- 這就是為什么需要淺拷貝和深拷貝的緣由,因為我們在賦值操作時候,操作引用類型的時候不想b影響a,所以需要淺拷貝或者深拷貝,這也從中可以看出賦值與拷貝深拷貝的區別
- 通常深淺拷貝是解決引用類型之間互相影響的,要明白這點
??當我們進行賦值,考慮到引用類型賦值完做修改會相互影響,就引出了對應的深淺拷貝方案去解決
this指向
其實JS中的this是一個非常簡單的東西,只需要理解它的??執行規則就行
顯示綁定
- call
- apply
- bind
隱式綁定
-
全局上下文
- 全局上下文默認this指向window, 嚴格模式下指向undefined。
-
直接調用函數
- this相當于全局上下文的情況
-
對象.方法的形式調用
- 誰調用這個方法,它就指向誰
-
DOM事件綁定(特殊)
- onclick和addEventerListener中 this 默認指向綁定事件的元素。
IE比較奇異,使用attachEvent,里面的this默認指向window。
-
new構造函數綁定
- 此時構造函數中的this指向實例對象
-
箭頭函數
- 箭頭函數沒有this, 因此也不能綁定。里面的this會指向當前最近的非箭頭函數的this,找不到就是window(嚴格模式是undefined)
JS數組
函數的arguments為什么不是數組?如何轉化成數組?
-
常見的類數組
- 用getElementsByTagName/ClassName()獲得的HTMLCollection
- 用querySelector獲得的nodeList
-
轉換成數組
- Array.prototype.slice.call()
- Array.from()
- ES6展開運算符
- 利用concat+apply
forEach中return有效果嗎?如何中斷forEach循環?
在forEach中用return不會返回,函數會繼續執行。
-
中斷方法:
- 使用try監視代碼塊,在需要中斷的地方拋出異常
- 官方推薦方法(替換方法):用every和some替代forEach函數。every在碰到return false的時候,中止循環。some在碰到return true的時候,中止循環 (面試問到團隊規范?)
JS判斷數組中是否包含某個值
- array.indexOf
- array.includes(searcElement[,fromIndex]) 推薦?
- array.find(callback[,thisArg])
- array.findeIndex(callback[,thisArg])
JS中flat---數組扁平化
- 遞歸
- reduce+遞歸
- 原型鏈上的flat方法(數組實例上的方法) [1,2,3].flat(2)
JS數組的高階函數
什么是高階函數:一個函數就可以接收另一個函數作為參數或者返回值為一個函數,這種函數就稱之為高階函數。
-
數組中的高階函數
- map
- reduce
- filter
- sort
JS如何實現繼承
什么是繼承
- 繼承是使用已存在的類的定義作為基礎建立新類的技術,新類的定義可以增加新的數據或新的功能,也可以用父類的功能,但不能選擇性地繼承父類。通過使用繼承我們能夠非常方便地復用以前的代碼,能夠大大的提高開發的效率
第一種: 借助call(構造函數式繼承
)
第二種: 借助原型鏈(原型鏈繼承)
第三種:將前兩種組合(組合繼承)
第四種:原型式繼承(如果我要繼承的父類是一個普通對象而不是構造函數(因為JavaScript 語言中,生成實例對象的傳統方法是通過構造函數),那么如何實現)
- Object.create方法
第五種:寄生繼承
- 核心:在原型式繼承的基礎上,增強對象,返回構造函數(類似工廠函數進行包裝)
第六種:寄生組合繼承
原型鏈
原型對象和構造函數有何關系?
- 在JavaScript中,每當定義一個函數數據類型(普通函數、類)時候,都會天生自帶一個prototype屬性,這個屬性指向函數的原型對象。
- 當函數經過new調用時,這個函數就成為了構造函數,返回一個全新的實例對象,這個實例對象有一個proto屬性,指向構造函數的原型對象。
能不能描述一下原型鏈
- 首先要明白實例的proto屬性與構造函數的protype屬性都是指向原型對象,原型對象的constructor屬性又是指向構造函數
- JavaScript對象通過proto 指向父類的原型對象,直到指向Object的原型對象為止,這樣就形成了一個原型指向的鏈條, 即原型鏈。
- 對象的 hasOwnProperty() 來檢查對象自身中是否含有該屬性
- 使用 in 檢查對象中是否含有某個屬性時,如果對象中沒有但是原型鏈中有,也會返回 true
DOM事件
綁定事件的?法
- HTML的內聯屬性
- 元素的onXXX屬性添加事件
-
- addEventListener
-
標準方法
-
el.addEventListener(eventNam
e, handle, useCapture |
options)- {String} eventName 事件名稱
- {Function} handle 事件函數
- {Boolean} useCapture 是否在事
件捕獲階段觸發事件,true 代表
捕獲階段觸發,false 代表在冒
泡階段觸發 - {Object} options 選項對象
el.removeEventListener(eventN
ame, handle)
-
-
IE?法
- el.attachEvent(eventName,
handle) - el.detachEvent(eventName,
handle)
- el.attachEvent(eventName,
-
對?:
- 由于IE8不?持 事件捕獲 ,所以
通過 attachEvent/detachEvent
綁定的時間也只能在 冒泡階段
觸發 - 通過 attachEvent/detachEvent
綁定的事件函數會在全局作?域
中運?,即: this === window - 通過 attachEvent/detachEvent
綁定的事件函數以綁定時的先后
順序 “倒序” 被執? - attachEvent/detachEvent 的第
?個參數要在事件名稱前?加
'on'
- 由于IE8不?持 事件捕獲 ,所以
-
評價
- 違反最佳實踐
- 由于只能賦值?個handler,
因此會存在覆蓋的問題
- 由于只能賦值?個handler,
- 調?addEventListener時,要
注意銷毀組件時回收handler,
removeEventListener。但是這
樣的話,handler?必須??個變
量保持引?
- 調?addEventListener時,要
事件對象
-
標準
-
屬性
currentTarget:currentTarget
的值始終等于 this,即指向事件
所綁定到的元素target:真正觸發事件的元素
bubbles:表示事件是否冒泡
cancelable:是否可以取消默認
?為defaultPrevented:為真則被調
?了preventDefault()detail:描述事件的細節
-
eventPhase:描述事件處理函數
的階段- 1:捕獲
- 2:處于?標
- 3:冒泡
trusted:為真則是瀏覽器原?事
件,為假則是?動添加的事件type:事件類型
-
方法
event.preventDefault():阻?默
認事件event.stopIPropagation():阻?
冒泡 也可以阻止捕獲(根據dom事件流 捕獲階段被阻止了 處于目標階段和事件冒泡也不會被觸發了)-
stopImmediatePropagation 既能阻止事件向父元素冒泡,也能阻止元素同事件類型的其它監聽器被觸發。而 stopPropagation 只能實現前者的效果
- react使用合成事件,如果出現點擊空白區域彈框消失,可以利用stopImmediatePropagation阻止事件的其它函數執行 (只執行我這個事件的回調函數,其它不執行)因為我都冒泡到document上了,阻止冒泡沒什么用了,另外一種解決方法關閉操作注冊到window上
- e.nativeEvent.stopImmediatePropagation 這個解決問題的不是阻止冒泡 而是不允許其他的事件回調觸發 因為我們這時候事件已經冒泡到document上了 為document再綁定了一個click事件 此時我們想觸發按鈕這個click事件(實際綁定到document上)觸發以后,不允許再觸發其他document上click的事件回調函數
-
-
IE
-
屬性
- srcElement:與target的值相同
- returnValue:默認為真,若設置
為false,可以阻?默認事件 - cancelBubble:默認為假,設置
為true可以阻?冒泡
-
方法
- el.onclick = function () {
window.event
} - el.attachEvent:回調中的event
可以為傳?的event參數也可為
window.event
- el.onclick = function () {
-
DOM事件流(統一這兩種事件流)
事件流:描述的是從頁面中接收事件的順序。但有意思的是
-
執行的三個階段
-
事件捕獲
- 當事件發生時,首先發生的是事件捕獲,為父元素截獲事件提供了機會
-
處于?標
- 事件到了具體元素時,在具體元素上發生,并且被看成冒泡階段的一部分。
-
事件冒泡
- 冒泡階段發生,事件開始冒泡
-
-
注意點
- DOM事件流確實會按照這三個階段執行,我們可以通過addEventListener注冊事件時候指定useCapture的值來規定事件在捕獲階段還是冒泡階段中執行(如果該對象是目標對象,則會在目標階段執行)
- 你會注意到按照DOM事件流這種執行順序,事件不會被觸發兩次吧,造成重復觸發,并不是的,我們可以有選擇是在冒泡階段觸發還是捕獲階段,默認是冒泡階段
- // 這段代碼表示該click事件會在事件捕獲階段執行(??注意得判斷是不是目標對象,如果是目標對象就是表示它在處于目標這個階段執行)
// 如何判斷是否是目標對象:最具體的元素(文檔中嵌套層次最深的那個節點)
document.querySelector("#button").addEventListener(
"click",
function () {
console.log("處于目標button click");
},
true
);
多種事件
-
UI 事件
-
load
- window上觸發:??完全加載
完,包括所有圖像、js?件、css
?件、<object>內嵌對象等等 - window上觸發:??完全加載
完,包括所有圖像、js?件、css
?件、<object>內嵌對象等等 - <script>/<link>:腳本或css加
載成功后。注意:script標簽只
?持內聯屬性
- window上觸發:??完全加載
resize
-
scroll
- window上觸發:滾動??時
- 元素:可滾動元素滾動時
-
焦點:可以捕獲,但不會冒泡
- focus
- blur
-
-
?標與滾輪事件
click
dblclick
mousedown
mouseup
-
mouseenter/mouseleave 與
mouseover/mouseout-
觸發時機
- 不論?標指針穿過被選元素或其
?元素,都會觸發 mouseover
事件。 - 只有在?標指針穿過被選元素
時,才會觸發 mouseenter 事件
- 不論?標指針穿過被選元素或其
-
是否冒泡
- mouseenter/leave不?持冒泡
- mouseover/mouseout?持冒泡
-
-
位置信息
- 客戶區坐標:event.clientX/Y
- ??坐標:event.pageX/Y
- 屏幕坐標:event.screenX/Y
-
修改鍵
- event.shiftKey
- event.ctrlKey
- event.altKey
- event.metaKey
-
鍵盤事件
- keydown
- keypress
- keyup
- 注意:通過 event.keyCode 獲取
鍵碼
-
?本事件
- ?本框插??字之前:textInput
-
HTML5事件
?標右鍵:contextmenu
??卸載前:beforeunload
形成完成DOM樹:
DOMcontentLoaded-
頁面可見性
pageshow
pagehide
-
注意
- pageshow/pagehide 必須添加
到 window對象上 - pageshow可?來監聽??前進
后退:??顯示時觸發,load 事
件只會在第?次加載??是觸
發,之后??會被 bfcache(往
返緩存)管理,通過前進后退按
鈕來顯示??時,load 事件并不
會觸發,但是 pageshow 事件會
觸發
- pageshow/pagehide 必須添加
路由的哈希值變化:
hashchange
DOM事件模型
-
DOM0級事件
- on-event (HTML 屬性)
-
DOM1級事件
- 沒有1級DOM。DOM級別1于1998年10月1日成為W3C推薦標準。1級DOM標準中并沒有定義事件相關的內容,所以沒有所謂的1級DOM事件模型
-
DOM2級事件
- el.addEventListener(event-name, callback, useCapture)
- 規定DOM事件流
-
DOM 3級事件
- 在DOM 2級事件的基礎上添加了更多的事件類型。(同多種事件)
事件代理(事件委托)
- 由于事件會在冒泡階段向上傳播到父節點,因此可以把子節點的監聽函數定義在父節點上,由父節點的監聽函數統一處理多個子元素的事件。這種方法叫做事件的代理(delegation)。
- 優點:減少內存消耗,提高性能
- 通過e.currentTarget拿到目標對象
JavaScript三大家族
client家族(只讀)
- Element.clientWidth:元素內部的寬度(單位像素),包含內邊距(padding),但不包括豎直滾動條、邊框(border)和外邊距(margin)
- Element.clientHeight:元素內部的高度(單位像素),包含內邊距(padding),但不包括水平滾動條、邊框(border)和外邊距(margin)
- MouseEvent.clientX:鼠標距離可視區域左側距離
- MouseEvent.clientY:鼠標距離可視區域上側距離
- Element.clientTop:表示一個元素頂部邊框的寬度
- Element.clientLeft:表示一個元素的左邊框的寬度
Scroll家族
- Element.scrollWidth(只讀):對內容寬度的一種度量,包括由于overflow溢出而在屏幕上不可見的內容,元素內部的高度(單位像素),包含內邊距(padding),但不包括豎直滾動條、邊框(border)和外邊距(margin)
- Element.scrollHeight(只讀):對內容高度的一種度量,包括由于overflow溢出而在屏幕上不可見的內容,元素內部的高度(單位像素),包含內邊距(padding),但不包括水平滾動條、邊框(border)和外邊距(margin)
- Element.scrollTop(讀取或設置):一個元素的 scrollTop 值是這個元素的內容頂部(卷起來的)到它的視口可見內容(的頂部)的距離的度量。當一個元素的內容沒有產生垂直方向的滾動條,那么它的 scrollTop 值為0
- Element.scrollLeft(讀取或設置):一個元素的 scrollLeft 值是這個元素的內容左部(卷起來的)到它的視口可見內容(的左部)的距離的度量。當一個元素的內容沒有產生垂直方向的滾動條,那么它的 scrollLeft 值為0
offset家族(只讀)
- Element.offsetWidth:通常,元素的offsetWidtht是一種元素CSS寬度度的衡量標準,包含元素的邊框(border)、水平線上的內邊距(padding)、水平方向滾動條(scrollbar)(如果存在的話)、以及CSS設置的寬度(width)的值。
- Element.offsetHeight:通常,元素的offsetHeight是一種元素CSS寬度的衡量標準,包含元素的邊框(border)、水平線上的內邊距(padding)、豎直方向滾動條(scrollbar)(如果存在的話)、以及CSS設置的寬度(width)的值。
- Element.offsetLeft:返回當前元素左上角相對于 Element.offsetParent 節點的左邊界偏移的像素值
- Element.offsetTop:返回當前元素相對于其 Element.offsetParent 元素的頂部內邊距的距離。
- Element.offsetParent:返回父系盒子中帶有定位的盒子節點,1.返回該對象帶有定位的父級 2.如果當前元素的父級元素沒有CSS定位, offsetParent為body;如果當前元素的父級元素中有CSS定位,offsetParent 取最近的那個有定位的父級元素。和盒子本身有無定位無關。
- Element.offsetX:規定了事件對象與目標節點的內填充邊(padding edge)在 X 軸方向上的偏移量。(目標節點坐上角為原點)
- Element.offsetY
拓展
- Element.getBoundingClientRect(): 方法返回元素的大小及其相對于視口的位置。以CSSS設置寬高作為衡量標準
- MouseEvent.pageX: pageX 是一個由MouseEvent接口返回的相對于整個文檔的x(水平)坐標以像素為單位的只讀屬性。
(pageY一樣) - MouseEvent.creenX 是只讀屬性,他提供了鼠標相對于屏幕坐標系的水平偏移量。
GC回收機制
原始數據類型是存儲在棧空間中的,引用類型的數據是存儲在堆空間中的,也就是去分析如何回收這兩種類型的內存空間
調用棧中的數據是如何回收的
- JS引擎中以棧的形式來處理執行上下文,而原始數據類型就存儲在棧中,調用棧有一個記錄當前執行狀態的指針(稱為 ESP),指向當前正在處理的執行上下文
- 當函數執行完后,對應的執行上下文就可以銷毀了,JavaScript 引擎會通過向下移動 ESP 來銷毀該函數保存在棧中的執行上下文。
- 如果存在內部函數引用變量(基本類型或者引用類型的都行),這時候是放入到閉包對象中的,閉包對象是儲存在堆內存空間中的,這屬于堆內存那塊的知識點了
堆中的數據是如何回收的
-
垃圾回收的策略:代際假說和分代收集
概念:不過在正式介紹 V8 是如何實現回收之前,你需要先學習下代際假說(The Generational Hypothesis)的內容,這是垃圾回收領域中一個重要的術語,后續垃圾回收的策略都是建立在該假說的基礎之上的,所以很是重要。
-
代際假說有以下兩個特點:
- 第一個是大部分對象在內存中存在的時間很短,簡單來說,就是很多對象一經分配內存,很快就變得不可訪問;
- 第二個是不死的對象,會活得更久。
-
堆
在 V8 中會把堆分為新生代和老生代兩個區域,新生代中存放的是生存時間短的對象,老生代中存放的生存時間久的對象
-
新生代
- 新生代中存放的是生存時間短的對象(新生區通常只支持 1~8M 的容量)
- 副垃圾回收器,主要負責新生代的垃圾回收
-
老生代
- 老生代中存放的生存時間久的對象
- 主垃圾回收器,主要負責老生代的垃圾回收
-
垃圾回收器的工作流程
- 現在你知道了 V8 把堆分成兩個區域——新生代和老生代,并分別使用兩個不同的垃圾回收器。其實不論什么類型的垃圾回收器,它們都有一套共同的執行流程。
- 第一步是標記空間中活動對象和非活動對象。所謂活動對象就是還在使用的對象,非活動對象就是可以進行垃圾回收的對象。
- 第二步是回收非活動對象所占據的內存。其實就是在所有的標記完成之后,統一清理內存中所有被標記為可回收的對象。
- 第三步是做內存整理。一般來說,頻繁回收對象后,內存中就會存在大量不連續空間,我們把這些不連續的內存空間稱為內存碎片。當內存中出現了大量的內存碎片之后,如果需要分配較大連續內存的時候,就有可能出現內存不足的情況。所以最后一步需要整理這些內存碎片,但這步其實是可選的,因為有的垃圾回收器不會產生內存碎片,比如接下來我們要介紹的副垃圾回收器。
-
副垃圾回收器
- 副垃圾回收器主要負責新生區的垃圾回收。而通常情況下,大多數小的對象都會被分配到新生區,所以說這個區域雖然不大,但是垃圾回收還是比較頻繁的。
- 新生代中用 Scavenge 算法來處理。所謂 Scavenge 算法,是把新生代空間對半劃分為兩個區域,一半是對象區域,一半是空閑區域
- 在垃圾回收過程中,首先要對對象區域中的垃圾做標記;標記完成之后,就進入垃圾清理階段,副垃圾回收器會把這些存活的對象復制到空閑區域中,同時它還會把這些對象有序地排列起來,所以這個復制過程,也就相當于完成了內存整理操作,復制后空閑區域就沒有內存碎片了。
- 完成復制后,對象區域與空閑區域進行角色翻轉,也就是原來的對象區域變成空閑區域,原來的空閑區域變成了對象區域。這樣就完成了垃圾對象的回收操作,同時這種角色翻轉的操作還能讓新生代中的這兩塊區域無限重復使用下去。
- 由于新生代中采用的 Scavenge 算法,所以每次執行清理操作時,都需要將存活的對象從對象區域復制到空閑區域。但復制操作需要時間成本,如果新生區空間設置得太大了,那么每次清理的時間就會過久,所以為了執行效率,一般新生區的空間會被設置得比較小。
-
對象晉升策略
- 也正是因為新生區的空間不大,所以很容易被存活的對象裝滿整個區域。為了解決這個問題,JavaScript 引擎采用了對象晉升策略,也就是經過兩次垃圾回收依然還存活的對象,會被移動到老生區中。
-
主垃圾回收器
垃圾回收器是采用標記 - 清除(Mark-Sweep)的算法進行垃圾回收的
首先是標記過程階段。標記階段就是從一組根元素開始,遞歸遍歷這組根元素,在這個遍歷過程中,能到達的元素稱為活動對象,沒有到達的元素就可以判斷為垃圾數據。然后就是擦除這些垃圾數據
-
標記 - 整理(Mark-Compact)
- 上面的標記過程和清除過程就是標記 - 清除算法,不過對一塊內存多次執行標記 - 清除算法后,會產生大量不連續的內存碎片。而碎片過多會導致大對象無法分配到足夠的連續內存,于是又產生了另外一種算法——標記 - 整理(Mark-Compact),這個標記過程仍然與標記 - 清除算法里的是一樣的,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內
-
增量標記算法
- 在 V8 新生代的垃圾回收中,因其空間較小,且存活對象較少,所以全停頓的影響不大,但老生代就不一樣了。如果在執行垃圾回收的過程中,占用主線程時間過久,就像上面圖片展示的那樣,花費了 200 毫秒,在這 200 毫秒內,主線程是不能做其他事情的。比如頁面正在執行一個 JavaScript 動畫,因為垃圾回收器在工作,就會導致這個動畫在這 200 毫秒內無法執行的,這將會造成頁面的卡頓現象。
- 為了降低老生代的垃圾回收而造成的卡頓,V8 將標記過程分為一個個的子標記過程,同時讓垃圾回收標記和 JavaScript 應用邏輯交替進行,直到標記階段完成,我們把這個算法稱為增量標記(Incremental Marking)算法
- 使用增量標記算法,可以把一個完整的垃圾回收任務拆分為很多小的任務,這些小的任務執行時間比較短,可以穿插在其他的 JavaScript 任務中間執行,這樣當執行上述動畫效果時,就不會讓用戶因為垃圾回收任務而感受到頁面的卡頓了。(??將大的任務拆分成小 減少卡頓)
??注意什么時候進行標記 什么時候進行清除
- 當執行上下文創建時,變量進入該環境,我們就可以對該變量對應的內存進行標記。如果執行上下文執行完畢,這個時候,就可以將所有進入該環境的變量標記為可清除狀態。我們通俗的說法就是,當一份內存失去了引用,那么它就會被垃圾回收工具回收。
- 不過還有兩個需要注意的地方。
一個是全局上下文。在程序結束之前,全局上下文始終存在。通常來說,JS程序運行期間,全局上下文不會有執行結束的時間節點。因此定義在全局上下文的狀態永遠都不會被標記。除非我們手動將變量設置為null,它對應的內存都不會被回收
- JS引擎執行代碼是邊解釋邊執行,對于未執行的函數代碼段 都還沒到到編譯階段呢,也不會分配變量內存給他們了,執行到這個階段才會的
閉包
什么是閉包
- 在 JavaScript 中,根據詞法作用域的規則,內部函數總是可以訪問其外部函數中聲明的變量,當通過調用一個外部函數返回一個內部函數后,即使該外部函數已經執行結束了,但是內部函數引用外部函數的變量依然保存在內存中,我們就把這些變量的集合稱為閉包。比如外部函數是 foo,那么這些變量的集合就稱為 foo 函數的閉包
從內存模型的角度來分析閉包
- 代碼例子:
function foo() {
var myName = "極客時間"
let test1 = 1
const test2 = 2
var innerBar = {
setName:function(newName){
myName = newName
},
getName:function(){
console.log(test1)
return myName
}
}
return innerBar
}
var bar = foo()
bar.setName("極客邦")
bar.getName()
console.log(bar.getName()) - 當 JavaScript 引擎執行到 foo 函數時,首先會編譯,并創建一個空執行上下文
- 在編譯過程中,遇到內部函數 setName,JavaScript 引擎還要對內部函數做一次快速的詞法掃描,發現該內部函數引用了 foo 函數中的 myName 變量,由于是內部函數引用了外部函數的變量,所以 JavaScript 引擎判斷這是一個閉包,于是在堆空間創建換一個“closure(foo)”的對象(這是一個內部對象,JavaScript 是無法訪問的),用來保存 myName 變量。
- 接著繼續掃描到 getName 方法時,發現該函數內部還引用變量 test1,于是 JavaScript 引擎又將 test1 添加到“closure(foo)”對象中。這時候堆中的“closure(foo)”對象中就包含了 myName 和 test1 兩個變量了。
- 由于 test2 并沒有被內部函數引用,所以 test2 依然保存在調用棧中。
- ??閉包對象的創建在編譯的時候創建,在編譯過程中,遇到內部函數 setName,JavaScript 引擎還要對內部函數做一次快速的詞法掃描,所以這也解釋了閉包對象的名稱是外部函數,這是對的 (外面的文章寫內部函數是閉包,這是錯誤的?)
- ??總的來說,產生閉包的核心有兩步:第一步是需要預掃描內部函數;第二步是把內部函數引用的外部變量保存到堆中,在編譯階段就會產生閉包對象(前提是函數跟函數之前),如果閉包對象存在引用,就不會被銷毀,這也是要注意到內存泄漏的地方(??總注意形成閉包的前提是 外部函數和內部函數)
閉包對象是編譯階段就產生的,如果存在引用則不會被GC回收機制回收
閉包的應用
- 模仿塊級作用域
- 私有變量
執行上下文
概念:JavaScript代碼在執行時,會進入一個執行上下文中。執行上下文可以理解為當前代碼的運行環境
執行上下文的三種類型
- 全局環境:代碼運行起來后會首先進入全局環境
- 函數環境:當函數被調用執行時,會進入當前函數中的執行代碼
- eval環境:不建議使用,不做介紹
執行上下文的生命周期
-
編譯階段(創建階段)
- 經過JS引擎編譯后,會生成兩部分內容:執行上下文(Execution context)和可執行代碼
- 在這個階段,執行上下文會分別創建變量對象、確定作用域鏈,以及this指向,明白這個階段也就會明白變量提升的現象,(也就是在編譯階段我們就確定了作用域鏈和this指向等)
-
執行階段
- 創建階段之后,就會開始執行代碼,這個時候會完成變量賦值,函數引用,以及執行其它可執行代碼,如圖所示
變量對象
在JavaScript代碼中聲明的所有變量都保存在變量對象中,除此之外,變量對象中還可能包含以下內容
函數的所有參數(Firefox中為參數對象arguments)
當前上下文中的所有函數聲明(通過function聲明的函數)
當前上下文的所有變量聲明(通過var聲明的變量)
-
變量對象創建過程
- 在Chrome瀏覽器中,變量對象會首先獲得函數的參數變量及其值;在Firefox瀏覽器中,是直接將參數對象arguments保存在變量對象中
- 依次獲取當前上下文中所有的函數聲明,也就是使用function關鍵字聲明的函數。在變量中會以函數名建立一個屬性,屬性值為指向該函數所在的內存地址引用。如果函數名的屬性已經存在,那么該屬性的值會被新的引用覆蓋
- 依次獲取當前上下文的變量聲明,也就是以var關鍵字聲明的變量。每找到一個變量聲明,就在變量對象中就以變量名建立一個屬性,屬性值為undefined,如果該變量名的屬性已經存在,為了防止同名的函數被修改為undefined,則會直接跳過,原屬性不會被修改,也就是如果變量與函數同名,則在這個階段,以函數值為準
作用域和作用域鏈
-
作用域
作用域規定了如何查找變量,也就是確定當前執行代碼對變量的訪問權限。
-
詞法作用域
-
詞法作用域就是指作用域是由代碼中函數聲明的位置來決定的,所以詞法作用域是靜態的作用域,通過它就能夠預測代碼在執行過程中如何查找標識符。(簡單理解書寫??代碼的時候,詞法作用域是代碼階段就決定好的,和函數是怎么調用的沒有關系)
-
全局作用域
- 最外層函數和在最外層函數外面定義的變量
- 沒有通過關鍵字"var"聲明的變量(包括嵌套的函數內)
- 瀏覽器中,window對象的屬性
函數作用域
塊級作用域
-
-
動態作用域
-
作用域鏈
- 作用域鏈,是由當前環境與上層環境的一系列變量對象組成,它保證了當前執行環境對符合訪問權限的變量和函數的有序訪問。
- 編譯階段就創造了作用域鏈,內部函數copy拷貝外部函數的作用域,在函數內部有[[scope]]屬性就是表示作用域鏈
- ??理解作用域鏈是理解閉包的基礎
V8
高級語言
-
編譯型語言在程序執行之前,需要經過編譯器的編譯過程,并且編譯之后會直接保留機器能讀懂的二進制文件,這樣每次運行程序時,都可以直接運行該二進制文件,而不需要再次重新編譯了。比如 C/C++、GO 等都是編譯型語言。
- 在編譯型語言的編譯過程中,編譯器首先會依次對源代碼進行詞法分析、語法分析,生成抽象語法樹(AST),然后是優化代碼,最后再生成處理器能夠理解的機器碼。如果編譯成功,將會生成一個可執行的文件。但如果編譯過程發生了語法或者其他的錯誤,那么編譯器就會拋出異常,最后的二進制文件也不會生成成功
-
而由解釋型語言編寫的程序,在每次運行時都需要通過解釋器對程序進行動態解釋和執行。比如 Python、JavaScript 等都屬于解釋型語言。
- 在解釋型語言的解釋過程中,同樣解釋器也會對源代碼進行詞法分析、語法分析,并生成抽象語法樹(AST),不過它會再基于抽象語法樹生成字節碼,最后再根據字節碼來執行程序、輸出結果。
V8 是如何執行一段 JavaScript 代碼的
-
生成抽象語法樹(AST)和執行上下文
將源代碼轉換為抽象語法樹,并生成執行上下文,主要是代碼在執行過程中的環境信息
高級語言是開發者可以理解的語言,但是讓編譯器或者解釋器來理解就非常困難了。對于編譯器或者解釋器來說,它們可以理解的就是 AST 了。所以無論你使用的是解釋型語言還是編譯型語言,在編譯過程中,它們都會生成一個 AST(解釋器和編譯器先把源代碼編譯成AST)
-
如何生成AST
- 第一階段是分詞(tokenize),又稱為詞法分析,其作用是將一行行的源碼拆解成一個個 token。所謂 token,指的是語法上不可能再分的、最小的單個字符或字符串。你可以參考下圖來更好地理解什么 token
- 第二階段是解析(parse),又稱為語法分析,其作用是將上一步生成的 token 數據,根據語法規則轉為 AST。如果源碼符合語法規則,這一步就會順利完成。但如果源碼存在語法錯誤,這一步就會終止,并拋出一個“語法錯誤”。(??有了 AST 后,那接下來 V8 就會生成該段代碼的執行上下文)
-
生成字節碼
- 有了 AST 和執行上下文后,那接下來的第二步,解釋器 Ignition 就登場了(解釋器也負責了解釋源代碼到AST任務),它會根據 AST 生成字節碼,并解釋執行字節碼。
- 字節碼就是介于 AST 和機器碼之間的一種代碼。但是與特定類型的機器碼無關,字節碼需要通過解釋器將其轉換為機器碼后才能執行。
-
執行代碼
- 生成字節碼之后,接下來就要進入執行階段了。
- 通常,如果有一段第一次執行的字節碼,解釋器 Ignition 會逐條解釋執行。到了這里,相信你已經發現了,解釋器 Ignition 除了負責生成字節碼之外,它還有另外一個作用,就是解釋執行字節碼。在 Ignition 執行字節碼的過程中,如果發現有熱點代碼(HotSpot),比如一段代碼被重復執行多次,這種就稱為熱點代碼,那么后臺的編譯器 TurboFan 就會把該段熱點的字節碼編譯為高效的機器碼,然后當再次執行這段被優化的代碼時,只需要執行編譯后的機器碼就可以了,這樣就大大提升了代碼的執行效率。
內存機制
三種類型內存空間
代碼空間
-
棧空間
- 原始類型的數據值都是直接保存在“棧”中的
-
堆空間
- 引用類型的值是存放在“堆”中的
事件循環
起因:在 JS 中,大部分的任務都是在主線程上執行,常見的任務有渲染事件,用戶交互事件,js腳本執行,網絡請求、文件讀寫完成事件等等,為了讓這些事件有條不紊地進行,JS引擎需要對之執行的順序做一定的安排,V8 其實采用的是一種隊列的方式來存儲這些任務, 即先進來的先執行。
注意??事件循環不單單是為了解決JS是單線程(渲染主線程)解決異步的原因,而是更大更全的去理解他是瀏覽器渲染主線程的調度系統,通過這個調度系統去有條不紊的安排任務執行(JavaScript沒有自己循環系統,它依賴的就是瀏覽器的循環系統,也就是渲染進程提供的循環系統!)
宏任務
- 渲染事件(如解析 DOM、計算布局、繪制)
- 用戶交互事件(如鼠標點擊、滾動頁面、放大縮小等)
- JavaScript 腳本執行事件;
- 網絡請求完成、文件讀寫完成事件。
微任務
-
起因
-
宏任務的時間粒度比較大,執行的時間間隔是不能精確控制的,對一些高實時性的需求就不太符合了,比如后面要介紹的監聽 DOM 變化的需求
- 如何處理高優先級的任務。
-
監聽 DOM 變化技術方案的演化史
- 從輪詢到 Mutation Event 再到最新使用的 MutationObserver。MutationObserver 方案的核心就是采用了微任務機制,有效地權衡了實時性和執行效率的問題
-
我們知道當 JavaScript 執行一段腳本的時候,V8 會為其創建一個全局執行上下文,在創建全局執行上下文的同時,V8 引擎也會在內部創建一個微任務隊列。顧名思義,這個微任務隊列就是用來存放微任務的,因為在當前宏任務執行的過程中,有時候會產生多個微任務,這時候就需要使用這個微任務隊列來保存這些微任務了。不過這個微任務隊列是給 V8 引擎內部使用的,所以你是無法通過 JavaScript 直接訪問的
??也就是說每個宏任務都關聯了一個微任務隊列
-
微任務的工作流程
- 微任務和宏任務是綁定的,每個宏任務在執行時,會創建自己的微任務隊列
- 微任務的執行時長會影響到當前宏任務的時長。比如一個宏任務在執行過程中,產生了 100 個微任務,執行每個微任務的時間是 10 毫秒,那么執行這 100 個微任務的時間就是 1000 毫秒,也可以說這 100 個微任務讓宏任務的執行時間延長了 1000 毫秒。所以你在寫代碼的時候一定要注意控制微任務的執行時長。
- 在一個宏任務中,分別創建一個用于回調的宏任務和微任務,無論什么情況下,微任務都早于宏任務執行。
MutationObserver
Promise.then(或.reject) 以及以 Promise 為基礎開發的其他技術(比如fetch API)
消息隊列
-
基于不同的場景來動態調整消息隊列的優先級
- 可以創建輸入事件的消息隊列,用來存放輸入事件。
- 可以創建合成任務的消息隊列,用來存放合成事件。
- 可以創建默認消息隊列,用來保存如資源加載的事件和定時器回調等事件。
- 還可以創建一個空閑消息隊列,用來存放 V8 的垃圾自動垃圾回收這一類實時性不高的事件。
事件循環的流程
- 宏任務==>清空所有的微任務===>UI渲染
rAF
window.requestAnimationFrame() 告訴瀏覽器——你希望執行一個動畫,并且要求瀏覽器在下次重繪之前調用指定的回調函數更新動畫。該方法需要傳入一個回調函數作為參數,該回調函數會在瀏覽器下一次重繪之前執行
-
VSync
- 當顯示器將一幀畫面繪制完成后,并在準備讀取下一幀之前,顯示器會發出一個垂直同步信號(vertical synchronization)給 GPU,簡稱 VSync
-
為什么需要用rAF代替setTimeout
- 我們知道 CSS 動畫是由渲染進程自動處理的,所以渲染進程會讓 CSS 渲染每幀動畫的過程與 VSync 的時鐘保持一致, 這樣就能保證 CSS 動畫的高效率執行。
- 用戶體驗動畫流暢的幀率大概是60FPS,使用setTimout很難精確控制,可所以使用rAF交由系統控制,保持跟顯示器的幀率大概一致
- 但是 JavaScript 是由用戶控制的,如果采用 setTimeout 來觸發動畫每幀的繪制,那么其繪制時機是很難和 VSync 時鐘保持一致的,所以 JavaScript 中又引入了 window.requestAnimationFrame,用來和 VSync 的時鐘周期同步
- VSync 和系統的時鐘不同步就會造成掉幀、卡頓、不連貫等問題
-
requestAnimationFrame 在 EventLoop 中是一個什么位置?
- rAF會在UI渲染之前
問題
-
UI渲染也會產生宏任務,那么按照實際循環流程,是會無限遞歸的那種
- 消息隊列也是分為優先級的(雖然微任務是高優先級任務 但是依賴于宏任務 比如交互操作 點擊事件產生的回調是個宏任務 )也就是這個每次執行一次宏任務,觸發UI渲染 這個宏任務應該屬于交互消息隊列的類型的, 應該是根據消息隊列類別來判斷的 (本身屬于合成消息隊列就不會再觸發UI渲染了)
-
觸發一次宏任務就一定會執行UI渲染嗎
- 進入更新渲染階段,判斷是否需要渲染,這里有一個 rendering opportunity 的概念,也就是說不一定每一輪 event loop 都會對應一次瀏覽 器渲染,要根據屏幕刷新率、頁面性能、頁面是否在后臺運行來共同決定,通常來說這個渲染間隔是固定的。(所以多個 task 很可能在一次渲染之間執行)
-
靜止不動的頁面需要每隔16ms觸發一次UI渲染嗎
- 我覺得完全沒必要,因為沒啥意義,都靜止不動了,根據瀏覽器FPS觀察幾乎為0,所以說瀏覽器不一定非跟顯示器保持百分百的幀率一致
-
16ms
渲染幀是指瀏覽器一次完整繪制過程,幀之間的時間間隔是 DOM 視圖更新的最小間隔。 由于主流的屏幕刷新率都在 60Hz,那么渲染一幀的時間就必須控制在 16ms 才能保證不掉幀。 也就是說每一次渲染都要在 16ms 內頁面才夠流暢不會有卡頓感
-
為什么需要這個判斷,為了動畫順暢性,所以不存在時間基點判定,你交互開始時候就算,保持一幀16ms左右就是流暢,也就是滿足這個幀率間隔就不會卡頓 (我這個交互或者動畫是幀率60左右就是流暢 靜止的頁面都不需要流暢這個概念 幀率為0就行 所以不要有絕對的那種時間線)
- scroll
- resize
-
這段時間內瀏覽器需要完成如下事情
- 腳本執行(JavaScript):腳本造成了需要重繪的改動,比如增刪 DOM、請求動畫等
- 樣式計算(CSS Object Model):級聯地生成每個節點的生效樣式。
- 布局(Layout):計算布局,執行渲染算法
- 重繪(Paint):各層分別進行繪制(比如 3D 動畫)
- 合成(Composite):合成各層的渲染結果
函數式編程
高階函數
高階函數(higher-order function)指操作函數的函數,一般地,有以下兩種情況
函數可以作為參數被傳遞
函數可以作為返回值輸出
-
作用
- 增強函數的功能,Redux中間件就是高階函數的產物
純函數
- 定義:純函數是這樣一種函數,即相同的輸入,永遠會得到相同的輸出,而且沒有任何可觀察的副作用。
- 場景:比如 slice 和 splice,這兩個函數的作用并無二致——但是注意,它們各自的方式卻大不同,但不管怎么說作用還是一樣的。我們說 slice 符合純函數的定義是因為對相同的輸入它保證能返回相同的輸出。而 splice 卻會嚼爛調用它的那個數組,然后再吐出來;這就會產生可觀察到的副作用,即這個數組永久地改變了。
函數柯里化
概念:在數學和計算機科學中,柯里化是一種將使用多個參數的一個函數轉換成一系列使用一個參數的函數的技術。
-
作用
參數復用
-
提前返回
- 比如判斷一次類型以后下次直接使用該類型對應的特性就行
延遲計算/運行
/** 利用遞歸加函數的length熟悉實現柯里化 */
const curry = fn =>
judge = (...args) =>
args.length === fn.length
? fn(...args)
: (arg) => judge(...args, arg)
偏函數
概念:在計算機科學中,局部應用是指固定一個函數的一些參數,然后產生另一個更小元的函數。
-
柯里化與局部應用
- 柯里化是將一個多參數函數轉換成多個單參數函數,也就是將一個 n 元函數轉換成 n 個一元函數。
- 局部應用則是固定一個函數的一個或者多個參數,也就是將一個 n 元函數轉換成一個 n - x 元函數。
-
實現
- 使用bind:add.bind(null, 1),然而使用 bind 我們還是改變了 this 指向,我們要寫一個不改變 this 指向的方法。
- function partial(fn) {
var args = [].slice.call(arguments, 1);
return function() {
var newArgs = args.concat([].slice.call(arguments));
return fn.apply(this, newArgs);
};
};
惰性函數
概念:惰性載入表示函數執行的分支只會在函數第一次調用的時候執行,在第一次調用過程中,該函數會被覆蓋為另一個按照合適方式執行的函數,這樣任何對原函數的調用就不用再經過執行的分支了。
const foo = function() {
var t = new Date();
foo = function() {
return t;
};
return foo();
}; // 重寫覆蓋foo函數-
應用
- DOM 事件添加中,為了兼容現代瀏覽器和 IE 瀏覽器,我們需要對瀏覽器環境進行一次判斷
函數組合
概念:函數組合就是組合兩到多個函數來生成一個新函數的過程。 將函數組合在一起,就像將一連串管道扣合在一起,讓數據流過一樣。 簡而言之,函數 f 和 g 的組合可以被定義為 f(g(x)) ,從內到外(從右到左)求值
-
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}if (funcs.length === 1) {
return funcs[0]
}return funcs.reduce((a, b) => (...args) => a(b(...args)))
} f1(f2(f3(..args))) 從右到左 compose(f1,f2,f3)(...args) 函數f3執行過后把值return給f2
-
reduce方法
-
callback:執行數組中每個值 (如果沒有提供 initialValue則第一個值除外)的函數,包含四個參數:
- accumulator:累計器累計回調的返回值; 它是上一次調用回調時返回的累積值,或initialValue(見于下方)
- currentValue:數組中正在處理的元素。
- index 可選
數組中正在處理的當前元素的索引。 如果提供了initialValue,則起始索引號為0,否則從索引1起始。 - array:調用reduce()的數組
- initialValue:作為第一次調用 callback函數時的第一個參數的值。 如果沒有提供初始值,則將使用數組中的第一個元素。 在沒有初始值的空數組上調用 reduce 將報錯。沒有提供initialValue的話累計計算就從數組下標1開始
-
函數記憶
- 函數記憶是指將上次的計算結果緩存起來,當下次調用時,如果遇到相同的參數,就直接返回緩存中的數據。(簡單點講就是緩存函數)
- let memoize = function (func, content) {
let cache = Object.create(null)
content = content || this
return (...key) => {
if (!cache[key]) {
cache[key] = func.apply(content, key)
}
return cache[key]
}
}
XMind - Trial Version