JS 函數式編程思維簡述(四):閉包 01

  1. 簡述
  2. 無副作用(No Side Effects)
  3. 高階函數(High-Order Function)
  4. 柯里化(Currying)
  5. 閉包(Closure)
    -- JavaScript 作用域
    -- 面向對象關系
    -- this調用規則
    -- 配置多樣化的構造重載
    -- 更多對象關系維護——模塊化
    -- 流行的模塊化方案
  6. 不可變(Immutable)
  7. 惰性計算(Lazy Evaluation)
  8. Monad

閉包(Closure)

? ? ? ?閉包(Closure)是函數式編程的重要特性,是緩存函數內部作用域,并且對外暴露的過程;是模塊化編程的基石。在探討這部分的過程中,我們將主要探討這幾個問題:

  • JavaScript 作用域;
  • 面向對象關系;
  • this調用規則;
  • 配置多樣化的構造重載;
  • 更多對象關系維護——模塊化;
  • 流行的模塊化方案;

讓我們帶著這些問題,一步一步進入主題,了解閉包產生的過程和必要性。

5.1 JavaScript 作用域

image

? ? ? ?我們經常用房間內外,來描述作用域的概念。作用域在編程語言中是一個廣泛的概念,主要是指變量可應用的范圍。應用范圍的區間廣義上分為全局環境局部環境,在運行機制上則是指變量被銷毀的時機。直接上一個簡單的例子:

// 全局作用域中
const num1 = 10;

// 聲明一個全局作用域中可調用的函數
function foo(){
    // 函數內部作為 局部作用域 ,此處聲明的變量只有該函數內部持有
    const num2 = 29;

    console.log('局部  num1:', num1);
    console.log('局部  num2:', num2);
}

// 調用函數 foo()
// 子作用域中,共享父作用域中的變量,因此打印結果為:
// 局部  num1:10
// 局部  num2:29
foo();

// 但是,如果在外層的父作用域調用局部的自作用域中聲明的變量:
console.log('全局  num1:', num1); // 結果:10
console.log('全局  num2:', num2); // 錯誤:ReferenceError: num2 is not defined

JavaScript 中,通常情況下一對 { } 會限制一段代碼運行的作用域,但也有特殊情況,比如:

  • 當使用 { } 描述一個對象字面量值時,其表示一個對象常量;
  • { } 所描述的作用域是選擇、循環結構,且其內部使用 var 關鍵字定義了變量時,var 關鍵字聲明的變量,將可以在 { } 作用域之外引用。因此該行為不推薦,ES2015 引入的新的更加嚴格的聲明變量的關鍵字 constlet 來解決 var 描述作用域不明確的歷史問題。

? ? ? ?在 JavaScript 內部,運行著一套與 Java 相似的自動垃圾回收機制,用于將之后不再使用的數據,在內存中清除掉。因此我們可以以這樣的方式觀測上述代碼:

  1. 全局環境創建了變量 num1 并為其賦值 10num1 存儲于內存之中;
  2. 全局環境創建了函數 foo ,并為其構建函數體,緩存于內存之中。未調用時不執行函數體內容;
  3. 函數 foo() 被調用;
  4. 發現函數 foo() 中需要創建變量 num2 ,則創建并賦值 29。變量 num2 存儲于內存中;
  5. 通過 console.log() 輸出變量 num1 的值于控制臺,輸出 10
  6. 通過 console.log() 輸出變量 num2 的值于控制臺,輸出 29
  7. 函數 foo() 調用完畢,銷毀函數中聲明的變量 num2
  8. 返回外部作用域。
  9. 通過 console.log() 輸出變量 num1 的值于控制臺,輸出 10
  10. 通過 console.log() 輸出變量 num2 的值,JS引擎發現變量 num2 在當前作用域中并不存在(實際上在局部作用域運行時曾經存在過,但已經被銷毀)。因此引發異常警告:ReferenceError: num2 is not defined

緩存作用域產生的結果

? ? ? ?以上是一段非常普通的 JS 運行示例,描述了程序運行過程中的作用域關系。在一段程序中,函數主要的作用就是為了解耦執行過程,提高可重用性。而在函數執行的過程中,我們通常都需要一個執行結果對外暴露,這就相當于擴大了函數內部運行數據的作用域,賦予了調用者可以跨子作用域獲取數據的能力。這就是我們常見的函數返回值

// 聲明一個全局作用域中可調用的函數
function foo(){
    // 函數內部作為 局部作用域 ,此處聲明的變量只有該函數內部持有
    const num2 = 29;

    return num2;
}
// 接收函數 foo() 的執行結果
const num1 = foo();

// 在控制臺輸出 num1 所持有的值
console.log('全局  num1:', num1); // 結果:29

