閉包與高階函數
- 本文描述了作用域,閉包,內存管理的理解及高階函數面向切面編程,節流防抖函數多種實現方案
作用域
理解作用域:var a = 2
,引擎幹了那些活
-
遇見 var a,編譯器會詢問當前作用域是否存在該變量,存在,忽略;不存在,在當前作用域下,聲明該變量,並命名爲 a;
-
運行處理 a=2 時,引擎會詢問當前作用域集合是否存在 a 的變量;存在就使用該變量,不存在繼續向外層作用域查找該變量;找到了,將 2 賦值給 a,沒找到,則拋出異常;
總結: 變量賦值會有兩個動作,當前作用域聲明該變量,查找變量,找到變量進行賦值; -
LHS 和 RHS 查詢:LHS 可以簡單說是左側查詢,RHS 右側查詢;LHS 查詢,沒找到變量會創建變量,RHS 查詢,沒找到變量拋出異常;
function(a){ var b = a; return a+b } var c = foo(2); LHS:3 RHS:4
變量作用域:
- 變量有效範圍。
函數作用域:
- 函數可以用來創造函數作用域;函數可以獲取外部變量,外部無法獲取函數內部變量;如果函數內部沒有該變量,函數會根據執行環境創造的作用域鏈,逐次向外層去尋找該變量。
- 下面代碼,也會幫大家理解函數作用域
var a = 123; var b = 'b1' var fn = function () { var b = 1; var fn1 = function(){ var c = 'c' console.log(c) // c console.log(b) // 1 } fn1(); console.log(c) // c is not defined } fn()
變量生存週期
- 在函數內部,函數調用結束就會被銷燬;函數外部全局變量,需要手動銷燬,不銷燬會一直存在內存中,銷燬的方案:
var a = null
;
閉包
-
上面將作用域和變量生存週期簡單的講述了一下,接下來將會談談閉包,閉包跟這些概念非常緊密。
-
定義:當函數可以記住並訪問所在的詞法作用域時,就產生了閉包,即使函數是在當前詞法作用域之外執行;
實例:不是閉包的例子function foo() { var a = 2; function bar() { console.log( a ); // 2 } bar(); } foo();
實例:閉包效果的實例
function foo() { var a = 2; function bar() { console.log( a ); // 2 } return bar; } var baz = foo(); baz();
分析: 閉包函數外部作用域,可以訪問函數內部詞法作用域的變量。baz()對該作用域進行引用,這個引用就叫做閉包
-
爲了更好的理解閉包,下面在寫兩個例子
實例 1:
function fo (){ var a = 2 function ba(){ console.log(a) } bg(ba) } function bg(fn){ fn() // 閉包 } fo()
實例 2:
var bg; function fo (){ var a = 2 function ba(){ console.log(a) } bg = ba; } function bar(){ bg() //閉包 } fo(); bar();
分析: 總而言之,閉包能將函數內部的作用域,傳遞到函數外部作用域,進行引用,都會使用閉包。
閉包與內存管理
- 經常也會聽說閉包,會造成內存泄漏,這個說法並不正確。內存泄漏,得看根對象裏有多少引用,沒有被根對象引用得變量或者引用類型,都會被標記-清除算法給回收。其實大量得閉包引用被濫用,纔是導致內存得元兇。函數內部得局部變量在函數調用完就會被回收。對於全局得引用變量,則是可以通過引用計數進行垃圾回收。循環引用,造成的內存泄漏,只要將循環引用類型的變量設置爲null,瀏覽器就會通過標記清除算法,進行回收內存。記住一點,閉包並不會造成內存泄漏。
閉包封裝函數
var fn4 = (function(){
var cache = {};
return function(){
var args = Array.prototype.join.call(arguments, ',')
if(cache[args]){
return cache[args]
}
var a = 1;
for(var i = 0;i<arguments.length; i++){
a = a*arguments[i]
}
return cache[args] = a;
}
})()
高階函數
- 高階函數可以說是日常開發中經常用的,其實可以說高階函數是閉包的一種運用。那咱們先來看看高階函數的定義吧;
- 定義: 函數可以作爲參數被傳遞; 函數可以作爲返回值輸出;這兩個條件滿足其中一個就可以稱爲高階函數。
- 函數作爲參數,像
Array.prototype.sort
,Array.prototype.filter
等等,都屬於高階函數 - 函數作爲返回值:示例,數據類型判斷
var isType = function(type){ return function(obj){ return Object.prototype.toString.call(obj) === '[object '+type+']' } } var isArray = function(obj){ return isType('Array').call(null, obj); } var isNumber = function(obj){ return isType('Number').call(null, obj) }
高階函數實現AOP編程:
AOP就是面向切面編程,把核心功能無關寫邏輯抽離出來,通過動態植入方式,摻進邏輯模塊中;達到模塊的純淨與高內聚。
1. 示例:
Function.prototype.before = function(fn){
var that = this; // function
return function(){
fn.apply(this, arguments);
return that.apply(this,arguments)
}
}
Function.prototype.after = function(fn){
var that = this;
return function(){
var self = that.apply(this, arguments)// 執行原函數,形成鏈式調用
fn.apply(this, arguments) // 執行摻入函數,修正this
return self
}
}
var fng1 = function(){
console.log(1)
}
var fng2 = fng1.before(function(){
console.log(0)
}).after(function(){
console.log(2)
})
高階函數實現柯里化函數:currerying
var cont = (function(){
var money = [];
return function(){
if(arguments.length === 0){
var mon = 0;
for(var i = 0; i<money.length; i++){
mon +=money[i]
}
return mon
}else{
Array.prototype.push.apply(money, arguments)
}
}
})()
cont(100);
cont(200);
console.log(cont())
uncurrying函數實現
Function.prototype.unCurrying = function(){
var that =this;
return function(){
var o = Array.prototype.shift.call(arguments);
return that.apply(o, arguments)
}
}
var push = Array.prototype.push.unCurrying();
var num = (function(){
push(arguments, 4)
console.log(arguments)
})(1,2,3)
分析: 上面的代碼是由javascript的之父 Brendan Eich寫的一段代碼,其實上面的一段代碼的效果也可以通過Array.prototype.push.call(arguments, 4)
是一樣的結果,關鍵在這種思維方法。
push方法接受兩個參數,num函數的argument和4,Array.prototype.push.unCurrying();
表示unCurrying方法複製了數組的push方法;而unCurrerying方法的返回值就是Function對象中的unCurreying方法並且實現了arguments繼承數組的push方法,並且把4中對應argumentsd的值,通過apply的顯性綁定,給添加到第一個arguments的值中去。這個時候閉包中nums對應的arguments
的長度就是4,參數就是1234;
節流與防抖
1. setTimeout節流
var setThrottle = (function(){
var throFlag = true; //節流判斷
return function(fn, time){
var that = this;
if(!throFlag){
return false;
}
throFlag = false;
setTimeout(()=>{
fn.apply(this, arguments)
throFlag = true;
}, time)
}
})();
2. date時間值節流
const setDateThro = (function(){
let old = Date.now();
return function(fn, time){
if(Date.now()- old >= time){
fn.apply(this, arguments);
old = Date.now();
}
}
})()
3. 混合節流,最後一次也需要執行該函數
const componentThro = (function(){
let old = Date.now();
let timeout;
return function(fn, time){
const args = arguments;
clearTimeout(timeout)
if(Date.now()- old > time){
fn.apply(this, args);
old = Date.now();
}else{ // 事件完結後再執行一次
timeout = setTimeout(()=>{
fn.apply(this, args);
}, time)
}
}
})()
div1.onclick = function(){
componentThro(()=>{
console.log('節流')
}, 1000)
}
4. 防抖,第一次執行
var deben = (function(){
let timeout = undefined;
let preFlag = true;
return function(fn, time){
const args = arguments;
if(preFlag){ // 第一次執行一次;
fn.apply(this, args)
}
preFlag = false
clearTimeout(timeout)
timeout = setTimeout(()=>{
preFlag = true;
}, time)
}
})()
div1.onclick = function(){
deben(()=>{
console.log('防抖')
}, 1000)
}
5. 防抖,最後一次點擊執行
var deben = (function(){
let timeout = undefined;
return function(fn, time){
const args = arguments;
preFlag = false
clearTimeout(timeout)
timeout = setTimeout(()=>{
fn.apply(this, args)
}, time)
}
})()
div1.onclick = function(){
deben(()=>{
console.log('防抖')
}, 1000)
}
6. 防抖,第一次和最後一次都需要執行
var deben = (function(){
let timeout = undefined;
let preFlag = true;
return function(fn, time){
const args = arguments;
if(preFlag){ // 第一次執行一次;
fn.apply(this, args)
}
preFlag = false
clearTimeout(timeout)
timeout = setTimeout(()=>{
fn.apply(this, args)
preFlag = true;
}, time)
}
})()
div1.onclick = function(){
deben(()=>{
console.log('防抖')
}, 1000)
}
參考文檔
- 節流
- javascript 設計模式與開發實踐
- 你不知道的javascript