JS相关

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

2018-12-23  本文已影响41人  阿拉拉布
  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 的插件设计中,我们经常使用这种设计方式,常见的如:

<!-- 网页部分 -->
<input type="text" name="birthday" value="2018-9-2" />
// javascript 插件设计及依赖部分
<script src="js/jquery-3.3.1.min.js" type="text/javascript"></script>
<script src="js/date-util.js" type="text/javascript"></script>
<script type="text/javascript">
    // 插件用于为 jQuery 对象添加格式化日期值的方法
    (function($){
        $.fn.extend({
            formartDate: function(){
                const v = this.val();
                if( v ){
                    const dutil = new DateUtil({date: v});
                    this.val( dutil.formart() );
                }
            }
        });
    })(jQuery);
</script>
// 插件应用部分,输入框失去焦点时尝试格式化值
$(':text[name="birthday"]').on('blur', function(e){
    $(this).formartDate();
});

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

缓存执行结果

       闭包最大的作用,便是缓存函数中的执行结果,以便调用方多次引用,提高执行效率。我们将上述插件做一个改造,构建一个<div> 标签渲染成为一个表格的插件,代码如下:

<!-- index.html 部分 -->
<div id="box-1"></div>
/** 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 => `<div class="row">${r.map( c => '<div class="cols" style="width:'+colsWidthPercent+';">'+c+'</div>' ).join('')}</div>`);
            // 为缓存的 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部分
<script src="js/jquery-3.3.1.min.js" type="text/javascript"></script>
<script src="js/build-table.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">

    // 构建一个二维表数据用以渲染表格      
    const arr1 = [
        ['a', 'b', 'c', 'd'],
        ['e', 'f', 'g', 'h'],
        ['i', 'j', 'k'],
        ['l', 'm', 'n', 'o', 'p'],
    ];
    
    // 渲染后, 变量 box1 获取了插件闭包中返回的缓存数据
    const box1 = $('#box-1').buildTable(arr1);
</script>

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


image

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

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

渲染结果:


image

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

上一篇下一篇

猜你喜欢

热点阅读