? ? ? ?當然,我們也可以使其返回一個對象類型的數據:

// 聲明一個全局作用域中可調用的函數
function createObject(name, tel, age=18){
    // 如果 age 小于等于 0 ,則重新為其賦值為 18
    age = (age > 0) ? age : 18;
    
    // 返回一個描述 作者信息 的對象
    return {
        name, tel, age
    };
}
// 接收函數 createObject() 的執行結果
const user = createObject('阿拉拉布', '18392019102', 16);

// 在控制臺輸出 作者信息
console.log('該文作者:', user); // 該文作者:{name: '阿拉拉布', tel: '18392019102', age: 16}

? ? ? ?通過 高階函數 特性,我們還了解到,由于函數本身也是一個對象,因此可以在一個函數中返回另一個函數

// 聲明一個用于創建 二元計算器 的函數
// 函數接收一個 oper 作為運算操作符( 字符串,可用值:+,-,*,/,% )
function createCalculator(oper){
    
    // 新的函數接收用于計算的兩個數字
    return function(a, b){
        switch(oper){
            case '+': return a+b;
            case '-': return a-b;
            case '*': return a*b;
            case '/': return a/b;
            case '%': return a%b;
            default: throw 'Operator Formart Error: ' + oper;
        }
    }
}
// 生成加法函數
const add = createCalculator('+');
const sub = createCalculator('-');

// 運行
add(10, 8);     // 結果: 18
sub(10, 8);     // 結果: 2

? ? ? ?那么,如果我們希望返回的結果包含有多個函數時,怎么辦呢?可以這么做:

// 聲明一個用于創建 多元計算器 的函數
function createCalculator(){
    
    // 創建加法算法函數
    const addArithmetic = (accumulator, currentValue) => accumulator + currentValue;
    // 創建加法運算函數,可接受 n 個數字用于累加
    const add = function() {
        return Array.from(arguments).reduce(addArithmetic);
    }

    // 創建減法算法函數
    const subArithmetic = (accumulator, currentValue) => accumulator - currentValue;
    // 創建減法運算函數,可接受 n 個數字用于累減
    const sub = function() {
        return Array.from(arguments).reduce(subArithmetic);
    }
    
    // 將返回的 函數 作為對象成員
    return {
        add, sub
    };
}

// 構建計算器對象
const calculator = createCalculator();

// 調用
calculator.add(15, 33, 39, 69);     // 結果: 156

calculator.sub(69, 28, 15);         // 結果: 26

5.2 面向對象關系

image

? ? ? ?有過 面向對象 編程語言經驗的同學,對于這種通過函數的方式締造一個包含有多個屬性或者函數的對象的方式都很熟悉,很像是通過一個類去創建對象。當然,以 面向對象 的方式,我們也可以解決上述問題,創建一個包含多個屬性或者函數的對象:

// JS 中使用函數對象作為構造函數,來模擬類結構
// 用于締造對象的函數,通常首字母大寫
function Calculator(){
    // 函數作為構造函數,這里可以做一些初始化工作...
}

// 通過為函數的原型添加方法,以便于讓子對象應用這些方法
Calculator.prototype.add = function(){
    // 創建加法算法函數
    const addArithmetic = (accumulator, currentValue) => accumulator + currentValue;
    return Array.from(arguments).reduce(addArithmetic);
}

Calculator.prototype.sub = function(){
    // 創建減法算法函數
    const subArithmetic = (accumulator, currentValue) => accumulator - currentValue;
    return Array.from(arguments).reduce(subArithmetic);
}

// 通過 new 關鍵字構建計算器對象
const calculator = new Calculator();

// 調用
calculator.add(15, 33, 39, 69);     // 結果: 156

calculator.sub(69, 28, 15);         // 結果: 26

與上一個例子,函數中返回對象字面量不同的是:函數返回的對象字面量構建的多個對象之間,調用的 add()sub() 函數,在內存中都有各自獨立的存儲空間,互不干擾。但也造成了更多的資源損耗:

// 這里是示例 1:函數的方式返回計算器對象
// ...
const calc1 = createCalculator();
const calc2 = createCalculator();

calc1.add == calc2.add; // 結果:flase

而通過綁定原型 prototype 的方式構建的對象,在調用 add()sub() 方法時,委托了原型鏈上層的對象模型,因此引用的都是同一個函數的副本。節省了內存資源:

// 這里是示例 2:構造函數調用的方式返回計算器對象
// ...
const calc1 = new Calculator();
const calc2 = new Calculator();

calc1.add == calc2.add; // 結果:true

小結:無論通過函數的方式,還是構造對象的方式,我們都在解決一個緩存數據的問題。我們希望緩存的數據,能夠被調用方(外界)更好的引用。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容