04JS函数式编程

46 阅读13分钟

函数对象的属性

我们知道JavaScript中函数也是一个对象,那么对象中就可以有属性和方法。

1 属性name:一个函数的名字我们可以通过name来访问;

function foo(params) {}
console.log(foo.name);//foo

2 属性length:属性length用于返回函数参数的个数;

function foo(n1, n2, n3, n4) {}
console.log(foo.length);//4
// 剩余参数不参与计数,给参数默认值也不算在里面
function foo(n1, n2, n3, ...args) {}
console.log(foo.length);//3

3 arguments转成数组

//方法一
var arr1 = Array.from(arguments); 
var arr2 = [...arguments]; 
// 方法二
var arr3 = [];
for (var item of arguments) {
  arr3.push(item);
}
// 方法三
var names:["acd","abc","cab","cba"];
names.slice();
//当我们这样去调用时候,slice函数内部this是names。而slice.call去调用时候就是为了改变this
var arr4 = Array.prototype.slice.call(arguments);//slice会返回一个新的数组。

理解JS纯函数

函数式编程:函数是第一公民,我们可以将函数作为返回值,在其它函数间互相传递,函数故此也是变得及其的重要,这种编程方式就是一种函数式的编程。

函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;

  1. 在react开发中纯函数是被多次提及的;
  2. 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数;
  3. 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;

纯函数维基百科的定义:

在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:

  1. 此函数在相同的输入值时,需产生相同的输出。
  2. 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
  3. 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。

简单总结:确定的输入,一定会产生确定的输出;函数在执行过程中,不能产生副作用;

副作用的理解

副作用(side effect)其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一 些其他的副作用。

在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生 了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;

纯函数在执行的过程中就是不能产生这样的副作用,因为副作用往往是产生bug的 “温床”。

优点

  1. 可以安心的编写和安心的使用。
  2. 在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或 者依赖其他的外部变量是否已经发生了修改;
  3. 在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;

纯函数案例

slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组。

const arr = [1,2,3,4,5,6];
const a = arr.slice(1,4);
console.log(a); // [2,3,4]
console.log(arr);//[1,2,3,4,5,6]

splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改。

const arr = [1,2,3,4,5,6];
const a = arr.splice(1,1);
console.log(a); // [2]
console.log(arr);//[1,3,4,5,6]

相比之下,slice就是一个纯函数,不会修改传入的参数;

纯函数练习

纯函数

// 相同的输入产生相同的输出,不会产生副作用。
function foo(s1,s2) {
  return s1 + s2
}

非纯函数

// 例子一
var msg = '你好';
function bar() {  
  console.log('一些代码操作');
  msg = '我好,大家好'; // 修改了外界的msg
}
bar()

// 例子二
function baz(info) {
  info.name = 'cs2';
}
var obj = {
  name:'cs',
  age:18
}
baz(obj)
console.log(obj);//{ name: 'cs2', age: 18 }

JS柯里化

柯里化也是属于函数式编程里面一个非常重要的概念。

维基百科的解释:

  1. 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化;
  2. 是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术;
  3. 柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”;

总结:

// 正常调用
function foo(x1,x2,x3,x4) {
  	return x1+x2+x3+x4
}
foo(1,2,3,4);

// 所谓的柯里化
function bar(x1) {
  return function(x2) {
    return function(x3) {
      return function(x4) {
    		//进行操作
        return x1+x2+x3+x4; // 10
      }
    }
  }
}

bar(1)(2)(3)(4)

这里有一个函数,函数接收了多个参数,但是我们只处理一个(或前几个)参数(或者说:通过一部分的参数来调用它),然后返回一个函数,让返回的函数,去处理下一个参数,以此类推,直到参数被处理完成。

这样的一个过程就叫做函数的柯里化。

其实我们进行函数的柯里化之后,不论是运行还是说阅读性,都是变低了?那么为什么有这种写法?或者说它的优势体现在何处呢?

简化写法

var sum2 = x1 => x2 => x3 => x4 => x1+x2+x3+x4; 
sum2(1)(2)(3)(4)

