函数式编程指南

函数式编程指南

是否遵循以下原则

可扩展性 易模块化 可重用性 可测性 易推理行

函数式编程的一些概念

声明式编程 纯函数 引用透明 不可变性

使用函数式来解决 array.map()只需要对应用在每个数组元素的行为予以关注,将循环交给系统的其他部分去空

[1, 1, 2, 3, 4].map(function (num) {
  return Math.pow(num, 2);
});

将 es6 的 lambda 表达式以及箭头函数的优势来将循环抽象成函数

Num => Math.pow(num, 2)

Function(num){
return Math.pow(num, 2);
}

函数式编程旨在尽可能的提高代码的无状态性和不变性

函数式编程

  1. 仅取决于提供的输入,而不依赖于任何在函数期间或调用期间间隔时可能变化的隐藏状态和外部状态

  2. 不会造成超出其作用域的变化, 例如修改全局对象或引用传递的参数

我们习惯了命令式 声明一个状态变为另一个状态的变量

常见副作用:

  1. 改变一个全局的变量属性或者数据结构 2.改变一个函数参数的原始值 3.处理用户输入 4.抛出一个异常,除非它又被当前函数捕获了 5.屏幕打印或记录日志 6.查询 html 文档、浏览器的 cookie 或访问数据库

常见的函数式编程技巧—- curry 科里化

确保一个函数有相同的返回值是一个优点, 引用透明

引用透明 又称为等式正确性

array.sort 函数是有状态的,会导致在排序过程中产生副作用,因为原始的引用被修改的了。

强迫自己去思考纯的操作,将函数看作永远不会修改数据的闭合功能单元,必然可减少这种潜在的 bug 的可能性

使用流式链来处理

函数链是一种惰性计算程序, 这意味着需要时才会执行。

学习响应式编程的第一部分就是学习函数式编程, 这种编程范式使用了一个叫做 observable 的概念,observable 能够订阅一个数据流,让开发者可以通过使用组合和链式操作优雅的处理数据

尽管 js 是面向对象的,但其并不具备 java 那样的语言中典型的继承关系。

面向对象的核心,就是将创建派生对象做为程序中代码重用的主要手段。

特加一些额外的功能。同对象的核心,就是将建派生对象作为程序中代码重用的主要手段。因此, CollegeStudent 将重用从其父类继承而来的所有数据与行为。但这使得在原对象中添加更多功能变得很棘手,因为它的后代们并不一定会适用于这些新功能。虽然 firstname 和 1 astname 适用于 Person 及其所有的子类型,但可以说让 workAddress 作为(从 Person 派生的) Employee 对象的一部分比起 Student 对象要更合理一些。之所以举样的例子,是为了解释在数据(对象属性)与行为(函数)的组织上,面向对象和函数式的主要差别。

面向对象的应用程序大多是命令式的,因在很大程度上依赖于使用基于对象的封装来保护其自身和继承的可变状态的完整性,再通过实例方法来暴露或修改这些状态。其结果是,对象的数据与其具体的行为以种内聚的包裹的形式紧耦合在一起。而这就是面向对象程序的目的,也正解释了为什么对象是抽象的核心。 再看函数式编程,它不需要对调用者隐藏据,通常使用一些更小且非常简单的数据类型。由于一切都是不可变的,对象都可以直接拿来使用的,而且是通过定义在对象作用域外的函数来实现的。换句话说,数与行为是松耦合的。正如在图 2.1 中看到的,函数式代码使用的是可以横切或工作于多种数据类型之上的更加粗粒度的操作,而不是一些细粒度的实例方法。在这种范式中,函数成为抽象的主要形式。 如图 2.1 所示,两种范式的差别随着横竖标的增长逐渐显现。在实践中,一些极好的面向对象代码均使用两种编程范式正是在这个相交的平衡点上。要做到这一

