目 录CONTENT

文章目录

JS中的高阶函数

俊阳IT知识库
2023-02-09 / 0 评论 / 0 点赞 / 430 阅读 / 2,667 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2023-02-09,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。
广告

文章已同步至掘金:https://juejin.cn/post/6844903921173921799
欢迎访问😃,有任何问题都可留言评论哦~

高阶函数是指至少满足下列条件之一的函数:

  • 函数可以作为参数被传递
  • 函数可以作为返回值输出

其实我们在平常撸代码的时候,会不知不觉使用到高阶函数

比如:数组中常用的方法:mapreducefiltersort 等等,好多都是高阶函数,(想了解数组中常用的方法,请参考我的文章《JS 数组中常用方法总结》)

这里就不再说这些方法了,没什么意思!!!

作为参数传递

先给一个简单的吧!!!

function func1(theFunc){ 
     theFunc(); 
} 
function func2(){ 
     alert("ok"); 
} 
func1(func2); 

就是这样的,把 func2 当做 func1 的参数传递

其实说起这个,最先想到的就是回调函数

提起回调,最经典的就是 Ajax 的异步请求
给出一段代码,看一哈:

// callback为待传入的回调函数
var getUserInfo = function(userId, callback) {
     $.ajax("http://xxx.com/getUserInfo?" + userId, function(data) {
        if (typeof callback === "function") {
            callback(data);
        }
    });
}

getUserInfo(666666, function(data) {
    alert (data.userName);
});

作为返回值输出

给出一个简单的例子看一下就好了

function a(){
    alert('aa')
    return function(){
        alert ('bb')
    }
}
newFun=a();
newFun();//aa  bb

实现AOP

其实上面的都没什么好说的,下面的才是重点

通常,在JS中实现AOP,都是指把一个函数“动态织入”到另外一个函数之中,具体的实现技术有很多,下面说几个方法,还是比较有难度的,

好好琢磨一下

before函数

before函数意思就是在某个函数执行前执行某个函数(什么某个某个,听不懂)
例子:

在 f1函数 执行前,执行 f2函数

const f1 = ()=>{
    console.log("正在执行任务...");
}

Function.prototype.before = function(beforeFn){
    return()=>{
        beforeFn()
        this()
    }
}

const f2 = f1.before(()=>{
    console.log("开始...");
})
f2()

还可以传参

//...args就是接收传递过来的所有参数
const f1 = (...args)=>{
    console.log("正在执行任务",args);
}
Function.prototype.before = function(beforeFn){
    return (...args)=>{ //...args 叫rest参数
        beforeFn()
        this(...args)
    }
}
const f2 = f1.before(()=>{
    console.log("开始...");
})
f2(1,2,3)

after函数

不知道怎么描述,看例子:

调用一个函数3次后,触发另一个函数

const after = (times,fn)=>{
    return ()=>{
        if(--times === 0){
            fn()
        }
    }
}
const f = after(3,()=>{
    console.log("调用三次后执行...");
})
f()
f()
f()

假如只调用了两次,那么是不会执行 f函数 的

all函数

以读取文件为例,在当前目录下新建两个文件name.txtage.txt
name.txt里写入内容:xiaoming
age.txt里写入内容:20

all函数,顾名思义,就是把所有文件读取成功,那么才可以,如果有一个读取不成功,那么就不行

下面给出两种实现方法,上代码:

//第一种
const fs = require("fs")
let content = []
let index = 0
fs.readFile("name.txt","utf8",(err,data)=>{
    content[0]=data
    index++
    out()
})
fs.readFile("age.txt","utf8",(err,data)=>{
    content[1]=data
    index++
    out()
})
function out(){
    if(index == 2){
        console.log(content);   //[ 'xiaoming' , '20'  ]
    }
}
//第二种
const fs = require("fs")
let content = {}
const after = (times,fn)=>{
    return ()=>{
        if(--times === 0){
            fn()
        }
    }
}
let newAfter = after(2,()=>{
    console.log(content);   //{ name: 'xiaoming', age: '20' }
})
fs.readFile("name.txt","utf8",(err,data)=>{
    content['name'] = data  
    newAfter()
})
fs.readFile("age.txt","utf8",(err,data)=>{
    content['age'] = data
    newAfter()
})

包括函数

在执行某个函数之前执行若干个函数,在执行某个函数之后执行若干个函数,事务函数

例子:

let f1 = function(){
    console.log("正在执行任务...")
}
let wrappers = [
    {
        // warpper
        init(){
            console.log("hello 1")
        },
        close(){
            console.log("bye 1")
        }
    },
    {
        // warpper
        init(){
            console.log("hello 2")
        },
        close(){
            console.log("bye 2")
        }
    }
]

// 定义一个函数,把上面两者结合起来
const work = (core,wrappers)=>{
    // core是核心函数
    wrappers.forEach(wrap=>{
        wrap.init()
    })
    core()
    wrappers.forEach(wrap=>{
        wrap.close()
    })
}

work(f1,wrappers)

这个就到此为止了

柯里化函数(currying)

函数柯里化,又称部分求值

一个currying的函数首先会接收一些参数,接收了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数
刚才传入的参数在函数形成的闭包中被保存起来。

转换成代码大致就这样:
f(1,2,3) --> f(1)(2)(3)()

