js中好玩的函数

584 阅读7分钟

函数柯里化

柯里化百科:在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接收余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

收集百科中重要消息:

  • 非柯里化:接收多个参数的函数
  • 柯里化后:接收一个单一参数(最初函数的第一个参数)的函数
  • 返回一个新函数
    • 新函数可以满足接收余下参数
    • 新函数必须返回结果

对于第三点。我们要返回的新函数能够接收余下参数且新函数返回结果。要同时满足这两点需要注意

以简单的累加器作为案例

非柯里化实现累加器

let sum = function (money0: number, money1: number, money2: number) {
  return money0 + money1 + money2;
}

let result = sum(100, 200, 300);
console.log(result);

简单柯里化实现

// 简单柯里化
const curringCost = function (money0: number): Function {
  // 定义一个具名函数,作为在未结束前的返回值
  return function (money1: number, money2: number) {
    return money0 + money1 + money2;  // 0号位
  };
};

// 新的调用方法
const result = curringCost(100)(200, 300);
console.log(result);

我们为什么能取到money0的值呢?这里涉及到闭包,我们在”0号位“打断点看看作用域

image-20220123054349661.png

注意两点内容:

  • 图片中我们看到,在这个匿名函数的作用域链上有3个,一个是 本地 -> 闭包 -> 全局(查找变量的方式也是这个顺序),我们的money0因为在本地作用域上没有值,所以沿着作用域链查找到了闭包作用域,找到了money0然后返回
  • 我们注意定义的test变量,并不再闭包中出现,说明原函数curringCost已经释放了,money0这个变量成为了匿名函数的专属背包

问题来了,那么这么一个函数有什么用呢?

提前执行一部分仅仅需要依赖参数1,并且是后面不再变化的死任务

// 原函数
function say (name , args1, args2){ 
	console.log(`${name}说:${args1} + ${args2} = ${args1 + args2}`)
}
say("lilei", 1, 9); 
say("hanmeimei", 4, 9);
say("lilei", 3, 9);
say("hanmeimei", 7, 9);



// 柯里化后函数
function curringSay (name){ 
	return function (args1, args2){
		console.log(`${name}说:${args1} + ${args2} = ${args1 + args2}`)
	}
}

let lilei =  curringSay("lilei");
let hanmeimei = curringSay("hanmeimei");

lilei(1, 9);
hanmeimei(4, 9); 
lilei(3, 9);
hanmeimei(7, 9); 

面试题1:实现一个函数curringSum,可以curringSum(100)(200)(300)()这样计算出结果为600

面试题2:实现一个函数,可以将普通函数累加函数丢进去,进化成一个面试题1中的curringSum函数

反柯里化

interface Function {
  uncurrying: () => (obj:any, number: any) => {};
} 

Function.prototype.uncurrying = function(){
      // 将this指针保留下来
      let self = this;

      // 返回一个匿名函数被“push”接收了
      return function() {
        /**
         * push(currentObj, 5)
         * 获取第一个参数,也就是需要执行的对象(currentObj)
         * 这个时候arguments就只剩下“5”这个变量了
         */
        let obj = Array.prototype.shift.call(arguments);

        /**
         * 执行这个方法,注意这个方法是由谁调用的
         * 在这个案例中,这个匿名函数是被Array的push方法调用的
         * 所以这里的self为push这个方法
         */
        return self.apply(obj, arguments);
      }
  }

  // 提取push方法
  const push  = Array.prototype.push.uncurrying();
  let currentObj = {
    0 : 1,
    length : 1
  }

  push(currentObj, 5);  // {0: 1, 1: 5, length: 2} 
  console.log(currentObj)

反柯里化: 扩大适用范围,创建一个应用范围更广的函数。使本来只有特定对象才适用的方法,扩展到更多的对象。 定义来源:JS中的反柯里化

本案例中将push方法提取出来,让对象也能正确执行数组的push方法

注意: 这里的length需要设置为1,如果没有这个标识,会覆盖掉0号元素,这个应该是由数组的特殊性决定的

分时函数

/**
 * 定义分时函数
 * @param arr 任务列表
 * @param fn 任务执行函数
 * @param count 一次执行任务函数的个数
 * @param time 多久开启下一批任务
 */
function timeSharing<T>(arr:Array<T>, fn:Function,  count:number, time:number) {
    // 内部函数start进行任务操作
    function strat(){
      let index = 0,
          len = Math.min(arr.length, count);
      for (; index < len; index++) {
        const element = arr.shift();
        fn(element);
      }
    }
    
    // 定义定时器进行任务
    let timer = setInterval(() => {
      // 当任务跑完后就清理掉定时器
      if(arr.length === 0) {
        return clearInterval(timer); 
      }
      // 按照传入时间进行任务
      strat();
    }, time)
}

// 模拟一千个数据
let arr = [];
for (let index = 0; index < 1000; index++) {
   arr.push(index);
}

// 进行分时操作,一组10个任务,200毫秒处理完成一批
timeSharing(arr, function(num: number){
  console.log(num)
}, 10, 200);

