JS 函数式编程思维简述(六):闭包 03

  1. 简述
  2. 无副作用(No Side Effects)
  3. 高阶函数(High-Order Function)
  4. 柯里化(Currying)
  5. 闭包(Closure)
    -- JavaScript 作用域
    -- 面向对象关系
    -- this调用规则
    -- 配置多样化的构造重载
    -- 更多对象关系维护——模块化
    -- 流行的模块化方案
  6. 不可变(Immutable)
  7. 惰性计算(Lazy Evaluation)
  8. Monad

前言

       函数提供了一个封闭的、通用性很强的执行空间,根据参数不同,来缔造不同的执行结果。
       通过 JavaScript 作用域 ,我们了解了函数作用域的限制,我们可以将什么样的执行结果交给函数的调用者。
       通过 面向对象关系 我们了解到了另一种封装数据的方式——面向对象设计,可以帮助我们更准确的定义同一种类型的数据模型。
       通过对 this调用规则 的了解,我们可以规避很多在 JavaScript 中设计函数或对象的坑,更明确当前的封闭环境执行过程。
       而接下来,我们继续回到闭包构想:如何进一步延展我们的黑箱设计

5.4 配置多样化的构造重载

       对象的设计往往主要需考虑实例之间的相似性,然而在应用对象的过程中,我们又常常要明确每个对象之间的个体独立性。因此面向对象的设计过程主要囊括以下两个步骤:

  • 抽象提取实例之间的共有属性、共有行为(方法);
  • 为确保实例独立性,提供共有属性不同的配置方式(赋值);

       通常我们会在构造函数中进行一系列初始化行为,用以配置这个对象的使用方式,入口依然是函数参数的传递:

class DateUtil{
                 
    constructor(date) {
        // 缓存日期对象
        this.date = typeof date === 'string'? new Date(date): date;
        if(this.date == 'Invalid Date'){
            throw '日期参数只能是有效格式的字符串或Date对象!';
        }
    }
                 
    // 按照 yyyy-MM-dd HH:mm:ss 格式输出字符串
    formart(){
        const d = this.date;
        const leftDate = [d.getFullYear(), d.getMonth()+1, d.getDate()];
        const rightDate = [d.getHours(), d.getMinutes(), d.getSeconds()];
        // 检查是否需要补零的函数
        const checkFormart = e => (''+e)[1]?(''+e):('0'+e);
        return leftDate.map( checkFormart ).join('-')
                .concat(' ')
                .concat(rightDate.map(checkFormart).join(':'));
    }
}

这是一个简单的日期工具类,在 constructor 构造中我们接收一个 date 参数作为该对象的默认配置,记录了构建对象时需要缓存的时间。又构建了一个 formart() 方法用于对于缓存的时间进行格式化字符串输出。当然,之后也许还有很多其他针对缓存数据 date 的操作,暂且不提。输出结果如下:

const d1 = new DateUtil(new Date());
d1.formart(); // 结果:"2018-12-23 12:08:08"

const d2 = new DateUtil('1998-3-12 9:3:21');
d2.formart(); // 结果:"1998-03-12 09:03:21"

       重载是指对于同名方法的不同参数(类型/数量)的调用,用于以相似的行为描述不同结果。一些强类型语言中提供了重载的严格语法,而在 JavaScript 中,函数参数要求非常宽松——你可以传递任意数量的实参,以及声明任意数量的形参,即使他们在调用过程中发生了参数过剩或者参数缺失,也不会出现运行时异常。
       假设现在又有一个需求:缓存一个是否只操作日期(忽略时间部分)的状态。在 Java 中我们可能会这样设计这个类的构造重载:

class DateUtil{
    DateUtil(Date date){
        // 只接收日期类型对象的构造
    }

    DateUtil(String dateStr){
        // 只接收字符串类型参数的构造
    }

    DateUtil(Date date, boolean ignoreTime){
        // 接收日期类型对象,及是否忽略时间部分的布尔值
    }

    DateUtil(String dateStr, boolean ignoreTime){
        // 接收字符串类型参数,及是否忽略时间部分的布尔值
    }
}

而在 JavaScript 中,我们的构造函数只有一个,因此我们经常这样设计不同参数的可配置对象:接收统一的参数 options 对象,将需要传递的参数作为 options 对象的属性。在构造中断言属性的有效性:

class DateUtil{
                 
    constructor(options) {
        // 获取参数
        const {date , ignoreTime} = options;
        // 缓存日期对象
        this.date = typeof date === 'string'? new Date(date): date;
        if(this.date == 'Invalid Date'){
            throw '日期参数只能是有效格式的字符串或Date对象!';
        }
        // 缓存 是否忽略时间 的状态
        this.ignoreTime = (ignoreTime === true);
    }
                 
    // 按照 yyyy-MM-dd HH:mm:ss 格式输出字符串
    formart(){
        const d = this.date;
        // 日期部分数据集合
        const leftDate = [d.getFullYear(), d.getMonth()+1, d.getDate()];
        
        // 检查是否需要补零的函数
        const checkFormart = e => (''+e)[1]?(''+e):('0'+e);
        // 缓存日期部分的格式化字符串
        let result = leftDate.map( checkFormart ).join('-');
        // 根据是否忽略时间部分做为判断
        if(!this.ignoreTime){
            // 时间部分数据集合
            const rightDate = [d.getHours(), d.getMinutes(), d.getSeconds()];
            result = result.concat(' ')
                .concat(rightDate.map(checkFormart).join(':'));
        }
        return result;  
    }
}

