JS 函数式编程思维简述(六):闭包 03
- 简述
- 无副作用(No Side Effects)
- 高阶函数(High-Order Function)
- 柯里化(Currying)
- 闭包(Closure)
-- JavaScript 作用域
-- 面向对象关系
-- this调用规则
-- 配置多样化的构造重载
-- 更多对象关系维护——模块化
-- 流行的模块化方案- 不可变(Immutable)
- 惰性计算(Lazy Evaluation)
- 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
所谓模块化,即是指在解决复杂问题时,由上自下的将问题进行拆解,例如一些插件设计的目的即是为了提高可重用性。我们可以将若干功能拆解成为功能独立的小模块,互相嵌套引用。闭包则是拆解过程中常用的技巧。在更为复杂的模块设计中,一个闭包中涉及到的对象、函数调用关系会更为复杂的多,示例中仅仅希望以最简单的形式表达一些调用关系。