举一个例子:

实现两个数相加

//普通函数写法
function add(x,y){
	return x+y;
}
add(1,2)	//3


//实现柯里化
let add = function(x){
    return function(y){
        return x + y;
    }
};
add(1)(2);  //3

通用的柯里化函数

function curry(fn) {
    let slice = Array.prototype.slice,  // 将slice缓存起来
        args = slice.call(arguments, 1);   // 这里将arguments转成数组并保存
        
    return function() {
        // 将新旧的参数拼接起来
        let newArgs = args.concat(slice.call(arguments));    
        return fn.apply(null, newArgs); // 返回执行的fn并传递最新的参数
    }
}

ES6版的柯里化函数

function curry(fn) {
    const g = (...allArgs) => allArgs.length >= fn.length ?
        fn(...allArgs) : 
        (...args) => g(...allArgs, ...args)

    return g;
}

// 测试用例
const foo = curry((a, b, c, d) => {
    console.log(a, b, c, d);
});
foo(1)(2)(3)(4);    // 1 2 3 4
const f = foo(1)(2)(3);
f(5);               // 1 2 3 5

两者实现的思路不同,可以尝试着分析一下

反柯里化函数(uncurrying)

与柯里化相反

  • 柯里化是为了缩小适用范围,创建一个针对性更强的函数;
  • 反柯里化则是扩大适用范围,创建一个应用范围更广的函数。

对应的代码转换就变成这样。
fn(1)(2)(3)() --> fn(1,2,3)

所以说,一个对象也未必只能使用它自身的方法,就比如callapply都可以完成这个需求,因为用callapply可以把任意对象当作this传入某个方法,这样一来,方法中用到this的地方就不再局限于原来规定的对象,而是加以泛化并得到更广的适用性。

uncurrying的目的是将泛化this的过程提取出来,将fn.call或者fn.apply抽象成通用的函数。

实现反柯里化

Function.prototype.uncurrying = function() {
    let self = this;
    return function() {
        return Function.prototype.call.apply(self, arguments);
    }
};

// 将Array.prototype.push进行uncurrying,此时push函数的作用就跟Array.prototype.push一样了,且不仅仅局限于只能操作array对象。
let push = Array.prototype.push.uncurrying();

let obj = {
    "length": 1,
    "0": 1
};

push(obj, 2);
console.log(obj);   // 输出:{0: 1, 1: 2, length: 2}

分时函数

当一次的用户操作会严重地影响页面性能,如在短时间内往页面中大量添加DOM节点显然也会让浏览器吃不消,结果往往就是浏览器的卡顿甚至假死。

这个问题可以用timeChunk函数解决
timeChunk函数可以让创建节点的工作分批进行,比如把1秒钟创建1000个节点,改为每隔200毫秒创建8个节点。

timeChunk函数接受3个参数:

  • 第1个参数是创建节点时需要用到的数据
  • 第2个参数是封装了创建节点逻辑的函数
  • 第3个参数表示每一批创建的节点数量
let timeChunk = function(ary, fn, count) {
    let t;
    let start = function() {
        for ( let i = 0; i < Math.min( count || 1, ary.length ); i++ ){
            let obj = ary.shift();
            fn( obj );
        }
     };

     return function() {
        t = setInterval(function() {
          if (ary.length === 0) {  // 如果全部节点都已经被创建好
              return clearInterval(t);
          }
          start();
        }, 200);    // 分批执行的时间间隔,也可以用参数的形式传入
    };
};

函数分流

当一个函数被频繁调用时,就可能会造成很大的性能问题
这个时候可以考虑函数节流,降低函数被调用的频率。

throttle函数的原理是,将即将被执行的函数用setTimeout延迟一段时间执行。如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求。

throttle函数接受2个参数:

  • 第一个参数为需要被延迟执行的函数
  • 第二个参数为延迟执行的时间
let throttle = function(fn, interval) {
    let __self = fn,    // 保存需要被延迟执行的函数引用
        timer,      // 定时器
        firstTime = true;    // 是否是第一次调用

    return function() {
        let args = arguments,
            __me = this;

        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 );

惰性加载函数

个人认为就是不同浏览器之间有相同的事情

在平常开发中,因为浏览器之间的实现差异,一些嗅探工作总是不可避免
比如我们需要一个在各个浏览器中能够通用的事件绑定函数addEvent

常见写法:

方法一:

let addEvent = function(elem, type, handler) {
    if (window.addEventListener) {
       return elem.addEventListener(type, handler, false);
    }
    
    if (window.attachEvent) {
          return elem.attachEvent('on' + type, handler);
    }
};

缺点:当它每次被调用的时候都会执行里面的if条件分支,虽然执行这些if分支的开销不算大,但也许有一些方法可以让程序避免这些重复的执行过程。

方法二:

let 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函数,这样看来,一开始的浏览器嗅探就是完全多余的操作,而且这也会稍稍延长页面ready的时间。

方法三:

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);
};

此时addEvent依然被声明为一个普通函数,在函数里依然有一些分支判断。
但是在第一次进入条件分支之后,在函数内部会重写这个函数,重写之后的函数就是我们期望的addEvent函数,
在下一次进入addEvent函数的时候,addEvent函数里不再存在条件分支语句。


+_+

0

评论区