javascript 中的纯函数

纯函数

引言

  • 对于函数,大家都不陌生,但是对于纯函数可能有些生疏。我们在写函数的时候,会肆无忌惮,充分的利用js的一些特性,比如使用作用域去修改外部变量,对象属性,数据结构等。这样写如果稍不注意,可能会出现隐藏的bug,使我们的函数,不再是一个可信赖的函数。而纯函数正是一个可以保证数据信赖的函数方式,他也是一个函数,只不过有他特有的要求规范,下面就让我们从函数的副作用开始,来学习纯函数的优缺点

什么是纯函数?

  • 纯函数是这样一种函数,相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用

比如常用的slicesplice,这两个函数的作用别二致。但是slice符合纯函数的定义,因为对于相同的输入,他能保证相同的输出。而splice却会嚼烂调用他的那个数组,然后吐出来;这样就会产生观察到的副作用

let arr = [1,2,3]
// 纯的
arr.slice(0, 3) // (3) [1, 2, 3]
arr.slice(0, 3) // (3) [1, 2, 3]
arr.slice(0, 3) // (3) [1, 2, 3]

// 不纯的
arr.splice(0, 3) // (3) [1, 2, 3]
arr.splice(0, 3) // []

在函数式编程中,我们讨厌这种会改变数据的笨函数。我们追求的是那种可靠的,每次都能返回同样结果的函数,而不是像 splice 这样每次调用后都把数据弄得一团糟的函数,这不是我们想要的。

来看另外一个例子

// 不纯的
let immutableState = 21
const checkAge = (age) => {
	return age >=immutableState
}

// 纯的
const checkAge = (age) => {
	let immutableState = 21
	return age >=immutableState
}

在不纯的版本中,checkAge的结果取决于immutableState这个变量是不是一个可变的值。 如果是可变的,那么对于immutableState的追踪,将会是一件棘手的事。我们可以通过参数的形式传递immutableState,也可以通过作用域的方式,限制变量的访问范围,当然我们也可以通过一些手段,使immutableState成为一个不可变(immutable) 对象.要实现这个效果,必须得创建一个对象,然后调用 Object.freeze 方法:

let immutableState = Object.freeze({
	immutableState: 21
})

什么是函数的副作用

如果函数与外部可变状态进行交互,则他是有副作用的

  • 任何修改外部的变量,对象属性,数据结构
  • 控制台(屏幕)输入输出的交互
  • 文件操作,网络操作
  • 抛出异常或错误终止
  • …不止于此

站在纯函数的角度来说,以上均是非纯函数的副作用。我们也不必太刻意使用纯函数,因为有很多功能,还是得依赖非纯函数来实现,毕竟你要和这个世界打交道,而不是只活在自己的世界里。

函数副作用demo - 输出最大数

let arrScore = [1, 3, 2, 5, 4];
const getMaxScore = () => {
  arrScore = arrScore.sort((a, b) => {
    return b - a;
  });
  return arrScore[0];
};
console.log('getMaxScore', getMaxScore()); // 5
console.log('arrScore', arrScore); //  [ 5, 4, 3, 2, 1 ]
问题分析
  1. 在函数内,直接使用了外部变量,并且sort会改变源数据。下次再使用源数据,数据处于不可知状态
问题解决
  1. 通过参数的形式,将数据传入
  2. 参数如果是引用类型,提前进行clone
  3. 避免使用修改源数据的api,使用其他方式实现
let arrScore = [1, 3, 2, 5, 4];
const getMaxScore = (arrScore) => {
  let temp = arrScore.slice();
  temp = temp.sort((a, b) => {
    return b - a;
  });
  return temp[0];
};
console.log('getMaxScore', getMaxScore(arrScore)); // 5
console.log('arrScore', arrScore); //  [ 1, 3, 2, 5, 4 ]
let arrScore = [1, 3, 2, 5, 4];
const getMaxScore = (arrScore) => {
  return Math.max(...arrScore);
};
console.log('getMaxScore', getMaxScore(arrScore)); // 5
console.log('arrScore', arrScore); //  [ 1, 3, 2, 5, 4 ]

追求“纯”的理由

