JS高级函数的增强使用、纯函数、柯里化、组合函数的详细介绍-及手写柯里化、手写组合函数

1.函数属性和arguments

1.1 函数对象的属性

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

  • 属性name:一个函数的名字我们可以通过name来访问
function foo() {}
console.log(foo.name) // foo

var bar = function() {}
console.log(bar.name) // bar
  • 属性length:属性length用于返回函数参数的个数, 注意:rest参数(剩余参数)是不参与参数的个数的(先了解, 1.5中会详细讲到剩余参数);
var foo = (name, ages) => {}
console.log(foo.length) // 2

var baz = (name, age, ...args) => {}
console.log(baz.length) // 2

1.2 回顾arguments

arguments 是一个 对应于传递给函数的参数的类数组(array-like)对象

函数中的所有参数都会传入arguments中

function foo(x, y) {
  console.log(arguments) // [arguments] {'0': 10, '1': 20, '2': 30, '3': 40}
}
foo(10, 20, 30, 40)

array-like意味着它不是一个数组类型,而是一个对象类型

  • 但是它却拥有数组的一些特性,比如说length,比如可以通过index索引来访问;
  • 但是它却没有数组的一些方法,比如filter、map等;
function foo(x, y) {
  console.log(arguments.length); // 4
  console.log(arguments[0]); // 10
  console.log(arguments[1]); // 20
  console.log(arguments[2]); // 30
  console.log(arguments[3]); // 40
}
foo(10, 20, 30, 40);

1.3 arguments转Array

在开发中,我们经常需要将arguments转成Array,以便使用数组的一些特性和方法。

  • 常见的转化方式如下

转化方式一:

  • 遍历arguments,添加到一个新数组中;

  • function foo(x, y) {
      var newArguments = [];
      for (item of arguments) {
        newArguments.push(item);
      }
    }
    foo(10, 20, 30, 40)
    

转化方式二:较难理解(有点绕),了解即可

  • 调用数组slice函数的call方法;

  • function foo(x, y) {
      // 通过一个空数组调用slice, 让slice方法通过显式绑定, 让this指向arguments
      // 通过slice方法将arguments截取成一个新数组
      var newArguments = [].slice.apply(arguments);
    }
    foo(10, 20, 30, 40);
    

转化方式三:ES6中的两个方法

  • Array.from

  • function foo(x, y) {
      var newArguments = Array.from(arguments); //传入一个可迭代对象
    }
    foo(10, 20, 30, 40);
    
  • […arguments]

  • function foo(x, y) {
      var newArguments = [...arguments];
    }
    foo(10, 20, 30, 40);
    

1.4 箭头函数不绑定arguments

箭头函数是不绑定arguments的,所以我们在箭头函数中使用arguments会去上层作用域查找

// 箭头函数不绑定arguments, 如下写法会报错: arguments is not defined
var foo = (x, y) => {
  console.log(arguments);
};
foo(10, 20);
// 箭头函数中使用arguments回去上层作用域中查找
function foo(x, y) {
  var bar = () => {
    console.log(arguments);
  };
  bar();
}
foo(10, 20);

1.5 函数的剩余参数

ES6中引用了剩余参数(rest parameter),可以将不定数量的参数放入到一个数组中:

  • 如果最后一个参数是 … 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组

    function foo(x, y, ...agrs) {
      console.log(x, y); // 10 20
      console.log(agrs); // (2) [30, 40]
    }
    foo(10, 20, 30, 40);
    
    // 允许函数中只有一个剩余参数
    function foo(...agrs) {
      console.log(agrs); // (4) [10, 20, 30, 40]
    }
    foo(10, 20, 30, 40);
    

那么剩余参数和arguments有什么区别呢?

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参
  • arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
  • arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此
    来替代arguments的;

注意: 剩余参数必须放到参数的最后一个位置,否则会报错。

2.纯函数的理解和应用

2.1 纯函数理解

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

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

纯函数的维基百科定义:

  • 在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
  • 此函数在相同的输入值时,必须产生相同的输出
  • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关
  • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等

当然上面的定义会过于的晦涩,所以我简单总结一下:

  • 确定的输入,一定会产生确定的输出
  • 函数在执行过程中,不能产生副作用

2.2 副作用概念的理解

那么这里又有一个概念,叫做副作用,什么又是副作用呢?

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

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

    // 示例代码
    function foo(info) {
      console.log(info.name, info.age, info.height);
      info.flag = "打印结束了";
    }
    
    var obj = {
      name: "kaisa",
      age: 18,
      height: 1.88,
    };
    
    foo(obj);
    console.log(obj);
    
  • 例如上面代码中, 调用函数对全局变量obj添加了一个flag属性, 这就叫产生了副作用

