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 作用域

JS 函数式编程思维简述(四):闭包 01_第1张图片
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 面向对象关系

JS 函数式编程思维简述(四):闭包 01_第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

小结:无论通过函数的方式,还是构造对象的方式,我们都在解决一个缓存数据的问题。我们希望缓存的数据,能够被调用方(外界)更好的引用。

你可能感兴趣的:(JS 函数式编程思维简述(四):闭包 01)