Skip to content

什么是函数式编程

函数式编程 (Functional Programming, FP) 是一种编程范式,它将计算机程序视为一系列函数的组合, 在函数式编程中, 函数是第一等公民, 这意味着函数可以像数据一样被传递, 存储和操作, 这种范式强调无副作用的纯函数以及数据的不可变性, 虽然 JavaScript 不是纯函数式编程 的语言, 但是, JavaScript 支持许多函数式编程语言的特性

函数是 JavaScript 中的一等公民

  1. 声明调用(封装功能)
  2. 赋值/传参/返回(作为一种数据来传递)
  3. 构造函数/构造实例(类/面向对象/设计模式)
  4. 立即执行(封装模块/块级作用域)

函数式编程的特点:

  1. 纯函数
  2. 高阶函数
  3. 递归调用
  4. 惰性求值(缓存)
  5. 函数组合

什么是纯函数

  1. 相同的输入,得到相同的输出

  2. 不依赖也不影响外部的环境, 也不会产生的副作用(发送请求,改变外部数据,console,DOM操作,存储数据等等操作)

  3. 可移植性和可测试性

  4. 拿数组的函数举例:

    1. slice 就是一个纯函数
    2. splice 就不是一个纯函数(因为他会改变原数组)
js
// 不依赖外部环境(给定参数不算外部环境), 也不会改变外部环境
// (返回一个值, 而不是改变外部作用域的值)
// 可移植性: 不管在浏览器环境还是 node.js 环境总都可以正常执行
// 可测试性: 可以很好的进行单元测试
// 如: expect(uniqueArray([1,2,1,3,8])).toEqual([1,2,3,8])
function uniqueArray(array) {
  if (!Array.isArray(array)) {
    throw new TypeError('The parameter must be instance of Array');
  }
  const results = [];
  for (let i = 0; i < array.length; i++) {
    const item = array[i];
    if (!results.includes(item)) {
      results.push(item);
    }
  }
  return results;
}

什么是高阶函数

高阶函数是一个比较抽象的编程概念, 一个函数的参数中有函数或者返回值是函数, 那么, 这个函数就是 高阶函数 通俗来说: 操作其他函数的的函数, 就是高阶函数 如: forEach map bind 都可以算高阶函数, 在实际应用中, 比如常见的 防抖 节流

js
// 函数作为参数
function forIn(obj, callback) {
  if (!(obj && typeof obj === 'object')) {
    throw new TypeError('obj must be a object');
  }

  if (typeof callback !== 'function') {
    throw new TypeError('callback must be a function');
  }

  for (var k in obj) {
    obj.hasOwnPorperty(k) && callback(obj[k], k, obj);
  }
}

// 函数作为参数, 并返回一个新的函数
function debounce(fn, delay) {
  let timer = null;
  return function () {
    timer && clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, arguments);
    }, delay);
  };
}

什么是惰性函数

惰性函数(Lazy Function)并不是一个严格定义的术语, 但它通常指的是函数式编程中的一种概念, 与惰性求值(Lazy Evaluation)相关, 惰性求值是一种策略, 在函数式编程中, 这种策略可以应用 于函数调用, 使得函数仅在必要时执行

比如加事件的兼容函数(需要判断浏览器, 但是只需要判断一次, 因为浏览器的API不会在js执行的过程中改变的)

js
// 原函数: 这样虽然也可以实现效果, 但是每次都要去判断
// 浏览器是否为老旧浏览器, 这其实是没有必要的
function addEvent(el, type, handler, captcha) {
  if (el.addEventListener) {
    el.addEventListener(type, handler, captcha);
  } else if (el.attachEvent) {
    el.attachEvent('on' + type, function () {
      handler.call(el);
    });
  } else {
    el['on' + type] = handler;
  }
}

// 惰性函数: 同样也能实现功能, 但是只会判断一次
// 当这个代码被加载的时候, 这个立即执行函数表达式就会执行
// 并且返回一个结果(也是一个函数) 后面, 就无需再次判断,
// 而直接调用被返回的函数即可
var addEventLazy = (function () {
  if (window.addEventListener) {
    return function (el, type, handler, captcha) {
      el.addEventListener(type, handler, captcha);
    };
  } else if (window.attachEvent) {
    return function (el, type, handler) {
      el.attachEvent('on' + type, handler.bind(el));
    };
  } else {
    return function (el, type, handler) {
      el['on' + type] = handler;
    };
  }
})();

