《js 设计模式与开发实践》读书笔记 3
高阶函数是指至少满足下列条件之一的函数:函数可以作为参数被传递,函数可以作为返回值输出。首先是把函数作为参数传递,这代表我们可以抽离一部分容易变化的业务逻辑,把这部分业务逻辑放在函数参数中,这样以来可以分离业务中变化与不变的部分。其中一个重要应用场景就是常见的回调函数。
在 ajax 异步请求的应用中,回调函数使用的非常频繁。ajax('url',function(data){callback()});回调函数的应用不仅只在异步请求中,当一个函数不适合执行一些请求时,我们也可以把这些请求封装成一个函数,并把它作为参数传递给另外一个函数,“委托”给另外一个函数来执行。
比如,我们想再页面中创建 100 个 div 节点,然后把这些 div 节点都设置为隐藏。
var appendDiv = function () {
for (var i = 0; i < 100; i++) {
var div = document.createElement('div')
div.innerHTML = i
document.body.appendChild(div)
div.style.display = 'none'
}
}
appendDiv()
把 div.style.display='none'的逻辑硬编码在 appendDiv 里显然是不合理的,因为不是每个创建了节点就让它隐藏的。我们把这行代码抽出来,用回调函数的形式传入 appendDiv 方法。
var appendDiv = function (callback) {
for (var i = 0; i < 100; i++) {
var div = document.createElement('div')
div.innerHTML = i
document.body.appendChild(div)
if (typeof callback === 'function') {
callback(div)
}
}
}
appendDiv(function (node) {
node.style.display = 'none'
})
隐藏节点的请求实际上是由客户发起的,但是客户并不知道节点什么时候会创建好,于是把隐藏节点的逻辑放在回调函数中,委托给 appendDIv 方法。appendDiv 方法知道节点什么时候创建好,所以在节点创建好的时候,appendDiv 会执行之前客户传入的回调函数。Array.prototype.sort 接受一个函数当作参数,这个函数里面封装了数组元素的排序规则。用什么规则去排序。是可变的部分。arr.sort((a,b)=>(a-b));
函数作为返回值输出:相比把函数当作参数传递,函数当作返回值输出的场景更多,也更能体现函数式编程的巧妙。让函数返回一个可执行的函数,意味着运算过程是可延续的。
判断一个数据是否是数组,以往的实现,可以基于鸭子类型的概念判断,比如判断这个数据有没有 length 属性,有没有 sort 方法或者 slice 方法等。但更好的方式是用 Object.prototype.toString 来计算。
var isString = function (obj) {
return Object.prototype.toString.call(obj) === '[object String]'
}
var isArray = function (obj) {
return Object.prototype.toString.call(obj) === '[object Array]'
}
var isNumber = function (obj) {
return Object.prototype.toString.call(obj) === '[object Number]'
}
这些函数的大部分实现都是相同的,不同的只是 Object.protoType.toString.call(obj)返回的字符串。我们可以尝试把这些字符串作为参数提前植入 isType 函数。
var isType = function (type) {
return function (obj) {
return Object.prototype.toString.call(obj) === '[object ' + type + ']'
}
}
var isString = isType('String')
var isArray = isType('Array')
console.log(isArray([1, 2, 3])) //true
//循环注册
var Type = {}
for (var i = 0, type; (type = ['String', 'Array', 'Number'][i++]); ) {
;(function (type) {
Type['is' + type] = function (obj) {
return Object.prototype.toString.call(obj) === '[object ' + type + ']'
}
})(type)
}
Type.isArray([]) //true
var getSingle = function (fn) {
var ret
return function () {
return ret || (ret = fn.apply(this, arguments))
}
}
var getScript = getSingle(function () {
return document.createElement('script')
})
var script1 = getScript()
var script2 = getScript()
console.log(script1 == script2) //true
这个是一个单例模式。这个高阶函数的例子,季把函数当作参数传递,又让函数执行后返回了另外一个函数。
AOP 面向切面编程的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计,安全控制,异常处理等。把这些功能抽离出来之后,再通过动态织入的方式参入业务逻辑模块中,这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。面向对象语言中,一般通过反射和动态代理机制来实现 AOP 技术。js 中有很多方式实现 AOP。通过 Function.prototype 可以做到这一点。
Function.prototype.before = function (beforefn) {
var _self = this
return function () {
beforefn.apply(this, arguments)
return _self.apply(this, arguments)
}
}
Function.prototype.after = function (afterfn) {
var _self = this
return function () {
var ret = _self.apply(this, arguments)
afterfn.apply(this, arguments)
return ret
}
}
var func = function () {
console.log(2)
}
func = func
.before(function () {
console.log(1)
})
.after(function () {
console.log(3)
})
func()
我们把负责打印数字 1 和打印数字 3 的两个函数通过 AOP 的方式动态植入 func 函数。这种是 js 语言中一种非常特别和巧妙的装饰者模式实现。在介绍一些常见的高阶函数应用。currying,函数柯里化。又被称为部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。举个例子:假设我们要编写一个计算每月开销的函数。在每天结束之前,我们都要记录今天花掉了多少钱。
var monthlyCost = 0
var cost = function (money) {
monthlyCost += money
}
cost(100) //1tian
cost(200) //2
cost(300) //3
console.log(monthlyCost) //600
这段代码中我们可以看到,每天结束后我们记录花了多少钱,但我们只关心月底的时候总共花了多少钱。如果在每个月的前 29 天,我们都是保存好当天的开销,到 30 天才进行求值计算,这样就达到了我们的要求。下面的 cost 函数不是一个 currying 函数的完整实现,但有助于我们了解其思想:
var cost = (function () {
var args = []
return function () {
if (arguments.length === 0) {
var money = 0
for (var i = 0, l = args.length; i < l; i++) {
money += args[i]
}
return money
} else {
;[].push.apply(args, arguments)
}
}
})()
cost(100)
cost(200)
cost(300)
console.log(cost())
在 js 中,当我们调用对象的某个方法时,其实不用去关心该对象原本是否被设计为拥有这个方法,这是动态类型语言的特点,也是常说的鸭子类型思想。同理,一个对象也未必只能使用它自身的方法,call 和 apply 都可以完成这个需求。
uncurrying 可以把泛化 this 的过程提出去来。通过uncurrying的方式,Array.prototype.push.call变成了一个通用的push函数。这样一来,push函数的作用就跟Array.prototype.push一样了。同样不仅仅局限于只能操作array对象。
Function.prototype.uncurrying = function () {
var self = this
return function () {
var obj = Array.prototype.shift.call(arguments)
return self.apply(obj, arguments)
}
}
var push = Array.prototype.push.uncurrying()
;(function () {
push(arguments, 4)
})(1, 2, 3)