本篇文章参考书籍《JavaScript设计模式》–张容铭
前言
熟悉 react 的同学,给绑定事件传递额外参数,这种操作是不是信手拈来,这是得益于 jsx 语法,所有代码都是 js 当然可以为所欲为了,但是当不使用框架的时候,我们又应该怎么实现这种功能呢?
本节就来学一个参与者模式,帮我们解决传参问题。
参与者模式
在特定的作用域中执行给定的函数,并将参数原封不动地传递。
之前我们封装过可以兼容各个浏览器的事件框架,如下:
//事件绑定方法
A.event.on = function(dom, type, fn) {
//w3c标准事件绑定
if(dom.addEventListener) {
dom.addEventListener(type, fn, false);
//ie事件绑定
} else if(dom.attachEvent) {
dom.attachEvent('on' + type, fn)
//dom 0级事件绑定
} else {
dom['on' + type] = fn;
}
}
我们知道 addEventListener 方法不能传递自定义参数,那么我们想要传递额外的数据的话,就得在回调函数中做文章。
A.event.on = function(dom, type, fn, data) {
//w3c标准事件绑定
if(dom.addEventListener) {
dom.addEventListener(type, function(e) {
//在dom环境中调用fn, 并传入事件对象与 data 数据参数
fn.call(dom, e, data);
}, false);
}
//ie绑定事件...
}
JavaScript 中的 call 和 apply 方法很神奇,它可以使我们在特定的作用域中执行某个函数并传入参数,所以,在回调函数中,我们借助 call 函数实现了需求。
不过上面代码有一个问题,添加的事件回调函数不能移除。因为此时事件回调函数是匿名函数,这就需要借用参与者模式来帮我们解决这一问题了。让更多的对象执行参与执行时的函数。
大家知道函数的绑定方法不? 如果熟悉框架的话,那一定接触过 bind ,这东西的实现思想很简单,就是让一个函数在一个作用域中执行,根据这一条原理我们实现它就简单多了,只需要一个闭包就行。
//函数绑定 bind
function bind(fn, context) {
//闭包返回新函数
return function() {
//对 fn 装饰并返回
return fn.apply(context, arguments);
}
}
我们可以测试一下,新建一个对象 demoObj 和一个函数 demoFn ,然后让 demoObj 对象参与 demoFn 的执行,并保存在 bindFn 变量中,我们来观察一下 demoFn 和 bindFn 执行的结果。
//测试对象
var demoObj = {
title: '这是一个例子'
}
//测试方法
function demoFn() {
console.log(this.title);
}
//让 demoObj 参与 demoFn 的执行
var bindFn = bind(demoFn, demoObj);
demoFn(); //undefined
bindFn(); //这是一个例子
bindFn 函数返回了结果,这说明 demoObj 参与进来并提供了作用域,不过注意 bindFn 是让 demoObj 寄生其中,并在执行时才让 demoObj 加入的,所以说 bindFn 和 demoFn 是两个不同的函数。应用于事件如下:
var btn = document.getElementsByTagName('button')[0];
var p = document.getElementsByTagName('p')[0];
//对 demoFn 改进,在控制台输出参数与 this 对象
function demoFn() {
console.log(arguments, this);
}
//未设置提供参与对象
var bindFn = bind(demoFn);
//绑定事件
btn.addEventListener('click', bindFn);
//chrome 输出:[MouseEvent] Window
//提供 btn 元素参与对象
var bidnFn = bind(demoFn, btn);
//chrome 输出:[MouseEvent] <button>按钮</button>
//提供 p 元素参与对象
var bidnFn = bind(demoFn, btn);
//chrome 输出:[MouseEvent] <p>hello</p>
//移除事件
btn.removeEventListener('click', bindFn);
当未提供参与对象时,执行的结果是返回 this 对象指向全局对象 Window , 说明此时是在全局作用域中执行的,当提供 btn 元素参与对象时,返回的 this 对象时元素自身,说明此时是在 btn 元素作用域中执行的,当提供 p 元素参与对象时,返回的是 p 元素,说明此时是在 p 元素作用域中执行的。并且我们发现函数的参数中还为我们传递了事件对象。并且这种方式添加的事件还可以通过 removeEventListener 来移除。
有个好消息是,在一些标准浏览器如高版本的 FireFox, chrome, Safari 等中还是为我们提供了原生 bind 方法,所以你还可以像下面这种方式使用原生 bind 方法。
//提供 p 元素参与对象
var bindFn = demoFn.bind(p);
这只是实现需求的第一步,将添加的事件成功移除。下面我们要完成第二部,为运行的函数添加额外的自定义数据参数。这就需要借助函数柯里化了。
函数柯里化
bind 函数的本质,其实就是柯里化,函数柯里化思想是对函数的参数分割,根据参数的不同,让一个函数存在多种状态,只不过函数柯里化处理的是函数,因此要以函数为基础,借助柯里化器伪造其他函数,这些伪造的函数在执行时调用这个计函数完成不同的功能。
怎对柯里化举个例子大家就明白了。
function add(x, y) {
return x + y
}
// 柯里化后
function curryingAdd(x) {
return function (y) {
return x + y
}
}
add(1, 2) // 3
curryingAdd(1)(2) // 3
大家有没有觉得上面代码很熟悉,没错,上一节我们学习惰性模式的时候,就i是通过这种返回函数来实现的。下面创建一个函数柯里化器
//函数柯里化
function curry(fn) {
//缓存数组 slice 方法 Array.prototype.slice
var Slice = [].slice;
//从第二个参数开始截取参数
var args = Slice.call(arguments, 1);
//闭包返回新函数
return function() {
//将参数(类数组)转化为数组
var addArgs = Slice.call(arguments),
//拼接参数
allArgs = args.concat(addArgs);
//返回新函数
return fn.apply(null, allArgs);
}
}
接下来测试下这个柯里化器,老规矩,拿加法器进行拓展。
//加法器
function add(num1, num2) {
return num1 + num2;
}
//加 5 加法器
function add5(num) {
return add(5, num);
}
//测试 add 加法器
console.log(add(1, 2)); //3
//测试加 5 加法器
console.log(add5(6)); //11
//函数柯里化创建加5加法器
var add_5 = curry(add, 5);
console.log(add_5(7)) //12
//7 + 8
var add7add8 = curry(add, 7, 8);
console.log(add7add8()) //15
通过柯里化器对 add 方法实现的多态拓展且不需要像以前那样明确声明函数了,因为函数的声明过程已经在柯里化器中完成了。
接下来回归到一开始的需求,我们需求的第二部是传递额外的自定义数据参数,所以我们需要用函数柯里化思想来拓展函数执行的参数就可以了。我们重写一下 bind 函数。
//重写 bind
function bind (fn, context) {
//缓存数组 slice 方法
var Slice = Array.prototype.slice,
//从第三个参数开始截取参数(包括第三个参数)
args = Slice.call(arguments, 2);
//返回新方法
return function() {
//将参数转化为数组
var addArgs = Slice.call(arguments),
//拼接参数
allArgs = addArgs.concat(args);
//对 fn 装饰并返回
return fn.apply(context, allArgs);
}
}
现在我们创建两个数据对象, demoData1 和 demoData2 ,然后传入事件的回调函数中,我们在控制台看看输出结果。
var demoData1 = {
text: '这是第一组数据'
},
demoData2 = {
text: '这是第二组数据'
};
//提供 btn 元素、demoData1 参与对象
var bindFn = bind(demoFn, btn, demoData1);
//chrome 输出:[MouseEvent, Object] <button>按钮</button>
//提供 btn 元素、demoData1 demoData2 参与对象
var bindFn = bind(demoFn, btn, demoData1, demoData2);
//chrome 输出:[MouseEvent, Object, Object] <button>按钮</button>
//提供 p 元素、demoData1 参与对象
var bindFn = bind(demoFn, p, demoData1);
//chrome 输出:[MouseEvent, Object] <p>hello</p>
在回调函数中果然可以访问到传入的自定义数据对象,浏览器内置的 bind 方法也可以这样用。
var bindFn = demoFn.bind(p, demoData1);
//chrome 输出: [Object, MouseEvent] <p>hello</p>
跟我们自己封装的函数不同的是,内置的 bind 把事件对象放在了后面。
接下来我们写个兼容版本,对未提供 bind 方法的浏览器的原生 Function 对象添加 bind 方法,这样在各个浏览器中就可以兼容了。
//兼容各个浏览器
if(Function.prototype.bind === undefined) {
Function.prototype.bind === function(context) {
//缓存数组 slice 方法
var Slice = Array.prototype.slice,
//从第二个参数截取参数
args = Slice.call(arguments, 1);
//保存当前函数引用
that = this;
//返回新函数
return fucntion() {
//将参数数组化
var addArgs = Slice.call(arguments),
//拼接参数,注意:传入的参数放在了后面
allArgs = args.concat(addArgs);
//对当前函数装饰并返回
return that.apply(context, allArgs);
}
}
}
参与者模式其实是两种技术的结晶,函数绑定和函数柯里化,早期浏览器未能提供 bind 方法,因此工程师们为了使添加的事件能够移除,事件回调函数中能够访问到事件源,并可以向事件回调函数中传入自定义数据,才发明了函数绑定与函数柯里化技术。
对于函数绑定,他将函数以函数指针的形式传递,是函数在被绑定的对象作用域中执行,因此函数的执行中可以顺利的访问到对象内部数据,由于函数绑定构造复杂,执行时需要消耗更多的内存,因此执行速度上稍慢一些,不过相对于解决的问题来说,这种程度的消耗还是可以接受的。
对于函数柯里化即是将接受多个参数的函数转化为接收一部分参数的新函数,余下的参数保存下来,当函数调用时,但会传入的参数与保存的参数共同执行的结果。通常保存下来的参数保存于闭包中,所以函数柯里化需要消耗一定的资源。
柯里化有点类似类的重载,不同点是类的重载是同一个对象,函数柯里化是两个不同的函数。随着柯里化的发展,现在又衍生出一种反柯里化函数,目的是方便我们对方法的调用。
//反柯里化
Function.prototype.uncurry = function() {
//保存当前对象
var that = this;
return function() {
return Function.prototype.call.apply(that, arguments);
}
}
当用 Object.prototype.toString 校验对象类型时:
//获取校验方法
var toString = Object.prototype.toString.uncurry();
//测试对象数据类型
console.log(toString(function() {
})) //chrome: [object Function]
console.log(toString([])); //chrome: [object Array]
用数组的 push 方法给对象添加成员
//保存数组 push 方法
var push = [].push.uncurry();
//创建一个对象
var demoArr = {
};
//通过 push 方法为对象添加数据成员
push(demoArr, '第一个成员', '第二个成员');
console.log(demoArr); //chrome: Object { 0: '第一个成员', 1: '第二个成员', length: 2}