欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文詳解JavaScript閉包典型應(yīng)用

 更新時(shí)間:2022年04月19日 10:59:27   作者:DarkHorse  
這篇文章主要介紹了關(guān)于Javascript閉包典型應(yīng)用,文中有非常詳細(xì)的代碼示例.對(duì)正在學(xué)習(xí)js的伙伴們有很好的幫助,需要的朋友可以參考下

1.應(yīng)用

以下這幾個(gè)方面是我們開發(fā)中最為常用到的,同時(shí)也是面試中回答比較穩(wěn)的幾個(gè)方面。

1.1 模擬私有變量

我們都知道JS是基于對(duì)象的語(yǔ)言,JS強(qiáng)調(diào)的是對(duì)象,而非類的概念,在ES6中,可以通過class關(guān)鍵字模擬類,生成對(duì)象實(shí)例。

通過class模擬出來的類,仍然無(wú)法實(shí)現(xiàn)傳統(tǒng)面向?qū)ο笳Z(yǔ)言中的一些能力 —— 比如私有變量的定義和使用。

我們通過看這樣一個(gè)User類來了解私有變量(偽代碼,不能直接運(yùn)行)

class User{
  constructor(username,password){
  // 用戶名
  this.username = username
  // 密碼
  this.password = password
  }
  
  login(){
    // 使用axious進(jìn)行登錄請(qǐng)求
    axios({
      method: 'GET',
      url: 'http://127.0.0.1/server', 
      params: {
        username,
        password
      },
    }).then(response => {
      console.log(response);
    });
  }
}

在這個(gè)User類里,我們定義了一些屬性,和一個(gè)login方法,我們嘗試輸出password這個(gè)屬性。

  let user = new User('小明',123456)
  user.password  // 123465

我們發(fā)現(xiàn),登錄密碼這么關(guān)鍵敏感的信息,竟然可以通過一個(gè)簡(jiǎn)單的屬性就可以拿到,這就意味著,后面人只有拿到user這個(gè)對(duì)象,就可以非常輕松的獲取,甚至改寫他的密碼。 在實(shí)際的業(yè)務(wù)開發(fā)中,這是一個(gè)非常危險(xiǎn)的操作,我們需要從代碼的層面保護(hù)password。

像password這樣變量,我們希望它只在函數(shù)內(nèi)部,或者對(duì)象內(nèi)部方法訪問到,外部無(wú)法觸及。 這樣的變量,就是私有變量,私有變量一般使用 _ 或雙 _ 定義。

在類里聲明變量的私有性,我們可以借助閉包實(shí)現(xiàn),我們的思路就是把我們把私有變量放在最外層立即執(zhí)行函數(shù)中,并通過立即執(zhí)行User這個(gè)函數(shù),創(chuàng)造了一個(gè)閉包作用域的環(huán)境。

// 利用IIFE生成閉包,返回user類
const User = (function () {
    // 定義私有變量_password
    let _password

    class User {
        constructor(username, password) {
            // 初始化私有變量_password
            _password = password
            this.username = username
        }

        login() {
            console.log(this.username, _password)

        }
    }

    return User
})()

let user = new User('小明',123465)
console.log(user.username); // 小明
console.log(user.password); // undefined
console.log(user._password); //undefined
user.login(); // 小明 undefined

在這段代碼中,私有變量_password被好好的保護(hù)在User這個(gè)立即執(zhí)行函數(shù)內(nèi)部,此時(shí)實(shí)例暴露的屬性已經(jīng)沒有_password,通過閉包,我們成功利用了自由變量模擬私有變量的效果。

1.2 柯里化

定義一個(gè)函數(shù),該函數(shù)返回一個(gè)函數(shù)。 柯里化是把接收 n個(gè)參數(shù)的1個(gè)函數(shù)改造為只接收1個(gè)參數(shù)的n個(gè)互相嵌套的函數(shù)的過程。也就是從fn(a,b,c)變成fn(a)(b)(c)。

我們通過以下案例進(jìn)行深入理解:以慕課網(wǎng)為例,我們使用site(站點(diǎn))、type(課程類型)、name(課程名稱)三個(gè)字符串拼接的方式為課程生成一個(gè)完整版名稱。對(duì)應(yīng)方法如下:

function generateName(site,type,name){
  return site + type + name
}

我們看到這個(gè)函數(shù)需要傳遞三個(gè)參數(shù),此時(shí)如果我是課程運(yùn)營(yíng)負(fù)責(zé)人,如我只負(fù)責(zé)“體系課”的業(yè)務(wù),那么我每次生成課程時(shí),都會(huì)固定傳參site,像這樣傳參:

generateName('體系課',type,name)

如果我是細(xì)分工種的前端助教,我僅僅負(fù)責(zé)“體系課”站點(diǎn)下的“前端”課程,那么我進(jìn)行傳參就是這樣:

generateName('體系課','前端',name)

我們不難發(fā)現(xiàn),調(diào)用generateName時(shí),真正的變量只有一個(gè),但是我每次不得不把前兩個(gè)參數(shù)手動(dòng)傳一遍。此時(shí),我們的柯里化就出現(xiàn)了,柯里化可以幫助我們?cè)诒匾闆r下,記住一部分參數(shù)。

function generateName(site){
  // var site = '體系課'
  return function(type){
    // var type = '前端'
    return function(name){
      // var name = '零基礎(chǔ)就業(yè)班'
      return prefix + type + name
    }
  }
}

// 生成體系課專屬函數(shù)
var salesName = generateName('體系課');

// “記住”site,生成體系課前端課程專屬函數(shù)
var salesBabyName = salesName('前端')

// 輸出 '體系課前端零基礎(chǔ)就業(yè)班'
res = salesBabyName('零基礎(chǔ)就業(yè)班')
console.log(res)

我們可以看到,在生成體系課專屬函數(shù)中,我們將site作為實(shí)參傳遞給generateName函數(shù)中,將site的值保留在generateName內(nèi)部作用域中。

在生成體系課前端課程函數(shù)中,將type的值保留在salesBabyName函數(shù)中,最終調(diào)用salesBabyName函數(shù),輸出。

這樣一來,原有的generateName (site, type, name)函數(shù)經(jīng)過柯里化變成了generateName(site)(type)(name)。通過后者這種形式,我們可以記住一部分形參,選擇性的傳遞參數(shù),從而編寫出更符合預(yù)期,復(fù)用性更高的函數(shù)。

function generateName(site){
   // var site = '實(shí)戰(zhàn)課'
   return function(type){
     // var type = 'Java'
     return function(name){
       // var name = '零基礎(chǔ)'
       return site + type + name
      }
    }
}
  
// "記住“site和type,生成實(shí)戰(zhàn)課java專屬函數(shù)
var shiZhanName = generateName('實(shí)戰(zhàn)課')('Java')
console.log(shiZhanName);

// 輸出 '實(shí)戰(zhàn)課java零基礎(chǔ)'
var res = shiZhanName('零基礎(chǔ)')
console.log(res)

 // 啥也不記,直接生成一個(gè)完整課程
var itemFullName = generateName('實(shí)戰(zhàn)課')('大數(shù)據(jù)')('零基礎(chǔ)')
console.log(itemFullName);

1.3 偏函數(shù)

偏函數(shù)和柯里化類似,如果理解了柯里化,那么偏函數(shù)就小菜一碟了。

柯里化是將一個(gè)n個(gè)參數(shù)的函數(shù)轉(zhuǎn)化成n個(gè)單參數(shù)函數(shù),這里假如你有三個(gè)入?yún)?,你得嵌套三層函?shù),且每層函數(shù)只能有一個(gè)入?yún)?。柯里化的目?biāo)是把函數(shù)拆解為精準(zhǔn)的n部分。

偏函數(shù)相比之下就比較隨意了,偏函數(shù)是固定函數(shù)中的某一個(gè)或幾個(gè)參數(shù),然后返回一個(gè)新的函數(shù)。假如你有三個(gè)入?yún)?,你可以只固定一個(gè)入?yún)ⅲ缓蠓祷亓硪粋€(gè)入?yún)⒑瘮?shù)。也就是說,偏函數(shù)應(yīng)用是不強(qiáng)調(diào) “單參數(shù)” 這個(gè)概念的。

仍然是上面的例子,原函數(shù)形式調(diào)用:

function generateName(site,type,name){
  return site + type + name;
}

// 調(diào)用時(shí)傳入三個(gè)參數(shù)
var itemFullName = generateName('體系課', '前端', '2022')

偏函數(shù)改造:

function generateName(site){
    return function(type,name){
      return site + type + name
    }
}
// 把3個(gè)參數(shù)分兩部分傳入
var itemFullName = generateName('體系課')('前端', '2022')

1.4 防抖

在瀏覽器的各種事件中,有一些容易頻繁觸發(fā)的事件,比如scroll、resize、鼠標(biāo)事件(比如 mousemove、mouseover)、鍵盤事件(keyup、keydown )等。頻繁觸發(fā)回調(diào)導(dǎo)致大量的計(jì)算會(huì)引發(fā)頁(yè)面抖動(dòng)甚至卡頓,影響瀏覽器性能。防抖和節(jié)流就是控制事件觸發(fā)的頻率的兩種手段。