分时函数能够分批解决大量任务,这样浏览器不会因为你一个任务时间过长而导致卡顿问题

惰性加载

其实我觉得这个还是比较勤快,也许是聪明的懒惰。如果方法里有判断,就先加载方法,后面就不需要再进行判断了,下面模拟 浏览器嗅探

  • if A浏览器 使用 aTest方法

  • if B浏览器 使用 bTest方法

公共代码

// 全局控制变量,模拟不同浏览器
const GLOBAL_CONTROL = "A";


function aTest(){
  console.log("A浏览器中使用")
}

function bTest(){
  console.log("B浏览器中使用")
}

基础版

/**
 * 简版
 * 需要使用if判断,每次执行都需要
 */
function compatibleFunction() {
  if(GLOBAL_CONTROL === "A") {
    aTest();
  } else {
    bTest();
  }
}
compatibleFunction();

console.log(compatibleFunction)

使用这种简版,每次都需要判断是使用什么方法,感觉就不是很智能

先行版

/**
 * 惰性加载函数 先行版 
 */
let lazyCompatibleBefore = (function() {
  if(GLOBAL_CONTROL === "A") {
    return aTest;
  } else {
    return bTest;
  }
})()
lazyCompatibleBefore(); 
console.log(lazyCompatibleBefore)

优点:

  • 避免了每次判断,只需要开始的时候执行一遍即可

缺点:

  • 但是如果项目中没有用到,那这个方法就累赘并且还占用了启动时间

懒加载版

/**
 * 惰性加载函数 懒加载版 
 */
let lazyCompatibleRuntime = function() {

  // 第一次执行后将lazyCompatibleRuntime替换为正确的方法
  if(GLOBAL_CONTROL === "A") {
    lazyCompatibleRuntime = aTest;
  } else {
    lazyCompatibleRuntime = bTest;
  }
  // 这里需要执行一遍,完成操作
  lazyCompatibleRuntime();
}

console.log(lazyCompatibleRuntime)
lazyCompatibleRuntime();
console.log(lazyCompatibleRuntime)

优点:

避免了每次判断,执行完第一遍后,后面调用都是正确的方法

同时避免了方法不用时,占用启动时间的问题

节流函数

/**
 * 节流函数
 * 通用的节流函数
 * @param fn       需要节流的函数
 * @param time     节流时间
 * @param isFrist  第一次是否立即执行
 */
function throttlingWrapper(fn:Function, time:any, isFrist = true) {
  var timer:any = null,
      _self = fn; 

  let throtting = () => {  
    let _me:any = this;

    // 如果timer有值,也就是还在执行中,则直接返回
    if(timer){
      return;
    } 

    // 是否首次触发
    if(isFrist){
      _self.apply(_me, arguments);
      isFrist = false;
      return;
    }

    // 节流函数本体
    timer = setTimeout(() => {
      _self.apply(_me, arguments);
      clearTimeout(timer);
      timer = null
    }, time)
  } 

  return throtting;
}

其他方法经过这个方法的加工后,我们能拿到一个全新的节流函数方法,这个方法的好处就在于,不管触发多少次,在规定时间内,只触发一次

面试题答案

题目一:实现一个函数curringSum,可以curringSum(100)(200)(300)()这样计算出结果为600
  • curringSum(100)调用返回的是一个函数,所以才可以这样继续调用curringSum(100)(200)
  • 当没有参数的时候就返回结果
  • 需要返回所有参数的和,所以我们需要使用闭包保存结果
var curringSum = function (args0: number): any {
  let sum: number = args0;

  let fun = function (args1?: number) {
    // 以这个参数有无为结束标志
    if (args1) {
      sum += args1;
      // 还没有结束,我继续将函数返回,让其继续调用
      return fun;
    } else {
      return sum;
    }
  }

  return fun;
}


let result = curringSum(100)(200)(300)();
console.log(result)

有没有发现这种调用方式是不是好玩多了

题目二 :实现一个函数,可以将普通函数累加函数丢进去,进化成一个面试题1中的curringSum函数
  • 需要一个累加函数
  • 需要返回一个柯里化的函数
function sum(...args: Array<number>) {
  let count = args.reduce((previousValue: number, currentValue: number) => previousValue += currentValue, 0);
  return count;
}


// 柯里化外套
const shellCurring = function (fn: Function): (args: number) => Function {
  let argsInner: Array<any> = [];

  const fun = function (args: number) {
    if (args !== void 0) {
      // 有参数,保存,并返回函数
      argsInner.push(args);
      return fun;
    } else {
      // 无参数,使用参入fn对数据处理
      return fn.apply(this, argsInner);
    }
  }

  return fun;
}

const curringSum = shellCurring(sum);

console.log(curringSum(100)(200)(300)())

总结

这些个函数大部分是在《JavaScript设计模式与开发实践》书中看到的案例,书中后面讲到的设计模式通俗易懂,可以去看看,还有函数内容以及书中的设计模式内容持续更新