而调用过程就变成了:

const d1 = new DateUtil( {date: new Date()});
d1.formart(); // 结果: "2018-12-23 12:47:17"

const d2 = new DateUtil( {date: new Date(), ignoreTime: true});
d1.formart(); // 结果: "2018-12-23"

这样的设计过程也符合柯里化标准,接收单一参数,使得调用过程变得更加单纯。

5.5 更多对象关系维护——模块化

       通过将粒度比较小的函数进行封装,我们得到的是粒度更大一些的对象模型。而很多时候,我们需要完整的实现一个较为复杂的功能,就没法单靠一个对象模型承担所有任务了。我们可能需要构建更多的对象模型或者更多的独立函数,完成整体业务流程的调动关系。
       这种复杂的调动关系会形成一个完整的应用,很多插件的设计基准就来源于此。

立即执行函数 (IIFE)

       在 JavaScript 中,通过 匿名函数函数表达式 的组合,我们可以构建一个 立即执行函数(IIFE)

const foo = (function(){
    return 29;
})();

console.log( foo ); // 结果: 29

变量 foo 得到了匿名函数执行的返回值,因此赋值符右侧部分的函数并不仅仅是在声明,而是在声明之后立即执行立即执行函数 (IIFE) 主要的两个优势是:

  • 隔离外部作用域的全局变量,保持函数内部的纯净;
  • 立即执行产生结果,减少不必要的重复声明;

jQuery 的插件设计中,我们经常使用这种设计方式,常见的如:



// javascript 插件设计及依赖部分



// 插件应用部分,输入框失去焦点时尝试格式化值
$(':text[name="birthday"]').on('blur', function(e){
    $(this).formartDate();
});

在这个简单插件的设计过程中,我们通过 IIFE 函数确保了插件作用域中的 $ 变量一定是指 jQuery 对象,而不必担心全局环境或者其他的插件引用会覆盖 $ 的值。并且调用了前边例子中的日期工具,用以对输入框的值进行格式化。

缓存执行结果

       闭包最大的作用,便是缓存函数中的执行结果,以便调用方多次引用,提高执行效率。我们将上述插件做一个改造,构建一个

标签渲染成为一个表格的插件,代码如下:


/** b-table.css 部分 **/
div{
    box-sizing: border-box;
}

.b-table{
    width: 500px;
    border: 1px solid #eee;
}

.b-table .row{
    height: 30px;
    line-height: 30px;
}

.b-table .row:nth-child(even){
    background-color: antiquewhite;
}

.b-table .row .cols{
    display: inline-block;
    float: left;
    text-align: center;
}
// 独立的 jQuery 插件 build-table.js
(function($) {
    
    class Table{
        constructor({el, data}) {
            this.el = el;
            this.data = data;
            // 为当前调用者添加插件定义的类样式
            this.el.addClass('b-table');
        }
        
        rander(newData){
            // rander() 也可以接收新的数据重新渲染
            const data = newData || this.data;
            // 计算最大列数
            const colsWidth = data.reduce( (prev, curr) => Math.max(Array.isArray(prev)? prev.length: prev, curr.length) );
            // 计算列宽百分比, 动态调整样式
            const colsWidthPercent = (100 / colsWidth) + '%';
            // 生成用于渲染的 html 文本
            const tableHtml = data.map( r => `
${r.map( c => '
'+c+'
' ).join('')}
`); // 为缓存的 el 元素渲染内容 this.el.html(tableHtml); } } // 为 jQuery 对象添加插件方法 $.fn.extend({ buildTable: function(data) { // 创建 Table 实例,传入配置项 const t = new Table({el: this, data}); // 调用渲染方法 t.rander(); // 返回当前的 Table 实例以便后续调用 return t; } }); })(jQuery);

最后是执行部分:

// index.html 中的js部分



渲染效果(将一个div变成了一个表格的样式):


JS 函数式编程思维简述(六):闭包 03_第1张图片
image

而当我们需要动态更新表格时,便可以使用变量 box1 进行重新渲染,而不需要再次指定该元素是哪个 div 元素:

// 为原始的 arr1 添加新数据
arr1.push(['q', 'r', 's', 't', 'u', 'v', 'w']);
// 重新将 arr1 数据渲染至 box1 指向的页面元素
box1.rander(arr1);

渲染结果:


JS 函数式编程思维简述(六):闭包 03_第2张图片
image

所谓模块化,即是指在解决复杂问题时,由上自下的将问题进行拆解,例如一些插件设计的目的即是为了提高可重用性。我们可以将若干功能拆解成为功能独立的小模块,互相嵌套引用。闭包则是拆解过程中常用的技巧。在更为复杂的模块设计中,一个闭包中涉及到的对象、函数调用关系会更为复杂的多,示例中仅仅希望以最简单的形式表达一些调用关系。

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