把对象视为不可变的实体或值,并将他们的功能拆分可应用在该对象上的函数 函数成为抽象的主要形式

// ex1
const res = data && data.length > 0 && data.map(...)
// ex2
const backgroundImage = state.popperImageURL
  ? state.popperImageURL.replace(...)
  : null

实例方法使用 this 访问对象的数据,这会产生副作用

将对象作为参数消除了 this 的使用,也消除了副作用

面向对象的关键是创建继承层次结构,并将方法与数据紧密的绑定在一起,函数式编程则更倾向于通过广义的多态函数交叉应用于不同的数据类型,同时避免使用 this

exports.Person = class Person {
  constructor(ssn, firstname, lastname, birthYear = null, address = null) {
    this._ssn = ssn;
    this._firstname = firstname;
    this._lastname = lastname;
    this._birthYear = birthYear;
    this._address = address;
  }

  get ssn() {
    return this._ssn;
  }

  get firstname() {
    return this._firstname;
  }

  set firstname(firstname) {
    this._firstname = firstname;
    return this;
  }

  get lastname() {
    return this._lastname;
  }

  get birthYear() {
    return this._birthYear;
  }

  get address() {
    return this._address;
  }

  get fullname() {
    return `${this._firstname} ${this._lastname}`;
  }
};

exports.Student = class Student extends Person {
  constructor(
    ssn,
    firstname,
    lastname,
    school,
    birthYear = null,
    address = null
  ) {
    super(ssn, firstname, lastname, birthYear, address);
    this._school = school;
  }

  get school() {
    return this._school;
  }
};

尽管 js 的原始类型不可改变,但引用原始类型的变量状态是可以被更改的。

封装是一个防止篡改的不错策略。 值对象模式, 值对象是指其相等性不依赖于标识或引用,而只基于值,一旦声明,其状态可能不会在改变

function zipCode(code, location) {
  let _code = code;
  let _location = location || "";
  return {
    code: function () {
      return _code;
    },
    location: function () {
      return _location;
    },
    fromString: function (str) {
      let parts = str.split("-");
      return zipCode(parts[0], parts[1]);
    },
    toString: function () {
      return _code + "-" + _location;
    },
  };
}

使用函数来保障 code 的内部访问权限, 通过返回一个对象字面接口来 公开一小部分方法给调用者,这个样就能够将_code_location 视为伪私有变量 只能通过闭包的方式由对象的字面定义中访问。

让方法返回一个新的副本是另一种实现不可变性的方式,在该对象上应用一次平移将产生一个新的 coordinate 对象

值对象是一个由函数式编程启发来的面向对象设计模式

object.freeze 机制 是一种浅冻结

我们可以使用递归函数来深冻结对象

var isObject = (val) => val && typeof val === "object";

function deepFreeze(obj) {
  if (isObject(obj) && !Object.isFrozen(obj)) {
    // 跳过已经冻结过的对象,冻结没有被冻结过的对象
    Object.keys(obj).forEach((name) => deepFreeze(obj[name]));
    Object.freeze(obj); // 冻结根对象
  }
  return obj;
}

module.exports = {
  deepFreeze: deepFreeze,
};

lenses 也被称为函数式引用, 从本质上是函数,是程序设计中用于访问和不可改变的操作状态,数据类型属性的解决方案,从本质上讲,lenses 与写实复制策略的工作方式类似。即采用一个能够合理管理和复制状态的内部存储部件

r.view(last);

函数只有在返回一个有价值的结果,而不是 null 或者是 undefined 时才有意义,反之它就会更改外部数据并产生副作用,为了达到学习目的,我们需要区分表达式(如返回一个值的函数)和语句(如不返回值的函数)命令式编程和过程式编程,大多是由一系列有序的语句组成,而函数式编程完全依赖于表达式,因此无知函数在该范式下并没有意义

一等函数

作为匿名函数或 landama 表达式给变量赋值

var square = x => x \* x;

