高阶函数
高阶函数是指至少满足下列条件之一的函数。
- 函数可以作为参数被传递
- 函数可以作为返回值输出
应用:
- Array.sort
- 解决var i的作用域问题
- 单例模式
- 各种钩子函数,修饰器。
- 节流,拉缩窗口大小,加购商品。页面滚动事件。
- 分时加载列表数据。
函数可以作为参数被传递
- 回调函数
- 生成节点,样式设置
- 数组排序Array.prototype.sort
let arr = [1, 4, 3]
arr.sort(function(a, b) { // 从小到大
return a - b
})
arr.sort(function(a, b) { // 从大到小
return b -a
})
函数可以作为返回值输出
- 判断数据的类型
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]';
};
var isType = function( type ){
return function( obj ){
return Object.prototype.toString.call( obj ) === '[object '+ type +']';
}
};
var isString = isType( 'String' );
var isArray = isType( 'Array' );
var isNumber = isType( 'Number' );
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
Type.isString( "str" ); // 输出: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();
alert ( script1 === script2 ); // 输出:true
高阶函数实现AOP
AOP(面向切面编程)的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些 跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后, 再通过“动态织入”的方式掺入业务逻辑模块中。这样做的好处首先是可以保持业务逻辑模块的纯净和高内聚性,其次是可以很方便地复用日志统计等功能模块。
实现技术有很多,这里我们通过扩展 Function.prototype 来做到这一点。
设计模式-装饰者模式
// 动态织入
Function.prototype.before = function(beforeFn) {
let _self = this // 这里的this指向调用before的函数,此处是func
return function() {
beforeFn.apply(this, arguments) // this指向调用闭包的函数,这里的this指window,2.执行before
return _self.apply(this, arguments) // 3. 执行func函数
}
}
Function.prototype.after = function(afterFn) {
let _self = this // this指向before
return function() {
let ret = _self.apply(this.arguments) // 1.指向before函数。
afterFn.apply(this, arguments)// 4,执行after函数
return ret
}
}
let func = function(){
console.log(2)
}
// 链式调用,先调用after,然后_self指向before,这时候调用before函数,里面则先执行before,然后func,
let func2 = func.before(function(){
console.log(1)
}).after(function(){
console.log(3)
})
func2()
应用
- 柯里化: 通过闭包来存储临时数据,最后一次解决问题
- 非柯里化:通过call和apply改变this指向,泛化this
- 节流:通过定时器,解决频繁调用
- 分时函数:解决大数据
- 惰性加载函数:通过重定义函数,解决条件分支
柯里化
又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后, 该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保 存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。
说白了就是暂存计算的值,直到需要的时候,扣动扳机,直接计算结果。
一般都是设计成两个部分,第一部分是传参存入数组,第二部分是不传参,通过参数个数判断,来执行数组循环输出,执行对应的数据操作.
// 计算每个月花销
// let monthlyCost = 0
// let cost = function(money) {
// monthlyCost += money
// }
// cost(100)
// cost(200)
// cost(300)
// console.log(monthlyCost)
// 每天记录并结算花了多少钱,而我们不关心每天花了多少钱,而是关心这个月共花了多少钱。
// 优化,使用闭包,如果传参则存入数组,没有参数则计算总和。
// let cost = (function() {
// let args = []
// return function() {
// if(arguments.length === 0) {
// let money = 0
// // 一次调用计算整个月花销
// for(let 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())
// 柯里化
let currying = function(fn) {
let args = []
return function() { // 返回的闭包1
if(arguments.length === 0) {
console.log(args)
return fn.apply(this, args) // 回调cost函数,返回闭包2
} else {
[].push.apply(args, arguments) // 存入数组
}
}
}
let cost = (function(){
let money = 0
return function() { // 返回的闭包2
for(let i = 0, l = arguments.length; i < l; i++){
money += arguments[i]
}
return money
}
})()
let costFn = currying(cost) // cost传入后暂时不调用,而是存了起来,返回了闭包1给costFn
costFn(100)
costFn(200)
costFn(300)
console.log(costFn()) // 执行cost函数返回的闭包2
非柯里化
非柯里化要解决的问题就是把泛化 this 的过程提取出来,让调用更加简洁和意图明了
- 一个对象怎么调用另一个对象的方法
call和apply都可以实现
var obj1 = {
name: 'sven'
};
var obj2 = {
getName: function(){
return this.name;
}
};
console.log( obj2.getName.call( obj1 ) ); // 输出:sven
我们常常让类数组对象去借用 Array.prototype 的方法,这是 call 和 apply 最常见的应用场
景之一:
Array.prototype.push.call( arguments, 4 ); // arguments 借用 Array.prototype.push 方法
console.log( arguments ); // 输出:[1, 2, 3, 4]
})( 1, 2, 3 );
在我们的预期中,Array.prototype 上的方法原本只能用来操作 array 对象。但用 call 和 apply
可以把任意对象当作 this 传入某个方法,这样一来,方法中用到 this 的地方就不再局限于原来
规定的对象,而是加以泛化并得到更广的适用性。
- 怎么把泛化 this 的过程提取出来
Function.prototype.uncurrying = function() {
let self = this // this =>Array.prototype.push 指向调用者
return function(){
let obj = Array.prototype.shift.call(arguments) // 截取push函数的第一个参数,obj 下面闭包的arguments [1 ,2, 3]
// console.log(obj) // 截取后的剩下 [4]
return self.apply(obj, arguments)
// => Array.prototype.push.apply(obj, arguments)
// 代入 Array.prototype.push.apply([1, 2, 3], 4) 输出[1, 2, 3, 4]
}
}
// push
let push = Array.prototype.push.uncurrying();
(function(){
push(arguments, 4)
console.log(arguments) // [1,2,3,4]
})(1, 2, 3);
另一种实现
// 另一种实现
Function.prototype.uncurrying = function () {
let self = this;
console.log(self)
return function () {
// 解绑定包装器
// Function.prototype.call.apply(self, arguments) =>
// 先执行apply, 那么call的this指向self, arguments作为参数传入
// 转换后得:self.call(arguments)
// 代入则是self.call(obj, 4),
// 相当于let obj = Array.prototype.shift.call(arguments) 然后 self.apply(obj, arguments)
return Function.prototype.call.apply(self, arguments);
};
};
for (let i = 0, fn, ary = ["push", "shift", "forEach"]; (fn = ary[i++]); ) {
Array[fn] = Array.prototype[fn].uncurrying();
}
let obj = {
0: 1,
1: 2,
length: 2,
};
Array.push(obj, 4);
console.log(obj); // {0: 1, 1: 2, 2: 4, length: 3}
let first = Array.shift(obj, 4);
console.log(first); // 1
console.log(obj); // {0: 2, 1: 4, length: 2}
Array.forEach(obj, function (v, k) {
console.log(v); // 2, 4
});
节流
函数有可能被非常频繁地调用,而造成大的性能问题.
作用:限制函数被频繁调用
- 场景:
- window.onresize
- mousemove
- 上传进度
- 原理:setTimeout按时间段来忽略一些事件请求。
- 实现
let throttle = function(fn, interval) {
let _self = fn // 存储回调
let timer = null
let firstTime = true
return function() {
let args = arguments
let _me = this // 这里是window
if(firstTime) { // 第一次直接执行
_self.apply(_me, args)
return firstTime = false
}
// 第二次看是否有定时器,有则拦截
if(timer) {
return false
}
// 定时清除之前的定时器,并清除变量,执行回调
timer = setTimeout(function(){
clearTimeout(timer)
timer = null
_self.apply(_me, args)
}, interval || 500)
}
}
window.onresize = throttle(function(){
console.log(1)
}, 500)
分时函数
解决一次加载大数据问题,使用setInterval分时执行函数,直到清空数据.
// console.time('1')
// // qq列表加载
// let arr = []
// for(let i = 0; i <= 1000; i++) {
// arr.push(i)
// }
// let renderFriendList = function(data) {
// for(let i = 0; i <= data.length; i++) {
// let span = document.createElement('span')
// span.innerHTML = data[i]
// document.body.appendChild(span)
// }
// }
// renderFriendList(arr)
// console.timeEnd('1') // 1: 3.242919921875 ms
// 分批进行
console.time('2')
let timeChunk = function(ary, fn, count){
let obj
let t;
let len = ary.length
let start = function(){
for(let i = 0; i < Math.min( count | 1, ary.length); i++) {
var obj = ary.shift()
fn(obj)
}
}
return function(){
t = setInterval(() => {
if(ary.length == 0) {
// console.timeEnd('2') // 2: 23001.0849609375 ms
return clearTimeout(t)
}
start()
}, 200);
}
}
let arr = []
for(let i = 0; i <= 1000; i++) {
arr.push(i)
}
let renderFriendList = timeChunk(arr, function(data) {
let span = document.createElement('span')
span.innerHTML = data
document.body.appendChild(span)
}, 8)
renderFriendList()
惰性加载函数
嗅探浏览器之间的实现差异的工作
第一种,分支判断,每次都需要判断一下if
var addEvent = function( elem, type, handler ){
if ( window.addEventListener ){
return elem.addEventListener( type, handler, false );
}
if ( window.attachEvent ){
return elem.attachEvent( 'on' + type, handler );
}
};
第二种,闭包回调,这里肯定会执行一次嗅探,即会走一次判断,并返回addEvent函数,但后面我们可能不会调用,而且还会延迟网页加载.
var addEvent = (function(){
if ( window.addEventListener ){
return function( elem, type, handler ){
elem.addEventListener( type, handler, false );
}
}
if ( window.attachEvent ){
return function( elem, type, handler ){
elem.attachEvent( 'on' + type, handler );
}
}
})();
第三种,惰性载入
此时 addEvent 依然被声明为一个普通函
数,在函数里依然有一些分支判断。但是在第一次进入条件分支之后,在函数内部会重写这个函
数,重写之后的函数就是我们期望的 addEvent 函数,在下一次进入 addEvent 函数的时候,addEvent
函数里不再存在条件分支语句
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="div1">绑定事件</div>
</body>
<script>
let addEvent = function(elem, type, handler) {
if(window.addEventListener) {
addEvent = function(elem, type, handler) {
elem.addEventListener(type, handler, false)
}
} else if(window.attachEvent) {
addEvent = function(elem, type, handler) {
elem.attachEvent('on' + type, handler)
}
}
addEvent(elem, type, handler)
}
let div = document.getElementById('div1')
addEvent(div, 'click', function(){
console.log(1)
})
addEvent(div, 'click', function(){
console.log(2)
})
</script>
</html>