[深入05] 柯里化 偏函数 函数记忆 尾递归

1,494 阅读10分钟

导航

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数

[react] Hooks

[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染

前置知识

函数的参数

  • length:函数的legth属性,返回函数预期的参数个数(形参)
  • arguments:arguments对象,包含了程序运行时的所有参数(实参)

类似数组的对象转换成数组

  • [].slice.call(类似数组的对象)
  • [].slice.apply(类似数组的对象)
  • Array.prototype.slice.call(类似数组的对象, x) // x是绑定this后传入slice函数的参数
  • Array.from()

偏函数和柯里化的概念

  • 柯里化 curry:
    • 将接收多个参数的函数,转换成接收一个单一参数的函数,并返回接收余下参数,并返回最终结果的新函数
    • 即当参数小于预期参数时,返回一个可以接收剩余参数的函数,参数大于等于预期参数时,返回最终结果
  • 偏函数 partial application:
    • 是固定一个或多个参数,产生另一个较小元的函数 n元函数 => 转换成n-x元函数

bind函数的手写实现

  • 因为在实现 partial偏函数 时,就是利用了bind函数bind函数的模拟实现
  // manual-bind
  const obj = {
    name: "woow_wu7",
    age: 20,
  };
  const fn2 = function (name, age) {
    // 注意:这里不能是箭头函数,不然this就不指向被绑定的函数了
    this.age = age;
    return this.name + this.age + name + age;
  };

  Function.prototype._bind = function () {
    const prevParams = Array.prototype.slice.call(arguments);
    const context = prevParams.shift() || window;
    const self = this;
    function bindFn() {
      const nextParams = Array.prototype.slice.call(arguments);
      const totalParams = prevParams.concat(nextParams);
      return self.apply(this instanceof self ? this : context, totalParams);
    }
    // bindFn.prototype = self.prototype; 使用寄生式继承替换掉原型链继承
    function Parasitic() {}
    Parasitic.prototype = self.prototype;
    bindFn.prototype = new Parasitic();

    return bindFn;
  };

  // bind返回函数 - 作为普通函数调用
  const refFn2 = fn2._bind(obj, "name7");
  const res2 = refFn2("age7");
  console.log(`res2`, res2);

  // bind返回函数 - 作为构造函数调用
  const refFn3 = fn2._bind(obj, "name7");
  const res3 = new refFn3("age7");
  console.log(`res3`, res3);

柯里化 curry

  • 柯里化函数,他接收函数A作为参数,运行后能够返回一个新的函数,并且这个新的函数能够处理函数A的剩余参数

1. 柯里化阶段一

  • 需求: 将add(1,2,3)转化成curryAdd(1)(2)(3)
  • 缺点:只能处理3个参数的情况,不能处理任意多个参数的情况,毫无扩展性

需求: 将add(1,2,3)转化成curryAdd(1)(2)(3)
缺点:只能处理3个参数的情况,不能处理任意多个参数的情况,毫无扩展性

function curryAdd(a) {
  return function(b) {
    return function(c) {
      return a+b+c
    }
  }
}
const res = curryAdd(1)(2)(3)
console.log(res, 'res1')

2. 柯里化阶段二

  • 需求:处理任意多个参数相加
  • 缺点:
    • 1. 处理相加逻辑的代码,只是在没有参数时才会执行,其他部分都在处理怎么收集所有参数,会多一次没有参数的调用
      • 更合理的方式是通过判断函数可以接收参数的总和,来判断是否参数收集完毕
    • 2. 相加逻辑可以单独抽离

function curryAdd() {
  let params_arr = [] // 用于收集所有实参
  function closure() {
    const args = Array.prototype.slice.call(arguments) // 每次调用闭包函数传入的实参,可以是多个
    if (args.length) {
      params_arr = params_arr.concat(args)
      // concat返回一个拼接过后的新数组,不改变原数组
      return closure
      // 如果还有参数,则继续返回闭包函数,则继续继续传参调用
    }
    return params_arr.reduce((total, current) => total + current)
    // 如果没有再传入参数,则相加所有传入的参数,缺点是要多一次没有参数的调用
  }
  return closure // 第一次调用curryAdd返回的闭包
}
const fn = curryAdd()
const res = fn(1,2)(3)(4)()
console.log(res, 'res'); // 10

2022/03/24 - curryAdd 可以修改为 curryCreator

3. 柯里化阶段三

function add(a,b,c,d,e) {
  return Array.prototype.slice.call(arguments).reduce((total, current) => total + current)
  // 注意:这里拿到的是实参的实际个数,即实参可能大于形参,当实参 (大于等于) 形参时,执行相加
}
function curryAdd(fn) {
  let paramsArr = []
  const paramsMaxLength = fn.length // function.length返回函数的形参个数,预期的参数个数为最大参数个数,即相加执行条件
  function closure() {
    const args = Array.prototype.slice.call(arguments)
    paramsArr = paramsArr.concat(args)
    if (paramsArr.length < paramsMaxLength) {
      return closure
    }
    // 当参数个数 大于等于 最大的期望个数,即形参的个数时,执行相加函数
    return fn.apply(this, paramsArr)
  }
  return closure
}
const fn = curryAdd(add)
const res = fn(1,2,3)(4)(5,6)
console.log(res, 'res');

4.柯里化阶段四 - 柯里化变通版

  • 上面版本的缺点:上面的版本需要知道add的参数length

function add() {
  return Array.from(arguments).reduce((total, current) => total + current)
}
function currentAdd(fn) {
  let paramsArr = []
  function closure() { // 该闭包函数只负责收集参数,处理相加可以在闭包上挂载新的方法getSum
    const args = Array.from(arguments)
    paramsArr = paramsArr.concat(args)
    return closure
  }
  closure.getSum = function() {
    return fn.apply(this, paramsArr) // getSum负责计算,利用了闭包中的变量paramsArr
  }
  return closure
}
const fn = currentAdd(add)
const resAdd = fn(1)(2,3)
const res = resAdd.getSum(); // 该方法的缺点就是需要单独再调用getSum函数
console.log(res, 'res')

2021/10/01 - 复习优化更新 - 阶段三

实现柯里化 - 阶段三

  • 解决:解决上面2的两个缺点,1.多了一次没有参数的调用的情况 2.解决只能相加的情况
  • 思路:
      1. 如何解决只能相加?通过传入处理函数,curry只负责收集参数和判断参数是否收集完毕
      1. 如果解决多一次调用?通过对比传入的处理函数的预期参数个数,即形参个数,因为实参是可以大于等于形参个数的
  • 缺点
    • 当参数大于处理函数executor的参数个数时,会返回计算的结果值,那如果我继续调用就报错了,因为值不能当函数调用了
    • add(1,2,3)
    • curry(1)(2)(3)(4) // 就报错了
    • curry(1)(2,3,4,5) // 这样就不会报错,因为计算返回值后没有在调用
  • 解决办法
    • 可以通过阶段二那样,通过不带参数的调用来解决是否是最后一个调用的问题
    • 不同的是,最后一个调用可以参数多了,但是不要紧,因为处理函数实参超出形参也是可以的,并且不影响计算结果
// stage3
const add3 = (a, b, c, d) => a + b + c + d;
const dec3 = (a, b, c, d) => a - b - c - d;
function curryClosure3(executor) {
  let totalParams = []; // 总的参数
  const totalParamsLength = executor.length; // 总的参数长度
  return function curry() {
    const currentParams = Array.prototype.slice.call(arguments);
    totalParams = totalParams.concat(currentParams);
    if (totalParams.length >= totalParamsLength) {
      // 大于等于说明参数收集完毕
      return executor.apply(this, totalParams);
    } else {
      return curry; // 未收集完参数,继续收集
    }
  };
}
const curry3Add = curryClosure3(add3);
const curry3Dec = curryClosure3(dec3);
const sum3 = curry3Add(1, 2)(3)(4, 5);
const dif3 = curry3Dec(20)(1)(2)(3);
console.log(`sum2`, sum3);
console.log(`dif3`, dif3);

图片.png

偏函数 partial

  • 将一个或者多个参数,固定到一个函数上,并产生返回一个更小元的函数
  • bind 函数类似 - 绑定一部分参数,返回一个新的函数,新的函数可以接受剩余参数
function add (a, b) {
  return a + b
}
function partial (fn) {...}

const addPartial = partial(add, 1)  // ------------------ 实现固定一部分参数1
const res = addPartial(2) // 3 -------------------------- 只传一部分参数 2

偏函数实现方式1

  • 通过bind方法实现
  • bind方法绑定this指向,同时也可以传入fn的部分和全部参数,并返回一个新函数,新函数可以传入参数作为fn的剩余参数

function add(a,b,c,d) {
  return a+b+c+d
}
function partail() {
  const params = Array.prototype.slice.call(arguments)
  const fn = params.shift() // 删除数组第一个元素,返回该元素,改变原数组
  return fn.bind(this, ...params)
  // 该params执行shift后已经改变\
  // params数组展开后的所有成员,都会作为fn的参数
  // 并且bind返回的新函数还可以传参
}

const fn = partail(add, 1, 2) // 固定了 1,2两个参数
const res = fn(3,4) // 除了固定的参数,剩下的参数在这里传入
console.log(res, 'res')

2021/10/01 复习优化更新 - 偏函数

  • 其实上面的写法可以用箭头函数优化
const partial = (fn, ...rest) => fn.bind(this, ...rest);
// 注意:箭头函数中的 this 指向函数定义时所在的对象,这里指向了window,也可以直接传入null,undefined,window

const fn = (num1, num2) => num1 + num2;
const resFn = partial(fn, 10);
const res = resFn(20);
console.log(`res`, res);

偏函数实现方式2

  • 这个其实就是手写bind函数,只是少了返回函数作为构造函数的逻辑

function add(a,b,c,d) {
  return Array.from(arguments).reduce((total, current) => total + current)
  // 相加实参
  // 因为实参可能大于形参
}
function partialAdd(fn) {
  let paramsFixed = Array.from(arguments).slice(1)
  // 除去fn的剩余参数
  // 注意:该方法和curry很相似,current第一调用是不需要传fn参数的,声明的是空数组,而在partial中需要传固定的参数
  const paramsMaxLength = fn.length // 形参个数
  function closure() {
    const args = Array.from(arguments)
    paramsFixed = paramsFixed.concat(args)
    if (paramsFixed.length < paramsMaxLength) {
      return closure
    }
    return fn.apply(this, paramsFixed) // 大于等于时
  }
  return closure
}
const fn = partialAdd(add, 2)
const res = fn(3)(4)(5)
console.log(res, 'res') // 14

函数记忆

  • 函数记忆:指将上次的(计算结果)缓存起来,当下次调用时,如果遇到相同的(参数),就直接返回(缓存中的数据)
  • 实现原理:将参数和对应的结果保存在对象中,再次调用时,判断对象key是否存在,存在返回缓存的值
    • 注意:函数是需要返回值的
  • 总结
    • 该函数不能有 副作用,应该是一个纯函数
    • 不然即使参数一样,也可能得到不同的结果,也就不能做缓存
function memorize(fn) {
  const cache = {}
  return function() {
    const key = Array.prototype.join.call(arguments, ',')
    if (key in cache) {
      return cache[key]
    }
    return cache[key] = fn.apply(this, arguments)
  }
}

我的简书:www.jianshu.com/p/eb583d764…

2021/10/01 复习优化更新 - 函数记忆

function memory(fn, ...params) {
  const paramsMap = {};
  const paramsArr = params;
  const paramsStr = params.join();
  let count = 0;

  return function () {
    if (Reflect.has(paramsMap, paramsStr)) {
      return Reflect.get(paramsMap, paramsStr);
    }
    console.log("该函数计算了多少次", ++count);
    const value = fn.apply(this, paramsArr);
    Reflect.set(paramsMap, paramsStr, value);
    return value;
  };
}

function fn(name, age) {
  return name + age;
}

const res1 = memory(fn, 1, 2)();
const res2 = memory(fn, 1, 2)(); // 调用了2次,但是计算过程只执行了1次
console.log(`res1`, res1);

图片.png

尾调用

尾调用: 函数执行的最后一个步骤,是返回另一个函数的调用,叫尾调用
优点:   
1. 尾调用,当里层函数被调用时,外层函数已经执行完,出栈了,不会造成内存泄漏
2. 在递归中,尾调用使得栈中只有一个函数在运行,不会造成性能问题


f(x) {
  return g(x)
}
// 尾调用,因为返回g(x)调用的时候,f(x)已经执行完


f(x) {
  return g(x) + 1
}
// 非尾调用,因为返回 g(x) 调用时,f(x)并未执行完,当g(x)执行完后,还有执行 g(x)+1,f(x)才执行完
// 函数只有执行完后才会出栈(执行上下文调用栈)


const a = x => x ? f() : g();
// f()和g()都是尾调用

const a = () => f() || g()
// f()非尾调用,还要接着判断

const a = () => f() && g();
// f()非尾调用

尾递归

递归  -- 尾递归和尾调用

1. 构成递归的条件
  - 边界条件
  - 递归前进段
  - 递归返回段
  - 当边界条件不满足时,递归前进
  - 当边界条件满足时,递归返回

2. 
Recursive:递归
factorial:阶乘

3. 尾调用和非尾调用
  - 尾调用和非尾调用的区别是 执行上下文栈不一样
  - 为调用:调用在函数结尾处
  - 尾调用的执行上下文栈,外层函数执行完就出栈,不会一层一层嵌套,不造成内存溢出
  - 尾调用自身就叫尾递归
// 尾调用
// 因为调用g(x)时,f(x)已经执行完了,就会出栈,不会压栈,不会造成内存溢出
function f(x){
    return g(x);
}
// 非尾调用
// 因为调用g(x)时,f(x)并未执行完,g(x)+1需要g(x)函数执行完,才会相加,返回后f(x)才会执行完
function f(x){
    return g(x) + 1;
}



------------------------------------------------------------------------------------

+++(例1)阶乘
     // recursive递归
    function factorial (n) {
      if (n < 2) return n
      return n * factorial(n-1)
    }
    const res = factorial(3)
    // 1. 3 => 3 * factorial(2) => 3 * 2 * factorial(1) => 3 * 2 * 1  
(分析)
    1. 每次返回一个递归的函数,都会创建一个闭包
    2. 所以维护这么多执行上下文栈,开销大,用以造成内存泄漏
    3. 优化方法:尾调用


+++(例1升级)阶乘优化
    function factorial(n, res) {
      if (n === 1) {
        return res
      }
      return factorial(n-1, n * res)
    }
(分析)
    第一次:factorial(3, 4* 1)
    第二次:factorial(2, 3* 4)
    第三次:factorial(1, 2* 12)
    第四次:24


+++(例1再升级)阶乘优化,多传了一个参数,可以用函数柯里化或者偏函数来实现

    function factorial(res, n) {
      if (n === 1) return res;
      return factorial(n * res, n-1)
    }

    function curring (fn) {
      let par_arr = Array.prototype.slice.call(arguments, 1)
      const closure = function () {
        par_arr = par_arr.concat(Array.prototype.slice.call(arguments))
        console.log(par_arr, 'par_arr')
        if (par_arr.length < fn.length) {
          return closure
        }
        return fn.apply(null, par_arr)
      }
      return closure
    }
    const curringFactorial =  curring(factorial, 1)
    const res = curringFactorial(4)
    console.log(res)

2021/10/01 复习优化更新 - 尾递归

  • 普通的递归
 // recursive
  function recursive(n) {
    if (n === 1) { // ------------------ 边界条件
      return 1; // --------------------- 递归返回段
    }
    return n * recursive(n - 1); // ---- 递归前进段
  }

  const res = recursive(4);
  console.log(`res`, res); // 1*2*3*4=24
  • 利用尾递归优化

// 尾递归优化
function recursive2(n, prevTotal) { // 类比 `reduce()`的参数函数中的前两个参数,第一个是累积变量,第二个是当前变量,这里反过来了
if (n === 1) { // 边界条件
  return prevTotal; // 返回段
}
return recursive2(n - 1, n * prevTotal); // 前进段,注意:这里就是 ( 尾递归 ),里层函数执行时,外层函数执行完出栈了
}
const res2 = recursive2(4, 1); // 这里第一个参数表示当前变量,第二个参数表示累积变量,累积变量的初始值从1开始,因为 ( 1 * 任何值 = 任何值 )
console.log(`res2`, res2)