可缓存性 (Cacheable)

  • 由于纯函数的输入就决定了输出,就和数学中的函数一样(所以我们也可以像数学公式一样推导),一个确定的输入对应一个确定的输出,

  • 因此我们可以根据传入的参数把结果缓存起来,这样后续以同样的参数调用的时候就可以直接返回结果,而不是重新执行一遍算法.

  • 实现缓存的一种典型方式是memoize技术,

vue 源码

/**
   * Create a cached version of a pure function.
   */
  function cached(fn) {
    var cache = Object.create(null);
    return function cachedFn(str) {
      var hit = cache[str];
      return hit || (cache[str] = fn(str));
    };
  }

  /**
   * Capitalize a string.
   */
  var capitalize = cached(function (str) {
    return str.charAt(0).toUpperCase() + str.slice(1);
  });

可移植性,自文档化(Portable / Self-Documenting)

  • 纯函数的依赖很明确,因此更易于观察和理解

  • 因为他们与环境无关,所以可以拷贝到任何地方运行,提高了代码的复用性。

  • 对比面向对象,你从类中拷贝一个方法,就要麻烦得多。

可测试性(Testable)

  • 纯函数让测试更加容易。

  • 只需要给定输入,断言输出就可以了。

  • 甚至有专门的测试工具帮我们自动生成输入,并断言输出。 比如Quickcheck

合理性,引用透明性(Reasonable)

  • 很多人相信使用纯函数最大的好处是引用透明性(referential transparency)。如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。
var Immutable = require('immutable');

var decrementHP = function(player) {
  return player.set("hp", player.hp-1);
};

var isSameTeam = function(player1, player2) {
  return player1.team === player2.team;
};

var punch = function(player, target) {
  if(isSameTeam(player, target)) {
    return target;
  } else {
    return decrementHP(target);
  }
};

var jobe = Immutable.Map({name:"Jobe", hp:20, team: "red"});
var michael = Immutable.Map({name:"Michael", hp:20, team: "green"});

punch(jobe, michael);
//=> Immutable.Map({name:"Michael", hp:19, team: "green"})

在上面的demo中,decrementHPisSameTeampunch都是纯函数,所以是透明引用。我们可以使用一种叫做“等式推到”的技术来分析代码
首先内联isSameTeam

var isSameTeam = function(player1, player2) {
  return player1.team === player2.team;
};

// 因为数据是不可变的,所以我们直接把team 替换为实际的值

var punch = function(player, target) {
  if("red" === "green") {
    return target;
  } else {
    return decrementHP(target);
  }
};

// if 执行的结果是false,所以可以把整个if 语句删掉

var punch = function(player, target) {
    return decrementHP(target);
};
// 然后再内联decrementHP,punch就编程一个让hp的值减1的调用

并行运算

  • 我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态(race condition)。

  • 并行代码在服务端 js 环境以及使用了 web worker 的浏览器那里是非常容易实现的,因为它们使用了线程(thread)。不过出于对非纯函数复杂度的考虑,当前主流观点还是避免使用这种并行。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于重构JavaScript高阶函数,有几个常见的方法和技巧可以使用。以下是一些常用的重构方法: 1. 使用箭头函数:箭头函数可以简化函数的定义,并且在某些情况下提供更简洁的语法。例如,可以将普通的匿名函数转换为箭头函数,从而减少代码的冗余。 2. 使用函数柯里化(Currying):函数柯里化是一种将多个参数的函数转换为接受一个参数的函数序列的技术。通过将函数柯里化,可以使函数更加灵活和可复用。 3. 使用函数组合:函数组合是将多个函数组合成一个新函数的技术。通过将多个函数组合起来,可以将复杂的操作拆分为简单的小步骤,提高代码的可读性和维护性。 4. 使用高阶函数作为参数或返回值:高阶函数是接受一个或多个函数作为参数或返回一个函数函数。通过使用高阶函数,可以实现更加灵活和可扩展的代码。 5. 使用函数函数是指在同样的输入下,始终返回相同的输出,并且没有任何副作用的函数。通过使用函数,可以减少代码的复杂性和不确定性。 6. 使用函数式编程范式:函数式编程范式强调使用函数、不可变数据和函数组合等技术来编写代码。通过使用函数式编程,可以提高代码的可读性、可维护性和可测试性。 以上是一些常见的重构方法,具体的重构方式取决于你的代码和需求。重构代码时,建议先编写测试用例,确保重构后的代码仍然正确运行。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值