// 相当于是
var sum3 = x1 => {
  return x2 => {
    return x3 => {
      return x4 => {
        return x1+x2+x3+x4;
      }
    }
  }
}
// 只是你的大括号和 return 可以省略。

柯里化的优势

让函数的职责单一

在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个 函数来处理;

那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果;

在设计模式里面有一个原则:单一职责原则。

在面向对象的时候,如果我们去封装一个类,那么应该让这个类尽可能完成单一的一件事情。

举个例子

我们想函数里面传递三个参数,每个参数需要经过处理后再调用。

function foo(x) {
  x = x +2;
  return function (y) {
    y = y * 2;
    return function (z) {
      z = z ** 2;
      return x + y + z;
    }
  }
}
const res = foo(x)(y)(z);
console.log( res );

当然,这仅仅是一个简单的小例子,如果对于每一个参数的处理是很多行代码,那么所有的处理逻辑都放在一个函数里面,那么这个函数不仅变得非常的臃肿,而且函数具体那一步是干了什么事情,都比较难去发现的。

柯里化的复用

例子一

function sum(x,y) {
  return x + y
}

// 每次调用都需要用到 10 
console.log(sum(10,5));
console.log(sum(10,36));
console.log(sum(10,12));
console.log(sum(10,369));

我们能够发现,每次对于函数的调用来说,都需要添加上10这个参数,那么用柯里化怎样去表示呢?

/ 柯里化之后的函数
function makeSum(x) {
  // 同时还可以对x进行逻辑处理
  return function (y) {
    return x + y
  }
}

const make10 = makeSum(10);
const res = make10(5);
console.log(res);

柯里化之后多的函数,我们可以对逻辑进行一些复用。

例子二

function log(date,type,message) {
  console.log(`[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`);
}

log(new Date(),'debug','有问题');
log(new Date(),'info','警告了');
log(new Date(),'debug','有问题2');

我们能够发现,在每次进行传递参数的时候,net Date()都是必须要传递的,如果说type是一样的话,那么也可能造成重复传递的现象。

var log = date => type => message => {
  console.log(`[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`);
}
// 当前时间
const nowTime = log(new Date());

// err类型
const errType = nowTime('error');
errType('错误了1');
errType('错误了2');

// debug类型
const debugType = nowTime('debug');
debugType('debug1');
debugType('debug2');

// 当然也可以这样写
nowTime('debug')('info');

经过柯里化之后,看起来非常的简便,每一个模块的功能也能够一眼看穿,不论是对代码后期的修改,还是其它的操作,都是非常有益的。

自动柯里化函数

function foo(x1,x2,x3,x4) {
  return x1 + x2 + x3 + x4;
}


// 封装一个函数,有人传入一个函数,返回一个柯里化后的函数。
function csChange(fn) {
  // 必须要返回一个新的函数.curried柯里化
  function curried(...args) {
    // 剩余参数接收多个参数
    // 判断接收到的参数个数,和原函数本身需要的参数个数
    // 原函数的参数个数,可以通过 fn.length 拿到
    if (args.length >= fn.length) {
      // console.log(this);//window
      return fn.apply(this,args);
    }else{
      // 参数没有达到个数之后,需要返回一个新的函数,来继续接收参数
      function curried2(...args2) {
        // 接收到参数后,需要递归去调用curried,来检查参数个数是否达到。
        return curried.apply(this,[...args,...args2]);// 继续进行调用
      }
      return curried2
    }
  }

  return curried
}

var newFoo = csChange(foo);

console.log(newFoo(10,20,30,40));
console.log(newFoo(10,20)(30)(40));
console.log(newFoo(10)(20)(30)(40));

