javascript閉包總結

来源:http://www.cnblogs.com/gongyue/archive/2017/11/15/7838619.html
-Advertisement-
Play Games

閉包是javascript中一個十分常見但又很難掌握的概念,無論是自己寫代碼還是閱讀別人的代碼都會接觸到大量閉包。之前也沒有系統學習過,最近系統地總結了一些閉包的使用方法和使用場景,這裡做個記錄,希望大家指正補充。 一、定義 《JavaScript忍者秘籍》中對於閉包的定義是這樣的: 閉包是一個函數 ...


閉包是javascript中一個十分常見但又很難掌握的概念,無論是自己寫代碼還是閱讀別人的代碼都會接觸到大量閉包。之前也沒有系統學習過,最近系統地總結了一些閉包的使用方法和使用場景,這裡做個記錄,希望大家指正補充。

一、定義 
      《JavaScript忍者秘籍》中對於閉包的定義是這樣的:

閉包是一個函數在創建時允許該自身函數訪問並操作該自身函數之外的變數時所創建的作用域。換句話說,閉包可以讓函數訪問所有的變數和函數,只要這些變數和函數存在於該函數聲明時的作用域內就行。

註意:這裡說的是創建時,而不是調用時。

二、外部操作函數私有變數 
      正常來講,函數可以聲明一個塊級作用域,作用域內部對外部是不可見的,如:

function P(){
    var innerValue = 1
}
var p = new P()
console.log(p.innerValue) //輸出undefined

 

但是,閉包可以讓我們能夠訪問私有變數:

function P(){
    var innerValue = 1
    this.getValue = function(){
        console.log(innerValue)
    }
    this.setValue = function(newValue){
        innerValue = newValue
    }
}
var p = new P()
console.log(p.getValue()) //1
p.setValue(2)
console.log(p.getValue()) //2

 

 

三、只要有回調的地方就有閉包 
      這可能是我們在日常開發中接觸閉包最多的場景,可能有些同學還沒有意識到這就是閉包,舉個例子:

function bindEvent(name, selector) {
    document.getElementById(selector).addEventListener('click',function () {
        console.log( "Activating: " + name );
    } );
} 
bindEvent( "Closure 1", "test1" );
bindEvent( "Closure 2", "test2" );

 

執行了兩次bindEvent函數後,最後傳入的name是Closure 2,為什麼點擊id為test1的按鈕輸出的不是Closure 2而是Closure 1?這當然是閉包幫我們記住了每次調用bindEvent時的入參name。 
通過chrome調試可以看到,匿名函數裡面實現了一個引用name的閉包

四、綁定函數上下文(bind方法的實現) 
      先看一段代碼:

HTML:
    <button id="test1">click1</button>
Js:
    var elem = document.getElementById('test1')
    var aHello = {
        name : "hello",
        showName : function(){
            console.log(this.name);
        }
    }
    elem.onclick = aHello.showName

web前端/H5/javascript學習群:250777811

歡迎關註此公眾號→【web前端EDU】跟大佬一起學前端!歡迎大家留言討論一起轉發

當點擊按鈕時會有什麼現象呢?會輸出“hello”嗎?結果是會輸出something,但是輸出的不是“hello”,而是空。為什麼呢?顯然是“this.name”的this搞的鬼,原來當我們綁定事件後觸發這個事件,瀏覽器會自動把函數調用上下文切換到目標元素(本例中是id為test1的button元素)。所以this是指向button按鈕的,並不是aHello 對象,所以沒有輸出“hello”。 
      那麼我們如何將代碼改成我們想要的樣子呢? 
1. 最常用的方式就是用一個匿名函數將showName包裝一下:

elem.onclick = function(){
    aHello.showName()
}
通過這樣使aHello來調用showName,這樣this就指向aHello了。 

2. 使用bind函數來改變上下文

elem.onclick = aHello.showName.bind(aHello)

強行把this指向aHello對象,再點擊按鈕,就能正常輸出“hello”了。是不是很神奇?那麼如果讓你來實現bind函數,怎麼寫呢?我簡單寫了一個:

Function.prototype.bind = function(){
    var fun = this; //指向aHello.showName函數
    var obj = Array.prototype.slice.call(arguments).shift(); //這裡沒有處理多個參數,假設只有一個參數
    return function(){
        fun.apply(obj)
    }
}
 

核心代碼是使用apply方法來改變this的指向,通過閉包來記住調用bind函數的函數,還有bind函數的入參。

五、函數柯里化 
      有同學可能會問柯里化是什麼?先看一個例子: 
      假如有一個求和函數:

function add(a,b){
    return a + b
}
console.log(add(1,2)) //3

 

如果是柯里化的寫法:

function add(a){
    return function(b){
        return a+b
    }
}
console.log(add(1)(2)) //3

 

來看百度百科中柯里化的定義:

把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,並且返回接受餘下的參數且返回結果的新函數的技術。

通俗來講,柯里化也叫部分求值(不會立刻求值,而是到了需要的時候再去求值),是一個延遲計算的過程。之所以能延遲,少不了閉包來記錄參數。來看一個更有深度的例子,這是社區中某大神丟出的一個問題:

