- 簡述
- 無副作用(No Side Effects)
- 高階函數(High-Order Function)
- 柯里化(Currying)
- 閉包(Closure)
-- JavaScript 作用域
-- 面向對象關系
-- this調用規則
-- 配置多樣化的構造重載
-- 更多對象關系維護——模塊化
-- 流行的模塊化方案- 不可變(Immutable)
- 惰性計算(Lazy Evaluation)
- Monad
閉包(Closure)
? ? ? ?閉包(Closure)是函數式編程的重要特性,是緩存函數內部作用域,并且對外暴露的過程;是模塊化編程的基石。在探討這部分的過程中,我們將主要探討這幾個問題:
-
JavaScript
作用域; - 面向對象關系;
- this調用規則;
- 配置多樣化的構造重載;
- 更多對象關系維護——模塊化;
- 流行的模塊化方案;
讓我們帶著這些問題,一步一步進入主題,了解閉包產生的過程和必要性。
5.1 JavaScript 作用域
? ? ? ?我們經常用房間內外,來描述作用域的概念。作用域在編程語言中是一個廣泛的概念,主要是指變量可應用的范圍。應用范圍的區間廣義上分為全局環境和局部環境,在運行機制上則是指變量被銷毀的時機。直接上一個簡單的例子:
// 全局作用域中
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
引入的新的更加嚴格的聲明變量的關鍵字const
和let
來解決var
描述作用域不明確的歷史問題。
? ? ? ?在 JavaScript
內部,運行著一套與 Java
相似的自動垃圾回收機制,用于將之后不再使用的數據,在內存中清除掉。因此我們可以以這樣的方式觀測上述代碼:
- 全局環境創建了變量
num1
并為其賦值10
,num1
存儲于內存之中; - 全局環境創建了函數
foo
,并為其構建函數體,緩存于內存之中。未調用時不執行函數體內容; - 函數
foo()
被調用; - 發現函數
foo()
中需要創建變量num2
,則創建并賦值29
。變量num2
存儲于內存中; - 通過
console.log()
輸出變量num1
的值于控制臺,輸出10
; - 通過
console.log()
輸出變量num2
的值于控制臺,輸出29
; - 函數
foo()
調用完畢,銷毀函數中聲明的變量num2
。 - 返回外部作用域。
- 通過
console.log()
輸出變量num1
的值于控制臺,輸出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 面向對象關系
? ? ? ?有過 面向對象
編程語言經驗的同學,對于這種通過函數的方式締造一個包含有多個屬性或者函數的對象的方式都很熟悉,很像是通過一個類去創建對象。當然,以 面向對象
的方式,我們也可以解決上述問題,創建一個包含多個屬性或者函數的對象:
// 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
小結:無論通過函數的方式,還是構造對象的方式,我們都在解決一個緩存數據的問題。我們希望緩存的數據,能夠被調用方(外界)更好的引用。