作为成员方法给对象的属性赋值

var obj = {
  method: function (x) {
    return x * x;
  },
};

构造函数已函数形参, 函数体作为参数,并需要使用 new 关键字

var multiplier = new Function("a", "b", "return a * b");
multiplier(2, 3);

在函数都是 Function 类型的一个实例, 函数的 length 属性可以用来获取形参的数量,而像 apply 和 call 方法可以用来调用函数并加入上下文

匿名函数表达式的右侧是一个具有真空 name 属性的函数式对象。 可以通过匿名函数作为参数的方式

function applyOperation(a, b, opt) {
  return opt(a, b);
}

forEach 是函数式推荐的循环方式

通过使用高阶函数,开始呈现出声明式

作为构造函数与 new 一起使用 -- 这种方式会返回新创建对象的引用

function MyType(arg) {
  this.prop = arg;
}

var someVal = new MyType("some argument");
function negate(func) {
  return function () {
    return !func.apply(null, arguments);
  };
}

function isNull(val) {
  return val === null;
}

let isNotNull = negate(isNull);
assert.ok(!isNotNull(null)); //-> false
assert.ok(isNotNull({})); //-> true

apply 接受一个参数组成的数组,而后者接收参数列表, 第一个参数 thisArg 可用于按需修改函数的上下文,他们的函数签名如下:

Function.prototype.apply(this.arg, [argsArray]);
Function.prototype.call(this.arg, arg1, arg2);

通过 thisArg 修改函数上下文可以灵活, 如果 this.arg 是一个对象,它表示该函数将对象的成员方法被调用。如果 this.isArg 为 null, 则表示 该函数的上下文为全局对象, 该函数的行为就像一个全局函数。 通过 thisArg 修改函数上下文可以灵活地应用在许多不同的技术中。

尽管全局共享以及对象上下文在函数式 js 编程中没有太大用处,函数上下文。

闭包和作用域

闭包是一种能够在函数声明过程中,将环境信息与所属函数绑定在一起的数据结构。它是基于函数声明的文本位置的,因此也被称为围绕函数定义的静态作用域或词法作用域。

闭包不仅应用于函数式编程的高阶函数中,也可用于事件处理和回调中.

从本质上讲, 闭包就是函数继承而来的作用域。这类似于对象方法是如何访问其继承的实例变量的, 它们都具有父类型的引用。

var outerVar = 'Outer';
 function makeInner(params){
     var innerVar = 'Inner';
     function inner(){
         console.log(`I can see: ${outerVar}, ${innerVar}, and ${}`)
     }
     return inner;
 }

var inner = makeInner('Params');
inner();

闭包的实际应用

  1. 模拟私有变量

  2. 异步服务端调用

  3. 创建人工块作用域变量

闭包还可以用来管理的全局命名空间,以免在全局范围内共享数据,一些库和模块还会使用背包来隐藏整个模块的所有方法和数据,这被称为模块模式,它采用了立即调用函数表达式。

全局作用域、函数作用域、伪块作用域

变量名称解析与之前所述的原型名称解析链非常相似。

  1. 首先检查变量的函数作用域

  2. 如果不是在局部作用域, 那么逐层向外检查各词法作用域,搜索该变量的引用,直到全局作用域。

  3. 如果无法找到变量引用,那么 JavaScript 将返回 undefined

将所有的功能代码包裹在良好封装的模块之中是一个通用的最佳实践 给 iife 一个名字, 这样有用的信息更方便栈跟踪

使用函数是开发风格,操作数据结构其实就是将数据控制流是为一些高级组件的连接。

方法链是一种能够在一个语句中调用多个方法的面向对象编程方式。但这些方法属于同一个对象时,里又称为方法级联?

方法链模式

"Functional Programming".substring(0, 10).toLowerCase() + " is Fun";

函数式