什么是缓存函数

缓存函数(Caching Function)是一种优化技术, 通过存储先前计算结果来避免重复计算相同输入的情况, 当函数被多次调用并且输入相同的情况下,缓存函数会直接返回之前存储的结果,而不是重新执行相同的计 算过程, 这种方法可以显著提高性能,尤其是在函数计算成本较高时, 这也是函数式编程中 惰性求值 的具体应用

在有的资料中也叫函数记忆,记忆函数

js
// --- 实现来自 underscroe.js 可以自定义生成key的方法 ----
function memorize(func, hasher) {
  var memoize = function (key) {
    var cache = memoize.cache;
    var address = String(hasher ? hasher.apply(this, arguments) : key);
    if (!cache[address]) {
      cache[address] = func.apply(this, arguments);
    }
    return cache[address];
  };
  memoize.cache = {}; // 在函数对象上挂一个 cache
  return memoize;
}

///////////////////////////
// 实际应用
///////////////////////////
// 1.一个将对象转字符串的纯函数
function join(obj, separator = ',') {
  console.log('join is called'); // 用于测试, 是否调用
  let str = '';
  Object.keys(obj).forEach((key) => {
    const value = obj[key];
    str += value;
    str += separator;
  });
  const sliceEndPos = str.length - separator.length;
  return str.slice(0, sliceEndPos);
}

// 2.给函数添加缓存, 不要每次循拼接字符串, 而是从缓存中获取
// 这种函数还有个别名叫记忆函数
var memorizedJoin = memorize(join, JSON.stringify);
var obj = {
  a: 1,
  b: 2,
  c: 3,
};

// join is called 只会输出一次,
// 那么也就说明: join 函数只会执行一次
// 后面都是直接从缓存中获取的值
console.log('第1次调用: ', memorizedJoin(obj));
console.log('第2次调用: ', memorizedJoin(obj));
console.log('第3次调用: ', memorizedJoin(obj));
console.log('第4次调用: ', memorizedJoin(obj));
console.log('第5次调用: ', memorizedJoin(obj));

函数科里化

函数柯里化(Currying)是一种将多参数函数转换为一系列单参数函数的技术, 柯里化的过程涉及到将一个多参数函数拆解为一系列嵌套的单参数函数, 每个单参数函数都返回一个新的函数,直到最后一个函数返回最终结果

通俗点说就是: 用闭包将函数的参数保存起来, 当函数的个数达到指定个数的时候, 才会执行

js
function curry(fn, preArgs) {
  var countArgs = fn.length;
  var preArgs = preArgs || [];

  return function () {
    var items = preArgs.concat(Array.prototype.slice.call(arguments));
    // 等同于: var items = [...preArgs, ...arguments]; 就是合并两个数组

    if (items.length < countArgs) {
      // 如果参数不够, 直接继续返回一个科里化函数, 然后把fn向下传递
      // 然后将上一次的args和这一次的 arguemnts 合并成一个新的数组
      // 然后把这个 items 传入到下一次科里化函数中
      // console.log("save-args:", { len, items });
      return curry.call(this, fn, items);
    } else {
      // 如果参数个数够了, 直接执行 fn 然后返回结果
      // console.log("exec:", { len, items });
      return fn.apply(this, items);
    }
  };
}

function sum(a, b, c) {
  return a + b + c;
}

var add = curry(sum);

// 这个不会递归, 会直接执行 fn.apply(this, items)
console.log(add(1, 2, 3));

// 后面这些都需要递归的去收集参数
console.log(add(1)(2)(3));
console.log(add(1, 2)(3));
console.log(add(1)(2, 3));
js
// 原理是一样的, 只不过更加简洁易读,
// 利用延展操作符替代 arguments 关键字
function curry(fn, ...preArgs) {
  const countArgs = fn.length;
  return function (...args) {
    const items = [].concat(...preArgs, ...args);
    if (items.length < countArgs) {
      return curry.call(this, fn, items);
    } else {
      return fn.apply(null, items);
    }
  };
}