防抖的中心思想是:在某段時(shí)間內(nèi),不管你觸發(fā)了多少次回調(diào),我都只執(zhí)行最后一次。

// fn是我們需要包裝的事件回調(diào), delay是每次推遲執(zhí)行的等待時(shí)間
function debounce(fn, delay) {
  // 定時(shí)器
  let timer = null
  
  // 將debounce處理結(jié)果當(dāng)作函數(shù)返回
  return function () {
    // 保留調(diào)用時(shí)的this上下文
    let context = this
    // 保留調(diào)用時(shí)傳入的參數(shù)
    let args = arguments
    // 每次事件被觸發(fā)時(shí),都去清除之前的舊定時(shí)器
    if(timer) {
        clearTimeout(timer)
    }
    // 設(shè)立新定時(shí)器
    timer = setTimeout(function () {
      fn.apply(context, args)
    }, delay)
  }
}
// 用debounce來包裝scroll的回調(diào)
const better_scroll = debounce(() => console.log('觸發(fā)了滾動(dòng)事件'), 1000)
document.addEventListener('scroll', better_scroll)

1.5 節(jié)流

節(jié)流的中心思想是:在某段時(shí)間內(nèi),不管你觸發(fā)了多少次回調(diào),我都只認(rèn)第一次,并在計(jì)時(shí)結(jié)束時(shí)給予響應(yīng),也就是隔一段時(shí)間執(zhí)行一次。

// fn是我們需要包裝的事件回調(diào), interval是時(shí)間間隔的閾值
function throttle(fn, interval) {
  // last為上一次觸發(fā)回調(diào)的時(shí)間
  let last = 0
  
  // 將throttle處理結(jié)果當(dāng)作函數(shù)返回
  return function () {
      // 保留調(diào)用時(shí)的this上下文
      let context = this
      // 保留調(diào)用時(shí)傳入的參數(shù)
      let args = arguments
      // 記錄本次觸發(fā)回調(diào)的時(shí)間
      let now = +new Date()
      
      // 判斷上次觸發(fā)的時(shí)間和本次觸發(fā)的時(shí)間差是否小于時(shí)間間隔的閾值
      if (now - last >= interval) {
      // 如果時(shí)間間隔大于我們?cè)O(shè)定的時(shí)間間隔閾值,則執(zhí)行回調(diào)
          last = now;
          fn.apply(context, args);
      }
    }
}
// 用throttle來包裝scroll的回調(diào)
const better_scroll = throttle(() => console.log('觸發(fā)了滾動(dòng)事件'), 1000)
document.addEventListener('scroll', better_scroll)

2.性能問題

以上我們講解了閉包的常見應(yīng)用,可見閉包是一個(gè)非常強(qiáng)大的特性,但人們對(duì)其也有諸多誤解。一種聳人聽聞的說法是閉包會(huì)造成內(nèi)存泄露,所以要盡量減少閉包的使用。真的是這樣嗎?

2.1 內(nèi)存泄漏

該釋放的變量沒有釋放,依然占據(jù)著內(nèi)存空間,導(dǎo)致內(nèi)存占用不斷攀高,帶來性能惡化,系統(tǒng)崩潰等一系列問題,這種現(xiàn)象叫做內(nèi)存泄漏。

閉包里的變量是我們需要的變量本就不該釋放,而又怎么稱之為內(nèi)存泄漏呢?

所以有關(guān)內(nèi)存泄露問題,是謠言,是誤傳。

這個(gè)誤傳來源于IE,IE在我們使用完閉包之后,依然會(huì)受不了里面的變量,而這是IE的bug,不是閉包問題。

如果還不放心,我們來看以下例子:

function f1(){
    var num  = Math.randon();
    function f2(){
      return num
    } 
    return f2
}

var f = f1();
f();

上面這段代碼,f2函數(shù)中存在對(duì)變量num的引用,所以num變量并不會(huì)回收,我們可以,在函數(shù)調(diào)用后,可以把外部引用關(guān)系置空,如下:

function f1(){
    var num  = Math.randon();
    function f2(){
      return num
    } 
    return f2
}

var f = f1();
f();
f = null;

事實(shí)上,閉包導(dǎo)致的內(nèi)存泄漏是誤傳,閉包中引用的變量,其實(shí)也就相當(dāng)于一個(gè)全局變量,并不會(huì)構(gòu)成內(nèi)存泄漏問題,內(nèi)存泄漏大多原因是由于代碼不規(guī)范導(dǎo)致。