纯函数在执行的过程中就是不能产生这样的副作用:

  • 因为副作用往往是产生bug的 “温床”。

2.3 纯函数的案例

我们来看一个对数组操作的两个函数:

  • slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组
  • splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改

slice就是一个纯函数,不会修改数组本身,而splice函数不是一个纯函数;

var names = ["aaa", "bbb", "ccc", "ddd"];

// slice截取数组时不会对原数组进行任何操作, 而是生成一个新的数组
var newNames1 = names.slice(0, 2);
console.log(newNames1);

// splice截取数组, 会返回一个新的数组, 也会对原数组进行修改
var newNames2 = names.splice(0, 2);
console.log(newNames2);
console.log(names);

判断下面函数是否是纯函数?

// 是纯函数, 确定输入一定产生确定的输出, 且没有副作用
function foo(item1, item2) {
  return = item1 + item2
}
// 不是纯函数, 有依赖全局中的foo, 确定的输入, 产生不确定的输出
var foo = 5;

function add(num) {
  return foo + num;
}

console.log(add(5)); // 10
foo = 10;
console.log(add(5)); // 15
// 不是提个纯函数, 有产生副作用, 对原对象的name属性进行了修改
function printInfo(info) {
  console.log(info.namem, info.age);
  info.name = "哈哈哈";
}

2.4 纯函数的作用和优势

为什么纯函数在函数式编程中非常重要呢?

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

React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:

  • React非常灵活, 但它有一个严格的规则:
  • 所有React组件都必须像纯函数一样, 保护他们的props不被更改

3.柯里化的理解和应用

3.1 柯里化理解

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

  • 是一种关于函数的高阶技术
  • 不仅被用于 JavaScript还被用于其他支持函数式编程的语言

我们先来看一下维基百科的解释:

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

维基百科的解释非常的抽象,我们这里做一个总结:

  • 传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数
  • 这个过程就称之为柯里化

柯里化是一种函数的转换,将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)©

  • 柯里化不会调用函数, 它只是对函数进行转换

示例代码:

function foo(x, y, z) {
  console.log(x + y + z) // 60
}
foo(10, 20, 30)
// 将上面代码转换成下面代码, 我们就称对这个函数进行了柯里化
// 柯里化函数 普通写法
function foo1(x) {
  return function foo2(y) {
    return function foo3(z) {
      console.log(x + y + z); // 60
    };
  };
}
foo1(10)(20)(30);
// 柯里化函数 箭头函数写法
var foo = (x) => {
  return (y) => {
    return (z) => {
      console.log(x + y + z);
    };
  };
};
foo(10)(20)(30);

// 简写箭头函数
var foo1 = x => y => z => console.log(x + y + z);
foo1(10)(20)(30);

3.2 柯里化优势

3.2.1 优势一: 函数的职责单一

那么为什么需要有柯里化呢?

  • 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理
  • 那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果

比如上面的案例我们进行一个修改:传入的函数需要分别被进行如下处理

  • 第一个参数 + 2

  • 第二个参数 * 2

  • 第三个参数 ** 2

    function add(x) {
      x = x + 2;
      return function (y) {
        y = y * 2;
        return function (z) {
          z = z ** 2;
          return x + y + z;
        };
      };
    }
    
3.2.2 优势二 - 函数的参数复用

另外一个使用柯里化的场景是可以帮助我们可以复用参数逻辑

  • makeAdder函数要求我们传入一个num(并且如果我们需要的话,可以在这里对num进行一些修改);

  • 在之后使用返回的函数时,我们不需要再继续传入num了;

    function makeAdder(num) {
      return function (count) {
        return num + count;
      };
    }
    
    // 将num参数固定为5, 以后和5相加, 只需要调用add5
    var add5 = makeAdder(5);
    add5(10); // 15
    add5(100); // 105
    
    // 将num参数固定为10, 以后和10相加, 只需要调用add10
    var add10 = makeAdder(10);
    add10(10); // 20
    add10(100); // 110
    

3.3 自动柯里化

实际开发中, 一般引用的第三库中都有自动柯里化的函数

我们尝试封装一个自动实现柯里化的函数, 目的是将多个普通的函数,转成柯里化函数(难度大, 了解):

// 假如有如下三个函数要进行柯里化
function foo(x, y, z) {
  console.log(x + y + z);
}

function sum(num1, num2) {
  return num1 + num2;
}

function logInfo(date, type, message) {
  console.log(`时间:${date} 类型${type} 内容${message}`);
}