防抖 && 节流

所谓的防抖节流本质上就是利用高阶优化高频率执行代码的一种手段

  • 防抖: n 秒后在执行该事件, 若在 n 秒内被重复触发, 则重新计时
  • 节流: n 秒内只运行一次, 若在 n 秒内重复触发, 只有一次生效
js
// 防抖: n秒后执行该方法, 如果重复触发(n秒内), 就重新计时
// 最简单的实现, 关注核心实现原理
function debounce(func, wait = 1000, thisArg = null) {
  let timer;
  return function (...args) {
    timer && clearTimeout(timer);
    timer = setTimeout(func.bind(thisArg, ...args), wait);
  };
}

// 防抖(第一次立即触发)
// 较为完善的实现, 支持更多参数
function debounced(func, wait = 1000, isImmediate = false, thisArg = null) {
  let timer = null;
  let shouldExecute = Boolean(isImmediate);
  return function (...args) {
    timer && clearTimeout(timer);
    if (shouldExecute) {
      // 只有第一次会立即执行, 后面都会走 else 分支
      func.apply(thisArg, ...args);
      shouldExecute = false;
    } else {
      timer = setTimeout(func.bind(thisArg, ...args), wait);
    }
  };
}
js
// 节流(使用时间戳): n秒内重复触发, 只会执行一次
// 最简单的实现, 关注核心实现原理
function throttle(func, wait = 1000, thisArg = null){
  let oldTime = Date.now();
  return function(...args) {
    const newTime = Date.now();
    if (newTime - oldTime >= wait) {
      func.apply(thisArg, arguments);
      oldTime = newTime;
    }
  };
}

// 节流: 时间时间戳 + 定时器
// 较为完善的实现, 实现一个更精确的节流
function throttled(func, wait = 1000, thisArg = null) {
  const timer;
  const startTime = Date.now();
  return function (...args) {
    timer && clearTimeout(timer);
    const nowTime = Date.now();
    if (nowTime - startTime >= wait) {
      // 如果间隔时间到了, 直接立即执行
      func.apply(thisArg, args);
      startTime = nowTime;
    } else {
      // 如果间隔时间没到, 延迟指定时间然后执行
      timer = setTimeout(func.bind(thisArg, ...args), wait);
    }
  };
}

偏函数(了解即可)

偏函数(Partial Application)是一种函数式编程技术, 它允许你提前固定一个函数的部分参数, 从而创建一个新的函数, 这个新函数只需接收剩余的参数即可, 通俗点说: 偏函数是将一个函数 的部分参数预先绑定, 返回一个新的函数, 这个新函数接收剩余的参数, 并能够直接使用已绑定的参数进行计算

偏函数和柯里化(Currying)有时会被混淆, 但它们是两个不同的概念:

  • 柯里化: 将一个多参数函数转换为一系列单参数函数的方式来调用
  • 偏函数: 固定一个函数的部分参数, 返回一个新的函数, 这个新函数接收剩余的参数
了解即可, 其实这种编程方式并不常用, 因为所谓的偏函数的本质就是一个高阶函数, 那既然如此, 为什么不直接使用高阶函数呢? 使用高阶函数, 可读性比偏函数要好, 还方便做单元测试
js
// 返回一个偏函数,并传入一个固定参数
function partial(fn, ...preArgs) {
  return (...postArgs) => fn(...preArgs, ...postArgs);
}

// 1.定义计算商品价格的函数(包含税费)
function calcPriceWithRate(rate, price) {
  return rate * price + price;
}

// 2.创建一个偏函数, 固定税率(10%)
var calcPriceWithRate10 = partial(calcPriceWithRate, 0.1);

// 3.使用偏函数计算商品价格
console.log(calcPriceWithRate10(50)); // 55
console.log(calcPriceWithRate10(100)); // 110
js
// 1.定义计算商品价格的函数(包含税费)
function calcPriceWithRate(rate, price) {
  return rate * price + price;
}

// 2.定义一个高阶函数
function calcPriceWithRate20(price) {
  return calcPriceWithRate(0.2, price);
}

// 3.使用高阶函数计算价格
console.log(calcPriceWithRate20(50)); // 60
console.log(calcPriceWithRate20(100)); // 120

Released under the MIT License.