2.2 常見的內(nèi)存泄漏

2.21 不必要的全局變量

function f1() {
  name = '小明'
}

在非嚴(yán)格模式下引用未聲明的變量,會(huì)在全局對(duì)象中創(chuàng)建一個(gè)新變量,在瀏覽器中,全局對(duì)象是window,這就意味著name這個(gè)變量將泄漏到全局。全局變量是在網(wǎng)頁(yè)關(guān)閉時(shí)才會(huì)釋放,這樣的變量一多,內(nèi)存壓力也會(huì)隨之增高。

2.22 遺忘清理的計(jì)時(shí)器

程序中我們經(jīng)常會(huì)用到計(jì)時(shí)器,也就是setInterval和setTimeout

var timeId = setInterval(function(){
  // 函數(shù)體
},1000)

在計(jì)時(shí)器中,定時(shí)器內(nèi)部邏輯是是無(wú)窮無(wú)盡的,當(dāng)定時(shí)器囊括的函數(shù)邏輯不再被需要、而我們又忘記手動(dòng)清除定時(shí)器時(shí),它們就會(huì)永遠(yuǎn)保持對(duì)內(nèi)存的占用。因此當(dāng)我們使用定時(shí)器時(shí),一定要明確計(jì)時(shí)器在何時(shí)會(huì)被清除,并使用 clearInterval(timeId)手動(dòng)清除定時(shí)器。

2.23 遺忘清理的dom元素引用

var divObj = document.getElementById('mydiv')

// dom刪除myDiv
document.body.removeChild(divObj);
console.log(divObj);
// 能console出整個(gè)div 說明沒有被回收,引用存在

// 移出引用
divObj = null;
console.log(divObj) 
// null

3.閉包與循環(huán)體

閉包和循環(huán)體的結(jié)合,是閉包最為經(jīng)典的一種考察方式。

3.1 這段代碼輸出啥

我們來看一個(gè)大家非常熟悉的題目,以上6行代碼輸出什么?

for(var i=0; i<5; i++){
  setTimeout(function(){
    console.log(i)
  },1000)
}
console.log(i)

如果你是剛?cè)腴T的新手,你可能會(huì)給出這樣的答案:

0 1 2 3 4 5  

給出這樣答案的同學(xué),內(nèi)心一般都是這樣想:for循環(huán)輸出了0-4個(gè)i的值,最后一行打印5,setTimeout這個(gè)好像在哪見過,但具體咋回事印象不深了,干脆直接忽略好了。

對(duì)于基礎(chǔ)還不錯(cuò)的同學(xué),對(duì)于setTimeout函數(shù)用法特性還有印象,很快就給出了“進(jìn)化版”答案:

5 0 1 2 3 4  

這一部分的同學(xué)是這樣想的:for循環(huán)逐個(gè)輸出0-4的值,但是setTimeout把輸入延遲了1s,所以最后一行先執(zhí)行,先輸出5,然后過了1000ms,0-4會(huì)逐個(gè)輸出。

如果你對(duì)JS中的for循環(huán)、同步與異步區(qū)別、變量作用域、閉包有正確理解,就知道正確答案應(yīng)該是:

5 5 5 5 5 5 

我們?cè)囍治鲆幌抡_答案,seTimeout內(nèi)函數(shù)延遲1000ms后執(zhí)行,最后一行console先輸出,最后一行輸出5,所以第一個(gè)值是5。

for(var i =0;i<5;i++){
  // 5<5? 不滿足 
}
console.log(i) // 5

for循環(huán)里setTimeout執(zhí)行了5次,函數(shù)延遲1000ms執(zhí)行,大家看這個(gè)函數(shù),它自身作用域壓根就沒有i這個(gè)變量,根據(jù)作用域鏈查找規(guī)則,要想輸出i,需要去上層查找。

setTimeout(function() {
   console.log(i);
}, 1000);

但是,這個(gè)函數(shù)第一次被執(zhí)行也是1000ms以后的事情了,此時(shí)它試圖向上一層作用域(這里也就是全局作用域)去找一個(gè)叫i的變量,此時(shí)for循環(huán)已執(zhí)行完畢,i也進(jìn)入了最終狀態(tài)5。所以當(dāng)1000ms后,這個(gè)函數(shù)真正被執(zhí)行的時(shí)候,引用到的i值已經(jīng)是5了。 此時(shí),這段代碼的作用域狀態(tài)示意如下:

對(duì)應(yīng)的作用域關(guān)系如下:

接下來的連續(xù)四次,都會(huì)有一個(gè)一模一樣的setTimeout回調(diào)被執(zhí)行,它輸出的也是同一個(gè)全局的i,所以說每一次輸出都是5。

3.2 改造方法

循環(huán)了五次,每次卻輸出一個(gè)值,這種輸出效果顯然不好。如果我們希望讓i從0-4依次被輸出,我們改如何改造呢?

方案一:利用setTimeout中第三個(gè)參數(shù)

開頭我們先復(fù)習(xí)一下setTimeout參數(shù)用法:

setTimeout(function(arg1,arg2){
  console.log(arg1);
  console.log(arg2);
},delay,arg1,arg2)
  • function(必須):調(diào)用函數(shù)執(zhí)行的代碼塊
  • delay(可選):函數(shù)調(diào)用延遲的毫秒值,默認(rèn)是0,意味著馬上執(zhí)行
  • arg1,...arg2(可選):附加參數(shù),當(dāng)計(jì)時(shí)器啟動(dòng)時(shí),會(huì)作為參數(shù)傳遞給function

我們來看例子:

setTimeout(function(a,b){
  console.log(a);  // 1
  console.log(b);  // 2
},1000,1,2)

需要注意的一點(diǎn)是,附加參數(shù)只支持在ie9及以上瀏覽器,如要兼容,需要引入一段MDN提供的兼容舊IE代碼。

利用setTimeout的第三個(gè)參數(shù),i作為形參傳遞給setTimeout的j,由于每次傳入的參數(shù)是從for循環(huán)里面取到的值,所以會(huì)依次輸出0~4:

for(var i=0; i<5; i++){
  setTimeout(function(j){
    console.log(j) // 0 1 2 3 4 
  },1000,i)
}

方案二:使用閉包

使用閉包,我們往往會(huì)用到匿名函數(shù)。匿名函數(shù)也叫一次性函數(shù),在函數(shù)定義時(shí)執(zhí)行,且只執(zhí)行一次。我們?cè)趕etTimeout外面套一個(gè)匿名函數(shù),利用匿名函數(shù)的實(shí)參來緩存每一個(gè)循環(huán)的i值。

for(var i= 0; i<5; i++){
  (function(j){
    setTimeout(function(){
      console.log(j)
    },1000)
  })(i)
}

當(dāng)輸出j時(shí),引用的是外部函數(shù)傳遞的變量i,這個(gè)i是根據(jù)循環(huán)來的,執(zhí)行setTimeout時(shí)已經(jīng)確定了里面i的值,進(jìn)而確定了j的值。

方案三:使用let

for(let i= 0; i<5; i++){
  setTimeout(function(){
    console.log(i)
  },1000)
 }

for循環(huán)每次循環(huán)產(chǎn)生一個(gè)新的塊級(jí)作用域,每個(gè)塊級(jí)作用域的變量是不同的。函數(shù)輸出的是自己的上一級(jí)(循環(huán)產(chǎn)生的塊級(jí)作用域)下i的值。

4.總結(jié)

  • 作用:模擬私有變量、柯里化、偏函數(shù)、防抖、節(jié)流、實(shí)現(xiàn)緩存。
  • 模擬私有變量:將私有變量放在外在的立即執(zhí)行函數(shù)中,并通過立即執(zhí)行U這個(gè)函數(shù),創(chuàng)造一個(gè)閉包環(huán)境(私有變量:只允許函數(shù)內(nèi)部,或?qū)ο蠓椒ㄔL問的變量)。
  • 柯里化:把接受n個(gè)參數(shù)的一個(gè)函數(shù)轉(zhuǎn)化成只接受一個(gè)參數(shù)n個(gè)函數(shù)互相嵌套的函數(shù)過程,目標(biāo)是把函數(shù)拆解為精準(zhǔn)的n部分,也就是將fn(a,b,c)轉(zhuǎn)化成fn(a)(b)(c)的過程。
  • 偏函數(shù):固定函數(shù)中的某一個(gè)或幾個(gè)參數(shù),然后返回一個(gè)新的函數(shù),不強(qiáng)調(diào)但函數(shù)。
  • 防抖:只執(zhí)行最后一次。
  • 節(jié)流:隔一段時(shí)間執(zhí)行一次。
  • 緩存變量:計(jì)時(shí)器打印問題。
  • 閉包造成內(nèi)存泄露問題是誤傳,誤傳來源于IE瀏覽器bug,可以放心大膽使用。

以上就是一文詳解JavaScript閉包典型應(yīng)用的詳細(xì)內(nèi)容,更多關(guān)于JavaScript閉包典型應(yīng)用的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論