// 之前我们是手动转换, 我们尝试手动实现一个自动柯里化的函数
function hyCurrying(fn) {
  function curryFn(...args) {
    // 两类操作:
    // 第一类操作: 继续返回一个新的函数, 继续接受参数
    // 第二类操作: 直接执行fn的函数
    if (args.length >= fn.length) {
      // 执行第二类
      // return fn(...args)
      return fn.apply(this, args);
    } else {
      // 执行第一类
      return function (...newArgs) {
        // return curryFn(...args.concat(newArgs))
        return curryFn.apply(this, args.concat(newArgs));
      };
    }
  }
  return curryFn;
}

// 对其他的函数进行柯里化
var fooCurry = hyCurrying(foo);
fooCurry(10)(20)(30);
fooCurry(55, 12, 56);

var sumCurry = hyCurrying(sum);
var sum5 = sumCurry(5);
console.log(sum5(10));
console.log(sum5(15));
console.log(sum5(18));

var logInfoCurry = hyCurrying(logInfo);
logInfoCurry("2022-06-01")("DEBUG")("我发现一个bug, 哈哈哈哈");

4.组合函数概念的理解

4.1 组合函数的理解

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

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

示例代码:

  • 如果我们想要对多个数字进行先乘2, 再平方的操作, 我们可以调用两个函数

  • var num = 100;
    
    // 第一个函数, 对数字乘2
    function fn1(num) {
      return num * 2;
    }
    
    // 第二个函数, 对数字平方
    function fn2(num) {
      return num ** 2;
    }
    
    // 对num乘2,再平方,可以依次调用fn1, fn2函数
    console.log(fn2(fn1(num)));
    console.log(fn2(fn1(10)));
    console.log(fn2(fn1(20)));
    
  • 如果有多个数字都需要这样的操作,就会变得麻烦, 我们可以将fn1 fn2两个函数组合起来, 生成一个新的函数来简化操作:

  • function composeFn(num) {
      return fn2(fn1(num));
    }
    // 再进行相同的操作我们就可以直接调用组合函数
    console.log(composeFn(num));
    console.log(composeFn(10));
    console.log(composeFn(20));
    

4.2 组合函数的封装

刚才我们实现的compose函数比较简单

我们需要考虑更加复杂的情况:比如传入了更多的函数,在调用compose函数时,自动将多个参数组合在一起, 挨个调用:

示例代码:

var num = 100;

// 第一个函数, 对数字乘2
function fn1(num) {
  return num * 2;
}

// 第二个函数, 对数字平方
function fn2(num) {
  return num ** 2;
}

// 封装自动组合函数的函数
function composeFn(...fns) {
  // 边界判断, 传入的参数至少有一个
  if (fns.length <= 0) return;
  // 边界判断, 传入的参数必须为函数类型
  for (var fn of fns) {
    if (typeof fn !== "function") {
      // 抛出一个错误, 并停止后续代码
      throw new Error("参数必须全部是函数");
    }
  }

  // 返回新的函数
  return function (...args) {
    var result = fns[0].apply(this, args);
    for (var i = 1; i < fns.length; i++) {
      var fn = fns[i];
      result = fn.apply(this, [result]);
    }
    return result;
  };
}

var newFn = composeFn(fn1, fn2);
console.log(newFn(num));
console.log(newFn(10));

5.额外知识

5.1 with语句的使用(了解)

with语句扩展一个语句的作用域链。

var obj = {
  name: "kaisa",
  age: 18,
  height: 1.88,
};

//使用with可以查找obj中的属性
with (obj) {
  console.log(name, age, height);
}

不建议使用with语句,因为它可能是混淆错误和兼容性问题的根源。

5.2 eval函数(了解)

内建(内置)函数 eval 允许执行一个代码字符串。

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

  • var string = `var name = "kaisa";
                  console.log(name);`;
    eval(string); // kaisa
    
  • eval会将最后一句执行语句的结果,作为返回值

  • var string = `var name = "kaisa";
                  console.log(name);
                  123`;
    var result = eval(string);
    console.log(result) // 123
    

不建议在开发中使用eval:

  • eval代码的可读性非常的差(代码的可读性是高质量代码的重要原则);
  • eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险
  • eval的执行必须经过JavaScript解释器,不能被JavaScript引擎优化

5.3 严格模式

5.3.1 认识严格模式

JavaScript历史的局限性:

  • 长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题;
  • 新的特性被加入,旧的功能也没有改变,这么做有利于兼容旧代码
  • 但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中

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

  • 严格模式很好理解,是一种具有限制性的JavaScript模式,从而使代码隐式的脱离了 ”懒散(sloppy)模式“
  • 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行

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

  • 严格模式通过 抛出错误 来消除一些原有的 **静默(silent)**错误;
  • 严格模式让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
  • 严格模式禁用了在ECMAScript未来版本中可能会定义的一些语法