concat(toLowerCase(substring("Functional Programming", 0, 10)), "is fun");
只要遵守不可变的原则, 函数式中也会应用这种隶属于单个对象实例的方法链

函数链

接受函数作为参数,以便能够注入替代解决特定任务的特定行为 代替冲充斥着类似变量与副作用的传统循环结构,从而减少所要维护以及可能做错的代码。

lambda 表达式适用于函数式的函数定义,因为它总是需要返回一个值。

函数名代表的不是一个具体的值而是(一种惰性计算的),可获取其值得描述, 换句话说它说明指向的是代表着如何计算该数据的箭头函数,就是在函数式编程中可以将函数作为数值使用的原因。

_.map(node.children, function (child) {
  Tree.map(child, fn, tree);
});

map 是一个只会从左往右遍历的操作,对于从右到左的遍历,必须先反转数组。 js 的 array.reverse()操作是不能用在这里使用的,因为他会改变原数组。可以将 lodash 中功能等价的 reverse 操作与 map 连接起来

_(person)
  .reverse()
  .map((p) => (p !== null && p !== undefined ? p.fullName() : ""));

容器的映射

_.reduce 收集结果

高阶函数 reduce 将一个数组中的元素精简为单一值,该值由每一个元素与一个累积值通过一个函数计算得出的。

map 和 reduce 会遍历整个数组

  • 实现 map

  • 实现 reduce

_.filter 删除不需要的元素

filter 是一个能够遍历数组种的元素并返回一个新子集数组的高阶函数, 其中的元素有谓词函数 p 计算得出的 true 值结果来决定

数组推导式和列表推导式

代码推理: 动态部分包括所有变量的状态和函数的输出, 静态部分包括可读性以及设计的表达水平

声明式惰性计算函数链

类 sql 的数据:函数即数据

这个函数包含以下两个主要部分 基例,也称之为终止条件,基地是能够令递归函数计算出具体结果的一种输入,而不必再重复下去, 递归条件则处理函数调用自身的一种输入必须小于原始值,如果速度不行不变小于就会无限的循环支持,程序崩溃。

es6 的尾调用优化

在函数式编程中,函数式输入和输出类型之间的数学映射

类型--函数的返回类型必须与接收函数的参数类型相匹配。 元数-接收函数必须声明至少一个参数才能处理上一个函数的返回值。

函数与元数: 元组的应用

元数定义为函数所接收的参数数量,也被称为函数的长度。

只有单一参数的纯函数是最简单的,因为其实现目的的非常单纯,意味着职责非常单一

函数式语言通过一个称为元组的结构来做到这一点类型--函数的返回类型必须与接收函数的参数类型相匹配。 元数-接收函数必须声明至少一个参数才能处理上一个函数的返回值。