完成plus函數,通過全部的測試用例 
function plus(n){} 
module.exports = plus

var assert = require('assert')

var plus = require('../lib/assign-4')

describe('閉包應用',function(){
  it('plus(0) === 0',function(){
    assert.equal(0,plus(0).sum())
  })
  it('plus(1)(1)(2)(3)(5) === 12',function(){
    assert.equal(12,plus(1)(1)(2)(3)(5).sum())
  })
  it('plus(1)(4)(2)(3) === 10',function(){
    assert.equal(10,plus(1)(4)(2)(3).sum())
  })
  it('方法引用',function(){
    var plus2 = plus(1)(1)
    assert.equal(12,plus2(1)(4)(2)(3).sum())
  })
})

 

整理思路時考慮到以下幾點: 
1. plus()()這種調用方式意味著plus函數的返回值一定是個函數,而且由於後面括弧的個數並沒有限制,想到plus函數是在遞歸調用自己。 
2. plus所有的入參都應該保存起來,可以建一個數組來保存,而這個數組是要放在閉包中的。 
3. plus()().sum(),sum的調用形式意味著sum應該是plus的一個屬性,而且最終的求和計算是sum來完成的

基於這幾點,我寫了一個plus函數:

var plus1 = function(){
    var arr = []

    var f = function(){
        f.sum = function(){
            return arr.reduce(function(total, curvalue){
                return total + curvalue
            }, 0)
        }
        Array.prototype.push.apply(arr, Array.prototype.slice.call(arguments))
        return arguments.callee
    }
    return f
}
var plus = plus1()

 

六、緩存記憶功能 
      有些函數的操作可能比較費時,比如做複雜計算。這時就需要用緩存來提高運行效率,降低運行環境壓力。以前我通常的做法是直接搞個全局對象,然後以鍵值對的形式將函數的入參和結果存到這個對象中,如果函數的入參在該對象中能查到,那就根據鍵讀出值返回就好,不用重新計算。 
      這種全局對象的搞法肯定不具有通用性,所以我們想到使用閉包,來看一個《JavaScript忍者秘籍》中的例子:

Function.prototype.memoized = function(key){
    this._values = this._values || {} //this指向function(num){...}函數
    return this._values[key] !== undefined ? this._values[key] : this._values[key] = this.apply(this, arguments);
}
Function.prototype.memoize = function(){
    var fn = this; //this指向function(num){...}函數
    return function(){
        return fn.memoized.apply(fn, arguments)
    }
}
var isPrime = (function(num){
    console.log("沒有緩存")
    var prime = num != 1;//1不是質數
    for(var i = 2;i < num; i++){
        if(num % i == 0){
            prime = false;
            break;
        }
    }
    return prime
}).memoize()

 

測試執行:

console.log(isPrime(5))
console.log(isPrime(5))

輸出:

沒有緩存
true
true

 

該例子巧妙地利用閉包將緩存存在計算函數的一個屬性中,而且實現了緩存函數與計算函數的解耦,使得緩存函數具有通用性。

七、即時函數IIFE 
      先來看代碼:

var p = (function(){
    var a = 0
    return function(){
        console.log(++a)
    }
})()
p() //1
p() //2
p() //3

web前端/H5/javascript學習群:250777811

歡迎關註此公眾號→【web前端EDU】跟大佬一起學前端!歡迎大家留言討論一起轉發

      有了IIFE和閉包,這種功能再也不需要全局變數了。所以,IIFE的一個作用就是創建一個獨立的、臨時的作用域,這也是後面要說的模塊化實現的基礎。

      再來看一個基本所有前端都遇到過的面試題:

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

 

      大家都知道這段代碼會在1s後列印5個6,為什麼會這樣呢?因為timer中每次列印的i和for迴圈裡面的i是同一個變數,所以當1s後要列印時,迴圈早已跑完,i的值定格在6,故列印5個6。 
      那麼,怎麼輸出1,2,3,4,5呢? 
答案就是使用IIFE:

for (var j=1; j<=5; j++) {
    (function(n){
        setTimeout(function timer() {
            console.log( n );
        }, 1000 )
    })(j)
}

 

通過在for迴圈中加入即時函數,我們可以將正確的值傳給即時函數(也就是內部函數的閉包),在for迴圈每次迭代的作用域中,j變數都會被重新定義,從而給timer的閉包傳入我們期望的值。 
      當然,在ES6的時代,大可不必這麼麻煩,上代碼:

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

 

問題解決!具體原因,大家請自行百度…

八、模塊機制 
      先看一個最簡單的函數實現模塊封裝的例子:

function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( " ! " ) );
    }
    return {
        doSomething: doSomething,
        doAnother: doAnother
    };
}

var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

 

      簡單分析一下,創建實例的過程就是執行構造函數的過程,執行後產生閉包,閉包使我們能達到使用模塊來封裝數據、函數的目的。再來看返回值,是不是有點“export”的意思,將函數封裝成一個對象return出來。 
      模塊模式的兩個必要條件: 