小结:想要拿到某个函数的参数长度,那么就是函数名.length

  1. 因为它返回的是一个函数,所以首先创建一个函数(curried),将这个函数给返回。
  2. 判断传入的参数长度和原来的参数长度是否相等,如果相等直接调用原来的函数,将接收到的参数放入原来的函数里,不相等进入下一步。
  3. 当参数的长度没有达到原来函数参数的长度后,还是需要返回一个函数(curried2),让我们能够继续的将参数传递进来。
  4. 将传入进来的参数和之前传递进来的参数进行合并传递给curried,然后递归调用curried,当参数长度一致的时候,就会执行原来的函数。

组合函数

组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式:

  1. 比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;
  2. 那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复;
  3. 那么是否可以将这两个函数组合起来,自动依次调用呢?
  4. 这个过程就是对函数的组合,我们称之为 组合函数(Compose Function);

例子

比如说,传入一个数字,先乘以2,然后再进行平方,返回最终的结果。

function double(n1) {
  return n1 * 2
}
function square(n2) {
  return n2 ** 2
}

var num = 10;
var res = square(double(10));
console.log(res);//400

使用组合函数,我们可以这样去完成。

function composeFn(...fns) {
  // 边界判断,如果传入的不是一个函数
  var length = fns.length;
  for (var i = 0; i < length; i++) {
    if (typeof fns[i] !== "function") {
      throw Error(`index position ${i} not is a function`);
    }
  }

  // 如果是函数
  return function (...args) {
    var res = fns[0].apply(this, args); //第一个函数
    // 后面的函数
    for (var i = 1; i < length; i++) {
      var fn = fns[i];
      fn.apply(this, [res]);
    }
    return res;
  };
}
var newFn = composeFn(double, square);
var result = newFn(100);

with语句

所谓的with语句,就和我们平时的if语句一样,但是不一样的地方就是它是会形成自己的作用域的,而if则不是这样。

var message = '你好呀!';
var hot = '热';

var obj = {
  name1:"cs",
  message:'我是obj',
  age:18
}
// ()写一个对象
with(obj){
  // 先在with传入的 对象 里面去查找,没有找到去上一层,上上一层继续查找。
  console.log(message);//我是obj
  console.log(hot);//热
}

查找的顺序还是一样的,先从传入的对象里面查找,当找不到的时候,就会沿着作用域链一直向外查找。在严格模式下是不允许使用with的。

eval函数

eval是一个特殊的函数,它可以将传入的字符串当做JavaScript代码来运行。

var mesaage = 'Hello Word'; 
console.log(mesaage);

// 使用eval
var JString = "var mesaage = 'Hello Word'; console.log(mesaage);";
eval(JString);// 全局函数

最后会在控制台进行输出。当然不建议在开发中使用eval,eval代码的可读性非常的差(代码的可读性是高质量代码的重要原则),eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险;eval的执行必须经过JS解释器,不能被JS引擎优化;

严格模式

在ECMAScript5标准中,JavaScript提出了严格模式的概念(Strict Mode):

  1. 是一种具有限制性的JavaScript模式,从而使代码隐式的脱离了 ”懒散(sloppy)模式“;
  2. 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行;
// 由于JS太过于灵活,所以这样写代码也是可以的。
msg = 'hello';
console.log(msg);//hello

严格模式对正常的JavaScript语义进行了一些限制:

  1. 严格模式通过 抛出错误 来消除一些原有的静默(silent,写的有问题,但不会造成恶劣影响)错误;
  2. 严格模式让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
  3. 严格模式禁用了在ECMAScript未来版本中可能会定义的一些语法;

开启严格模式

// 给某个js文件开启严格模式
"use strict";

// 给某个函数开启严格模式
function sayHello(){
	"use strict";
}

当我们使用webpack等打包工具之后,会自动去开启严格模式的。

严格模式限制

  1. 无法意外的创建全局变量。
  2. 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常。
  3. 严格模式下试图删除不可删除的属性。
  4. 严格模式不允许函数参数有相同的名称。
  5. 不允许0的八进制语法。
  6. 在严格模式下,不允许使用with。
  7. 在严格模式下,eval不再为上层引用变量。
  8. 严格模式下,this绑定不会默认转成对象。
  9. 独立显示绑定this不传值的时候,这个时候this不再是window,而是undefined