但如何返回两个不同的值呢?函数式语言通过一个称为元组的结构来做到这一点元组是有限的、有序的元素列表,通常由两个或三个值成组出现,记为(a,b,c)。由此,可以使用一个元组作为 isValid 函数的返回值--它将状态与可能的错误消息捆绑,作为单个实体返回,并随后传递到另一个函数中(如果需要的话)。下面详细探讨 一下元组。 元组是不可变的结构,它将不同类型的元素打包在一起,以便将它们传递到其他压数中。将数据打包返回的方式还包括字面对象或数组等: return i status : false or return [false, 'Input is too long!'! message: 'Input is too long!"

但当涉及函数间的数据传输时,元组能够具有更多的优点。 不可变的--一旦创建,就无法改变一个元组的内部内容。 避免创建临时类型--元组可以将可能毫不相关的数据相关联。而定义和实例化一些仅用于数据分组的新类型使得模型复杂并令人费解。 避免创建异构数组--包含不同类型元素的数组使用起来颇为困难,因为会导致代码中充满大量的防御性类型检查。传统上对象。

在缺少参数的情况下调用非柯里化函数会导致缺失参数的实参变成 undefined

柯里化是一种在所有参数被提供之前挂起或延迟函数执行,将多参数函数转化为一元函数序列的技术

由于 JavaScript 本身不支持自动柯里化函数,因此需要编写一些代码来启动它

柯里化是一种词法作用域,其返回的函数,只不过是一个接受后续参数的简单嵌套函数包装器

function curry(fn) {
  return function (firstArg) {
    return function (secondArg) {
      return fn(firstArg, secondArg);
    };
  };
}

const name = (curry(function (lats, first) {
  return new StringPair("Barkley", "Rosser");
})[(first, last)] = name("Curry")("Haskll").values());

当完整的传递两个参数是,函数会完全求值

当提供一个参数时,返回一个函数,而不是将第二个参数当作 undefined

将多元函数转化为一元函数才是颗粒化的主要动机,客体化的可替代方案是部分应用和函数绑定。

组合后的函数也是输入和输出之间的引用透明映射

function compose(fn) {
  let args = arguments;
  let start = args.length - 1;
  return function () {
    let i = start;
    let result = args[start].apply(this, arguments);
    while (i--) result = args[i].call(this.result);
    return result;
  };
}

将 compose 增加到 JavaScript 的 function 原型中,还能函数链

// findStudent :: String -> Student
const findStudent = findObject(db);

const csv = ({ ssn, firstname, lastname }) =>
  `${ssn}, ${firstname}, ${lastname}`;

// append :: String -> String -> String
const append = R.curry(function (elementId, info) {
  console.log(info);
  return info;
});

// showStudent :: String -> Integer
const showStudent = R.compose(
  append("#student-info"),
  csv,
  findStudent,
  normalize,
  trim
);

let result = showStudent("44444-4444"); //-> 444-44-4444, Alonzo, Church
assert.equal(result, "444-44-4444, Alonzo, Church");
const runProgram = R.pipe(R.map(R.toLower), R.uniq, R.sortBy(R.identity));

let result = runProgram([
  "Functional",
  "Programming",
  "Curry",
  "Memoization",
  "Partial",
  "Curry",
  "Programming",
]);
assert.deepEqual(result, [
  "curry",
  "functional",
  "memoization",
  "partial",
  "programming",
]);
//-> [curry, functional, memoization, partial, programming]

使用函数组合子来管理程序的控制流

// checkType :: Type -> Type -> Type | TypeError

functor 把值包裹到容器中的模式是为了构建无副作用的代码

引用透明带来的好处是可以进行基于属性的测试

给 function 添加记忆

Function.prototype.memoized = function () {
  let key = JSON.stringify(arguments);
  this._cache = this._cache || {};
  this._cache[key] = this._cache[key] || this.apply(this, arguments);
  return this._cache[key];
};

Function.prototype.memoize = function () {
  let fn = this;
  if (fn.length === 0 || fn.length > 1) {
    return fn;
  }
  return function () {
    return fn.memoized.apply(fn, arguments);
  };
};

持续传递式样(cps) 是一种用于非阻塞程序的编程风格。 回调式函数被称为当前的延续,他们由调用者在返回值上提供。

rxjs 数据作为 Observable 序列

流是随时间发生的有序事件的序列

let res = [];
Rx.Observable.range(1, 3).subscribe(
  (x) => {
    console.log(`Next: ${x}`);
    res.push(x);
  },
  (err) => console.log(`Error: ${err}`),
  () => console.log("Completed")
);
assert.deepEqual(res, [1, 2, 3]);
Rx.Observable.fromEvent(document.querySelector("#student-ssn"), "keyup")
  .pluck("srcElement", "value")
  .map((ssn) => ssn.replace(/^\s*|\s*$|\-/g, ""))
  .filter((ssn) => ssn !== null && ssn.length === 9)
  .subscribe((validSsn) => {
    console.log(`Valid SSN ${validSsn}`);
  });

最后更新于

这有帮助吗?