閉包的使用場景,使用閉包需要注意什么

閉包
什么是閉包
閉包很簡單,就是能夠訪問另一個函數(shù)作用域變量的函數(shù),更簡單的說,閉包就是函數(shù),只不過是聲明在其它函數(shù)內(nèi)部而已。
例如:
function getOuter(){
var count = 0
function getCount(num){
count += num
console.log(count) //訪問外部的date
}
return getCount //外部函數(shù)返回
}
var myfunc = getOuter()
myfunc(1) // 1
myfunc(2) // 3
myfunc 就是閉包, myfunc 是執(zhí)行 getOuter 時創(chuàng)建的 getCount 函數(shù)實例的引用。getCount 函數(shù)實例維護(hù)了一個對它的詞法環(huán)境的引用,所以閉包就是函數(shù)+詞法環(huán)境
當(dāng) myfunc 函數(shù)被調(diào)用時,變量 count 依然是可用的,也可以更新的
function add(x){
return function(y){
return x + y
};
}
var addFun1 = add(4)
var addFun2 = add(9)
console.log(addFun1(2)) //6
console.log(addFun2(2)) //11
add 接受一個參數(shù) x ,返回一個函數(shù),它的參數(shù)是 y ,返回 x+y
add 是一個函數(shù)工廠,傳入一個參數(shù),就可以創(chuàng)建一個參數(shù)和其他參數(shù)求值的函數(shù)。
addFun1 和 addFun2 都是閉包。他們使用相同的函數(shù)定義,但詞法環(huán)境不同, addFun1 中 x 是 4 ,后者是 5
即:
閉包可以訪問當(dāng)前函數(shù)以外的變量 即使外部函數(shù)已經(jīng)返回,閉包仍能訪問外部函數(shù)定義的變量與參數(shù) 閉包可以更新外部變量的值
所以,閉包可以:
避免全局變量的污染 能夠讀取函數(shù)內(nèi)部的變量 可以在內(nèi)存中維護(hù)一個變量
使用閉包應(yīng)該注意什么
代碼難以維護(hù): 閉包內(nèi)部是可以訪問上級作用域,改變上級作用域的私有變量,我們使用的使用一定要小心,不要隨便改變上級作用域私有變量的值
使用閉包的注意點: 由于閉包會使得函數(shù)中的變量都保存在內(nèi)存中,內(nèi)存消耗很大,所以不能濫用閉包,否則會造成網(wǎng)頁的性能問題,在IE中可能導(dǎo)致內(nèi)存泄漏。解決方法是,在退出函數(shù)之前,將不使用的局部變量全部刪除(引用設(shè)置為
null,這樣就解除了對這個變量的引用,其引用計數(shù)也會減少,從而確保其內(nèi)存可以在適當(dāng)?shù)臅r機回收)內(nèi)存泄漏: 程序的運行需要內(nèi)存。對于持續(xù)運行的服務(wù)進(jìn)程,必須及時釋放不再用到的內(nèi)存,否則占用越來越高,輕則影響系統(tǒng)性能,重則導(dǎo)致進(jìn)程崩潰。不再用到的內(nèi)存,沒有及時釋放,就叫做內(nèi)存泄漏
this指向: 閉包的this指向的是window
應(yīng)用場景
閉包通常用來創(chuàng)建內(nèi)部變量,使得這些變量不能被外部隨意修改,同時又可以通過指定的函數(shù)接口來操作。例如 setTimeout 傳參、回調(diào)、IIFE、函數(shù)防抖、節(jié)流、柯里化、模塊化等等
setTimeout 傳參
//原生的setTimeout傳遞的第一個函數(shù)不能帶參數(shù)
setTimeout(function(param){
alert(param)
},1000)
//通過閉包可以實現(xiàn)傳參效果
function myfunc(param){
return function(){
alert(param)
}
}
var f1 = myfunc(1);
setTimeout(f1,1000);
回調(diào)
大部分我們所寫的 JavaScript 代碼都是基于事件的 — 定義某種行為,然后將其添加到用戶觸發(fā)的事件之上(比如點擊或者按鍵)。我們的代碼通常作為回調(diào):為響應(yīng)事件而執(zhí)行的函數(shù)。
例如,我們想在頁面上添加一些可以調(diào)整字號的按鈕??梢圆捎胏ss,也可以使用:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>test</title>
<link rel="stylesheet" href="">
</head>
<style>
body{
font-size: 12px;
}
h1{
font-size: 1.5rem;
}
h2{
font-size: 1.2rem;
}
</style>
<body>
<p>測試</p>
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>
<script>
function changeSize(size){
return function(){
document.body.style.fontSize = size + 'px';
};
}
var size12 = changeSize(12);
var size14 = changeSize(14);
var size16 = changeSize(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
</script>
</body>
</html>
IIFE
var arr = [];
for (var i=0;i<3;i++){
//使用IIFE
(function (i) {
arr[i] = function () {
return i;
};
})(i);
}
console.log(arr[0]()) // 0
console.log(arr[1]()) // 1
console.log(arr[2]()) // 2
函數(shù)防抖、節(jié)流
debounce 與 throttle 是開發(fā)中常用的高階函數(shù),作用都是為了防止函數(shù)被高頻調(diào)用,換句話說就是,用來控制某個函數(shù)在一定時間內(nèi)執(zhí)行多少次。
使用場景
比如綁定響應(yīng)鼠標(biāo)移動、窗口大小調(diào)整、滾屏等事件時,綁定的函數(shù)觸發(fā)的頻率會很頻繁。若稍處理函數(shù)微復(fù)雜,需要較多的運算執(zhí)行時間和資源,往往會出現(xiàn)延遲,甚至導(dǎo)致假死或者卡頓感。為了優(yōu)化性能,這時就很有必要使用 debounce 或 throttle了。
debounce 與 throttle 區(qū)別
防抖 (debounce) :多次觸發(fā),只在最后一次觸發(fā)時,執(zhí)行目標(biāo)函數(shù)。
節(jié)流(throttle):限制目標(biāo)函數(shù)調(diào)用的頻率,比如:1s內(nèi)不能調(diào)用2次。
源碼實現(xiàn)
debounce
// 這個是用來獲取當(dāng)前時間戳的
function now() {
return +new Date()
}
/**
* 防抖函數(shù),返回函數(shù)連續(xù)調(diào)用時,空閑時間必須大于或等于 wait,func 才會執(zhí)行
*
* @param {function} func 回調(diào)函數(shù)
* @param {number} wait 表示時間窗口的間隔
* @param {boolean} immediate 設(shè)置為ture時,是否立即調(diào)用函數(shù)
* @return {function} 返回客戶調(diào)用函數(shù)
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延遲執(zhí)行函數(shù)
const later = () => setTimeout(() => {
// 延遲函數(shù)執(zhí)行完畢,清空緩存的定時器序號
timer = null
// 延遲執(zhí)行的情況下,函數(shù)會在延遲函數(shù)中執(zhí)行
// 使用到之前緩存的參數(shù)和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 這里返回的函數(shù)是每次實際調(diào)用的函數(shù)
return function(...params) {
// 如果沒有創(chuàng)建延遲執(zhí)行函數(shù)(later),就創(chuàng)建一個
if (!timer) {
timer = later()
// 如果是立即執(zhí)行,調(diào)用函數(shù)
// 否則緩存參數(shù)和調(diào)用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延遲執(zhí)行函數(shù)(later),調(diào)用的時候清除原來的并重新設(shè)定一個
// 這樣做延遲函數(shù)會重新計時
} else {
clearTimeout(timer)
timer = later()
}
}
}
throttle
/**
* underscore 節(jié)流函數(shù),返回函數(shù)連續(xù)調(diào)用時,func 執(zhí)行頻率限定為 次 / wait
*
* @param {function} func 回調(diào)函數(shù)
* @param {number} wait 表示時間窗口的間隔
* @param {object} options 如果想忽略開始函數(shù)的的調(diào)用,傳入{leading: false}。
* 如果想忽略結(jié)尾函數(shù)的調(diào)用,傳入{trailing: false}
* 兩者不能共存,否則函數(shù)不能執(zhí)行
* @return {function} 返回客戶調(diào)用函數(shù)
*/
_.throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
// 之前的時間戳
var previous = 0;
// 如果 options 沒傳則設(shè)為空對象
if (!options) options = {};
// 定時器回調(diào)函數(shù)
var later = function() {
// 如果設(shè)置了 leading,就將 previous 設(shè)為 0
// 用于下面函數(shù)的第一個 if 判斷
previous = options.leading === false ? 0 : _.now();
// 置空一是為了防止內(nèi)存泄漏,二是為了下面的定時器判斷
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
// 獲得當(dāng)前時間戳
var now = _.now();
// 首次進(jìn)入前者肯定為 true
// 如果需要第一次不執(zhí)行函數(shù)
// 就將上次時間戳設(shè)為當(dāng)前的
// 這樣在接下來計算 remaining 的值時會大于0
if (!previous && options.leading === false) previous = now;
// 計算剩余時間
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果當(dāng)前調(diào)用已經(jīng)大于上次調(diào)用時間 + wait
// 或者用戶手動調(diào)了時間
// 如果設(shè)置了 trailing,只會進(jìn)入這個條件
// 如果沒有設(shè)置 leading,那么第一次會進(jìn)入這個條件
// 還有一點,你可能會覺得開啟了定時器那么應(yīng)該不會進(jìn)入這個 if 條件了
// 其實還是會進(jìn)入的,因為定時器的延時
// 并不是準(zhǔn)確的時間,很可能你設(shè)置了2秒
// 但是他需要2.2秒才觸發(fā),這時候就會進(jìn)入這個條件
if (remaining <= 0 || remaining > wait) {
// 如果存在定時器就清理掉否則會調(diào)用二次回調(diào)
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 判斷是否設(shè)置了定時器和 trailing
// 沒有的話就開啟一個定時器
// 并且不能不能同時設(shè)置 leading 和 trailing
timeout = setTimeout(later, remaining);
}
return result;
};
};
柯里化
在計算機科學(xué)中,柯里化(Currying)是把接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)(最初函數(shù)的第一個參數(shù))的函數(shù),并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)的技術(shù)。這個技術(shù)由 Christopher Strachey 以邏輯學(xué)家 Haskell Curry 命名的,盡管它是 Moses Schnfinkel 和 Gottlob Frege 發(fā)明的。
var add = function(x) {
return function(y) {
return x + y;
};
};
var increment = add(1);
var addTen = add(10);
increment(2);
// 3
addTen(2);
// 12
add(1)(2);
// 3
這里定義了一個 add 函數(shù),它接受一個參數(shù)并返回一個新的函數(shù)。調(diào)用 add 之后,返回的函數(shù)就通過閉包的方式記住了 add 的第一個參數(shù)。所以說 bind 本身也是閉包的一種使用場景。
柯里化是將 f(a,b,c) 可以被以 f(a)(b)(c) 的形式被調(diào)用的轉(zhuǎn)化。JavaScript 實現(xiàn)版本通常保留函數(shù)被正常調(diào)用和在參數(shù)數(shù)量不夠的情況下返回偏函數(shù)這兩個特性。
模塊化
模塊化的目的在于將一個程序按照其功能做拆分,分成相互獨立的模塊,以便于每個模塊只包含與其功能相關(guān)的內(nèi)容,模塊之間通過接口調(diào)用。
模塊化開發(fā)和閉包息息相關(guān),通過模塊模式需要具備兩個必要條件可以看出:
外部必須是一個函數(shù),且函數(shù)必須至少被調(diào)用一次(每次調(diào)用產(chǎn)生的閉包作為新的模塊實例) 外部函數(shù)內(nèi)部至少有一個內(nèi)部函數(shù), 內(nèi)部函數(shù)用于修改和訪問各種內(nèi)部私有成員
function myModule (){
const moduleName = '我的自定義模塊'
var name = 'sisterAn'
// 在模塊內(nèi)定義方法(API)
function getName(){
console.log(name)
}
function modifyName(newName){
name = newName
}
// 模塊暴露: 向外暴露API
return {
getName,
modifyName
}
}
// 測試
const md = myModule()
md.getName() // 'sisterAn'
md.modifyName('PZ')
md.getName() // 'PZ'
// 模塊實例之間互不影響
const md2 = myModule()
md2.sayHello = function () {
console.log('hello')
}
console.log(md) // {getName: ?, modifyName: ?}
常見錯誤
在循環(huán)中創(chuàng)建閉包
var data = []
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i)
}
}
data[0]() // 3
data[1]() // 3
data[2]() // 3
這里的 i 是全局下的 i,共用一個作用域,當(dāng)函數(shù)被執(zhí)行的時候這時的 i=3,導(dǎo)致輸出的結(jié)構(gòu)都是3
方案一:閉包
var data = []
function myfunc(num) {
return function(){
console.log(num)
}
}
for (var i = 0; i < 3; i++) {
data[i] = myfunc(i)
}
data[0]() // 0
data[1]() // 1
data[2]() // 2
方案二:let
如果不想使用過多的閉包,你可以用 ES6 引入的 let 關(guān)鍵詞:
var data = []
for (let i = 0; i < 3; i++) {
data[i] = function () {
console.log(i)
}
}
data[0]() // 0
data[1]() // 1
data[2]() // 2
方案三:forEach
如果是數(shù)組的遍歷操作(如下例中的 arr ),還有一個可選方案是使用 forEach()來遍歷:
var data = []
var arr = [0, 1, 2]
arr.forEach(function (i) {
data[i] = function () {
console.log(i)
}
})
data[0]() // 0
data[1]() // 1
data[2]() // 2
來自:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/453