1. 必須有外部的封閉函數, 該函數必須至少被調用一次(每次調用都會創建一個新的模塊實例)。 
2. 封閉函數必須返回至少一個內部函數, 這樣內部函數才能在私有作用域中形成閉包, 並且可以訪問或者修改私有的狀態。

      上面的代碼每調用一次就會創建一個實例,如果只需要一個實例,可使用單例模式:

var foo = (function CoolModule() {
    var something = "cool";
    var another = [1, 2, 3];
    function doSomething() {
        console.log( something );
    }
    function doAnother() {
        console.log( another.join( " ! " ) );
    }
    return {
        doSomething: doSomething,
        doAnother: doAnother
    };
})();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

 

      在ES6的import和export之前,大多數模塊載入庫的核心代碼基本如下:

var MyModules = (function Manager() {
    var modules = {};
    function define(name, deps, impl) {
        for (var i=0; i<deps.length; i++) {
            deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply( impl, deps );
    }
    function get(name) {
        return modules[name];
    }
    return {
        define: define,
        get: get
    };
})();

 

這段代碼的核心是 modules[name] = impl.apply(impl, deps)。 為了模塊的定義引入了包裝函數(可以傳入任何依賴), 並且將返回值, 也就是模塊的 API, 儲存在一個根據名字來管理的模塊列表中。

下麵展示瞭如何使用它來定義模塊:

MyModules.define( "bar", [], function() {
    function hello(who) {
        return "Let me introduce: " + who;
    }
    return {
        hello: hello
    };
});
MyModules.define( "foo", ["bar"], function(bar) {
    var hungry = "hippo";
    function awesome() {
        console.log( bar.hello( hungry ).toUpperCase() );
    }
    return {
        awesome: awesome
    };
});
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log(bar.hello( "hippo" )); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

web前端/H5/javascript學習群:250777811

歡迎關註此公眾號→【web前端EDU】跟大佬一起學前端!歡迎大家留言討論一起轉發

      這是一個很基礎的模擬模塊載入器的代碼,但是十分經典,完整的向我們展示了閉包在其中的作用。

小結: 
      以上閉包的用法都是在學習和工作中可能遇到的比較常見的用法,相信在掌握這些用法後自己對閉包的認識會上一個臺階,起碼在閱讀源碼時,對這塊不會有太多困難。 
      最後,有什麼問題或者不對的地方歡迎大家在評論區指正,後面我也會繼續完善此文。

參考文獻:

《你不知道的JavaScript》 
《JavaScript忍者秘籍》


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 應用場景:一些圖片較多的頁面,一些需要加上進度條或者百分比讀取等載入效果的頁面,一般移動端頁面用得比較多 ...
  • 如今網站幾乎100%使用JavaScript。JavaScript看上去是一門十分簡單的語言,然而事實並不如此。它有很多容易被弄錯的細節,一不註意就導致BUG。 1. 錯誤的對this進行引用 在閉包或則回調中,this關鍵字的作用域很容易弄錯。舉個例子: Game.prototype.restar ...
  • CSS3基礎 1 樣式表的使用 1.內聯樣式表。 隻影響單個元素,常用於標簽。 <p style="color: aqua;font-size: 20px">This is CSS.</p> 2.內部樣式表。 對本頁面元素起作用,一般寫在<head></head>中,用<style></style> ...
  • js的函數傳參的方式是按值傳遞,正常情況下,改變函數參數的值,並不會對函數外部的變數造成影響。例如: 這是因為js的函數在接收參數時,會生成一個副本變數,該副本變數等於參數的值,可以分析js這樣運行的: 但是當函數的參數傳遞的是一個對象呢? 發現函數內部居然改變了函數外部變數的值,那這又是為什麼呢? ...
  • 在網上找到一個練手項目,記錄一下自己的實現過程和遇到的問題 附上鏈接 前端練手項目-先定一個小目標,做他一個天貓官網 前端練手項目-天貓官網 先確定一下需要幾個頁面 首先要有公共頁面 其次是 步驟 1 : 首頁 步驟 2 : 分類頁 步驟 3 : 查詢結果頁 步驟 4 : 產品頁 步驟 5 : 結算 ...
  • 案例1:效果 代碼: 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 2 <html xmlns=" ...
  • 在面向對象的程式設計里,一般都提供了實現隊列(queue)和堆棧(stack)的方法,而對於JS來說,我們可以實現數組的相關操作,來實現隊列和堆棧的功能,看下麵的相關介紹. 一 看一下它們的性質,這種性質決定了它們的使用場合 隊列:是一種支持先進先出(FIFO)的集合,即先被插入的數據,先被取出! ...
  • 什麼是變數:存放物體的一個容器,以便後續利用該容器存放的物體。 變數的聲明及賦值: 聲明變數關鍵字var; 變數名的規範:變數名由英文字母、數字、下劃線、美元符號組成,但是首字母只能是英文字母、下劃線、美元符號; 聲明變數使用單一var模式(多個變數只用一個var,因為每出現一個var就需要向系統請 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...