5.3.2 开启严格模式

那么如何开启严格模式呢?严格模式支持粒度话的迁移:

  • 可以支持在js文件中开启严格模式;
  • 也支持对某一个函数开启严格模式;

严格模式通过在文件或者函数开头使用 use strict 来开启。

<script>
  // 给整个script标签开启严格模式
  "use strict"
</script>
function foo() {
  // 在foo函数作用域中开启严格模式
  "use strict";
}

没有类似于 “no use strict” 这样的指令可以使程序返回默认模式。

  • 现代 JavaScript 支持 “class” 和 “module” ,它们会自动启用 use strict
5.3.3 严格模式限制
  • 这里我们来说几个严格模式下常见的严格语法限制:

    • JavaScript被设计为新手开发者更容易上手,所以有时候本来错误语法,被认为也是可以正常被解析的

    • 但是这种方式可能会留下来安全隐患

    • 在严格模式下,这种失误就会被当做错误,以便可以快速的发现和修正;

  1. 无法意外的创建全局变量

    "use strict"
    function foo() {
      message = "意外创建的全局变量";
    }
    foo();
    console.log(message);
    

    非严格模式下, 不使用var变量创建变量, 会默认创建到全局变量

    严格模式下是不允许的, 如上代码创建报错

  2. 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常

    "use strict"
    var obj = {
    	name: "why"
    }
    
    // 明确设置obj对象中的name属性不可修改
    Object.defineProperty(obj, "name", {
      writable: false
    })
    
    obj.name = "kobe"
    console.log(obj.name); // name
    

    非严格模式下不允许修改, 但是也不会报错

    严格模式下, 明确说明obj中的name不可修改, 如果修改就会报错

  3. 严格模式下试图删除不可删除的属性

    "use strict"
    var obj = {
    	name: "why"
    }
    
    // 明确设置obj对象中的name属性不可修改
    Object.defineProperty(obj, "name", {
      configurable: false
    })
    
    delete obj.name
    console.log(obj); // {name: 'why'}
    

    非严格模式下, 不允许删除, 也不会报错

    严格模式下不允许删除, 如果删除就会报错

  4. 严格模式不允许函数参数有相同的名称

    "use strict"
    function foo(num, num)
    

    非严格模式下, 允许参数名字相同

    严格模式下, 不允许参数名相同, 相同就会报错

  5. 不允许0的八进制语法

    "use strict"
    console.log(0123)
    console.log(0o123)
    

    非严格模式下, 0开头的数字都会被默认为八进制

    严格模式下, 0开头8进制是不允许写的, 0o是允许的

  6. 在严格模式下,不允许使用with

  7. 在严格模式下,eval不再为上层创建变量

    "use strict"
    eval(`var message = "Hello World"`)
    console.log(message)
    

    非严格模式下, eval中创建的变量可以方位

    严格模式下, 访问eval中创建的就会报错

  8. 严格模式下,this绑定不会默认转成对象

    "use strict"
    unction foo() {
      console.log(this);
    }
    foo.apply("abc");
    foo.apply(123);
    foo.apply(undefined);
    foo.apply(null);
    
    foo() // undefined
    

    非严格模式下, 字符串和数字类型会转换成对应的包装类对象

    严格模式下, 不会转换成对象, 且严格模式下独立函数调用不绑定全局对象window, 而是一个undefined
    模式下**, 不允许参数名相同, 相同就会报错

  9. 不允许0的八进制语法

    "use strict"
    console.log(0123)
    console.log(0o123)
    

    非严格模式下, 0开头的数字都会被默认为八进制

    严格模式下, 0开头8进制是不允许写的, 0o是允许的

  10. 在严格模式下,不允许使用with

  11. 在严格模式下,eval不再为上层创建变量

    "use strict"
    eval(`var message = "Hello World"`)
    console.log(message)
    

    非严格模式下, eval中创建的变量可以方位

    严格模式下, 访问eval中创建的就会报错

  12. 严格模式下,this绑定不会默认转成对象

    "use strict"
    unction foo() {
      console.log(this);
    }
    foo.apply("abc");
    foo.apply(123);
    foo.apply(undefined);
    foo.apply(null);
    
    foo() // undefined
    

    非严格模式下, 字符串和数字类型会转换成对应的包装类对象

    严格模式下, 不会转换成对象, 且严格模式下独立函数调用不绑定全局对象window, 而是一个undefined

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

学全栈的灌汤包

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值