点击查看【music163】
5. 函数式编程 - 图1

实现 call、apply、bind

call

正常的使用 call 方法为fn.call(),我们会发现这和普通的对象obj.sayName()一样。fn 函数对象上有一个 call 属性,并且这个属性值是个函数。更重要的是所有函数对象上都有这个 call 属性。所以首先可以确定的是 call 属性肯定定义在函数原型上,apply,bind 也一样。

第二,函数 fn 调用 call 函数,fn 函数会被立即执行。这就说明 call 函数内部拿到了 fn 函数并且执行了它。那 call 函数怎么拿到的 fn 函数呢?其实准确的说,这里的 fn 不是一个函数,而是一个对象。是对象就好理解了,这不是典型的隐式绑定 this 吗?所以通过 this 就能拿到函数 fn。

我们就可以达到这样的效果:

  1. function fn(a, b) {
  2. console.log(this);
  3. console.log(a + b);
  4. };
  5. // 1. dd_call 定义在函数原型上
  6. Function.prototype.dd_call = function(thisArg) {
  7. // 2. dd_call 函数中的 this 指向了调用者函数 fn
  8. const fnObj = this;
  9. // 3. 手动调用 fn 函数
  10. fnObj();
  11. }
  12. fn.dd_call();

第三点:怎么实现 call 的绑定 this 功能。
假设我们想要绑定的 this 为一个对象,实际你会发现原生的 call 绑定的 this 其实都是转成包装类了。比如fn.call(123),fn 的 this 指向的就是Number(123)。所以说我们不用假设手动绑定的 this 为一个对象,它本就应该绑定一个对象。

js 中怎么将基本类型转成包装类? 通常会想到数字就要 Number() 函数,字符串就用 String()。对于确切的知道类型确实可以这么用,但是如果不知道类型就是可以使用Object(),它是通用的,它会自动识别类型并转成对应的包装类,如果包装的本就是一个对象,则不会进行包装。

那怎么将传入的 this 绑定到 fn 中呢?绑定 this 我们知道有 4 种。显示绑定可以,但我们本来就是来实现显示绑定的;this 需要对象形式,fn 也是函数对象,这很容易联想到隐式绑定。
假设传入要绑定的 this 为 123,我们可以让Object(123)动态的添加 fn 为属性,并且属性值就为 fn。然后Object(123).fn()执行函数,fn 中的 this 就隐式绑定为Number(123)。因为 fn 属性的属性值为 fn,所以 fn 也原封不动的执行了。
最后因为给 this 的包装对象多添加了一个属性 fn,所以使用完后最好删掉。

  1. function fn(a, b) {
  2. console.log(this);
  3. console.log(a + b);
  4. };
  5. // 1. dd_call 定义在函数原型上
  6. Function.prototype.dd_call = function(thisArg) {
  7. // 2. dd_call 函数中的 this 指向了调用者函数 fn
  8. const fnObj = this;
  9. // 3. 绑定传入的 this
  10. // 3.1 对要绑定的 this 进行包装并且当绑定undefined,null时绑为 window
  11. const objThis = thisArg ? Object(thisArg) : window;
  12. // 3.2 给 this 对象添加调用者函数为属性
  13. objThis.fnObj = fnObj;
  14. // 4. 执行调用者函数 fn,并且隐式绑定了传入的 this
  15. objThis.fnObj();
  16. // 4.1 删除多余属性——调用者函数 fn
  17. delete objThis.fnObj;
  18. }
  19. fn.dd_call(123);

第四点:参数和返回值。
fn 函数自身的属性,我们可以用剩余参数来接收,并且用展开运算符在调用的时候传入。返回值就接收一下直接返回即可。

  1. function fn(a, b) {
  2. console.log(this);
  3. console.log(a + b);
  4. };
  5. // 1. dd_call 定义在函数原型上
  6. // 5. 剩余参数接收所有参数
  7. Function.prototype.dd_call = function(thisArg, ...args) {
  8. // 2. dd_call 函数中的 this 指向了调用者函数 fn
  9. const fnObj = this;
  10. // 3. 绑定传入的 this
  11. // 3.1 对要绑定的 this 进行包装并且当绑定undefined,null时绑为 window
  12. const objThis = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window;
  13. // 3.2 给 this 对象添加调用者函数为属性
  14. objThis.fnObj = fnObj;
  15. // 4. 执行调用者函数 fn,并且隐式绑定了传入的 this
  16. // 5.1 展开运算符传入参数
  17. const result = objThis.fnObj(...args);
  18. // 4.1 删除多余属性——调用者函数 fn
  19. delete objThis.fnObj;
  20. return result;
  21. }
  22. fn.dd_call(123, 1, 2);
  23. // [Number: 123] { fnObj: [Function: fn] }
  24. // 3

apply

apply 和 call 没什么区别,唯一的区别就是参数需要是个数组。这就可能导致一个问题,因为 apply 的参数明确了只有一个,所以 dd_apply 接收参数只要一个形参就可以。
但是如果源函数没有参数呢,那这个形参的值就为 undefined,这时候展开运算符对 undefined 进行迭代就会报错,因为 undefined 是不可迭代的。
所以我们需要对形参进行类型缩小,或者设置默认值为空数组。

  1. function fn() {
  2. console.log(this);
  3. };
  4. // 给参数数组添加默认值为空数组
  5. Function.prototype.dd_apply = function(thisArg, paramArr = []) {
  6. let fn = this;
  7. const thisObj = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window;
  8. thisObj.fn = fn;
  9. // 类型缩小的方式,两种写法
  10. // paramArr = paramArr || [];
  11. // paramArr = paramArr ? paramArr : [];
  12. const result = thisObj.fn(...paramArr);
  13. delete thisObj.fn;
  14. return result;
  15. }
  16. fn.dd_apply(123);

bind

bind 其实也和前面差不多,无非就是用函数包裹一下隐式绑定 this 的过程和调用。调用 bind 返回的新函数,实际触发执行的还是thisObj.fn()。另外 bind 可以不需要以数组的方式接收参数,所以 dd_bind 接收参数要改成剩余参数接收。

  • 我们一般使用 bind 函数不会传入函数的参数,因为这会导致新生成函数的参数的值被固定了,只能是调用 bind 时传入的实参。

bind 的简单实现:

  1. function fn(a, b) {
  2. console.log(this);
  3. return a + b;
  4. };
  5. Function.prototype.dd_bind = function(thisArg, ...params) {
  6. let fn = this;
  7. // 返回新函数,并且新函数接收参数也是剩余参数搭配展开运算符
  8. return function(...newParams) {
  9. const thisObj = (thisArg !== undefined && thisArg !== null) ? Object(thisArg) : window;
  10. thisObj.fn = fn;
  11. const result = thisObj.fn(...params, ...newParams);
  12. delete thisObj.fn;
  13. return result;
  14. }
  15. }
  16. const foo = fn.dd_bind(123, 1, 3);
  17. console.log(foo(3, 4));
  18. // [Number: 123] { fn: [Function: fn] }
  19. // 4

arguments

arguments是一个类数组(伪数组)(array-like)对象保存了函数接收的参数。它和 this 一样默认存在。

  1. function foo(num1, num2) {
  2. console.log(arguments)
  3. }
  4. foo(1,2,3,4,5) // [Arguments] { '0': 'a', '1': 'b', '2': 'c', '3': 'd', '4': 'e' }

为什么是类数组对象?因为它不是一个数组,但是有一些数组的特性,比如:

  • 拥有 length 属性,也可以通过 index 索引来访问元素
  • 但是它却没有数组的一些方法,比如forEach、map等; ```javascript function foo(num1, num2) {

    // 1. 获取指定索引的参数 console.log(arguments[0])

    // 2. 获取参数长度 console.log(arguments.length)

    // 3. 获取 arguments 所在的函数 console.log(arguments.callee) // 注意 callee 会返回所在的函数,所以要小心递归操作 // arguments.callee; // 无限递归了 }

foo(‘a’, ‘b’, ‘c’, ‘d’, ‘e’)

// a // 5 // [Function: foo]

  1. <a name="AMHBM"></a>
  2. ## arguments 转成真正的数组 Array
  3. **方式一:全部遍历后一个一个放入新数组**
  4. ```javascript
  5. let arr = []
  6. function foo(num1, num2) {
  7. for (let i = 0; i < arguments.length; i++) {
  8. arr.push(arguments[i])
  9. }
  10. }
  11. foo('a', 'b', 'c', 'd', 'e')
  12. console.log(arr) // [ 'a', 'b', 'c', 'd', 'e' ]

方式二:利用**slice()**的返回值

  • arr.slice(begin, end)能裁剪数组并返回一个新数组。

重点在于返回值是数组,所以利用它把arguments给假装裁剪,从而得到arguments数组。

关键点:arguments并不是真正的数组,无法调用slice()方法,所以利用数组原型对象来调用,并且通过call()来使slice()arguments进行裁剪。如果没有修改 this,则 slice 就是裁剪隐式绑定的数据原型对象了。

也受到一点启发:当需要使用其他某个对象中的方法的时候,可以使用它的原型对象来调用这个方法,并通过**call()**方法来修改对象中方法的 this 来对我们想要处理的目标生效。

  1. let arr = []
  2. function foo(num1, num2) {
  3. arr = Array.prototype.slice.call(arguments)
  4. // 效果是一样的 arr = [].slice.call(arguments)
  5. }
  6. foo('a', 'b', 'c', 'd', 'e')
  7. console.log(arr) // [ 'a', 'b', 'c', 'd', 'e' ]
  1. Array.prototype.mySlice = function(start, end) {
  2. // 将this指向的数组保存一下
  3. var arr = this
  4. // 参数默认值的初始化
  5. start = start || 0
  6. end = end || arr.length
  7. // 朴实无华的按参数区间遍历,然后一个一个放进新数组并返回
  8. var newArray = []
  9. for (var i = start; i < end; i++) {
  10. newArray.push(arr[i])
  11. }
  12. return newArray
  13. }

方式三:es6 新语法

  • 剩余参数**...剩余参数名**:多出来的实参会以数组的形式保存到剩余参数中 ```javascript function foo(n1, n2, …m) { console.log(m); }

// n1,n2 精确匹配参数,剩余的参数都会被保存到 m 数组中 foo(‘a’, ‘b’, ‘c’, ‘d’, ‘e’) // [ ‘c’, ‘d’, ‘e’ ]

  1. 利用剩余参数是数组这点,我们可以让剩余参数全部接收`arguments`
  2. ```javascript
  3. function foo(...m) {
  4. let arr = m;
  5. console.log(arr) // [ 'a', 'b', 'c', 'd', 'e' ]
  6. }
  7. foo('a', 'b', 'c', 'd', 'e')
  • **Array.from()**方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。可以直接遍历伪数组并返回一个新数组。 ```javascript function foo() { let arr = Array.from(arguments) console.log(arr) // [ ‘a’, ‘b’, ‘c’, ‘d’, ‘e’ ] }

foo(‘a’, ‘b’, ‘c’, ‘d’, ‘e’)

  1. - **展开运算符**`**...数组名**`**,将数组中的元素一个一个遍历取出来**
  2. ```javascript
  3. let nums = [1, 2, 3]
  4. let n = [...nums] // 一个一个取出元素放入空数组[]中
  5. console.log(n) // [1, 2, 3]
  6. function foo(num1, num2, num3) {
  7. console.log(num1 + num2 + num3)
  8. }
  9. // 元素一个一个赋值给形参传入函数
  10. // 注意和剩余参数区分,剩余参数是函数定义时的操作,是个形参;展开运算符是函数执行时,是实参
  11. // 无论是 nums 元素多了还是foo 形参少了,都是一个一个赋值完为止。
  12. foo(...nums) // 6
  1. function foo() {
  2. let arr = [...arguments]
  3. console.log(arr)
  4. }
  5. foo('a', 'b', 'c', 'd', 'e') // [ 'a', 'b', 'c', 'd', 'e' ]

箭头函数不绑定 arguments

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

  • 浏览器环境下,全局作用域没有 arguments ,会直接报错
  • node 环境下,全局中有 arguments ```javascript function foo() {

    let fn = () => { console.log(arguments) }

    fn(666) // 箭头函数中没有 arguments,不打印 666 }

foo(‘hhh’) // [Arguments] { ‘0’: ‘hhh’ }

  1. <a name="DDDke"></a>
  2. # 纯函数
  3. 函数式编程是指按函数式的规范编程,函数式是一种编程的规范,与之相对的是面向对象编程。函数式编程中有一个非常重要的概念叫纯函数。不光 JavaScript 有纯函数,任何一个符合函数式编程的语言都有。
  4. <a name="TydPL"></a>
  5. ## 什么是纯函数
  6. 符合以下特点的函数称为纯函数:
  7. 1. 同一个输入必得同一个输出,输入是确定的,输出就是确定的,不会被函数内部或外部的隐藏条件影响。
  8. 2. 函数执行不会产生额外的副作用
  9. - 副作用表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储
  10. ```javascript
  11. var names = ["abc", "cba", "nba", "dna"]
  12. // slice只要给它传入一个start/end, 那么对于同一个数组来说, 它会给我们返回确定的值
  13. // slice函数本身它是不会修改原来的数组
  14. // slice函数本身就是一个纯函数
  15. var newNames1 = names.slice(0, 3)
  16. console.log(newNames1) // [ 'abc', 'cba', 'nba' ]
  17. console.log(names) // ["abc", "cba", "nba", "dna"]
  18. // splice在执行时, 有修改掉调用的数组对象本身, 修改的这个操作就是产生的副作用
  19. // splice不是一个纯函数
  20. var newNames2 = names.splice(2)
  21. console.log(newNames2) // [ 'nba', 'dna' ]
  22. console.log(names) // [ 'abc', 'cba' ]

纯函数的优势

最大的优势就是确定性,让你开发的时候可以大胆的使用。

你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改

你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;

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

柯里化

柯里化也是属于函数式编程里面一个非常重要的概念。在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化。

是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术;
柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”;

说人话就是:将一个可以接受多个参数的函数,改成只接收一个参数的函数,并且这个函数会返回一个新函数,新函数中再按次一个一个接收剩余的参数。这个过程就叫函数柯里化。

柯里化就相当于将一个函数整体拆成链表一样一串的函数。
最核心的点就是:柯里化函数能让我们对每一个参数在函数中进行单独处理,并让处理结果传递给下一个函数。这让函数更加灵活了,可以定制参数,定制一串中的某个函数,从而改变整个函数的功能。

  1. // 未柯里化函数
  2. function foo(n1, n2, n3) {
  3. return n1 + n2 + n3;
  4. }
  5. console.log( foo(1, 2, 3) ) // 6
  6. // 柯里化
  7. function fn(n1) {
  8. return function(n2) {
  9. return function(n3) {
  10. return n1 + n2 + n3;
  11. }
  12. }
  13. }
  14. console.log( fn(1)(2)(3) ) // 6
  15. // 简化版柯里化
  16. let bar = (n1) => (n2) => (n3) => n1 + n2 + n3
  17. console.log( bar(1)(2)(3) ) // 6

函数柯里化的优势

符合单一原则

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

  1. let bar = (n1) => {
  2. // 单独对 n1 处理
  3. n1 += 1;
  4. return (n2) => {
  5. // 单独对 n2 处理
  6. n2 += 1;
  7. return (n3) =>{
  8. // 单独对 n3 处理
  9. n3 += 1;
  10. return n1 + n2 + n3;
  11. }
  12. }
  13. }
  14. console.log( bar(1)(2)(3) ) // 9

复用参数逻辑

对参数单独处理也可以使得一些逻辑代码复用。比如打印日志:

  1. // 为柯里化
  2. function log(date, type, message) {
  3. console.log(`[${date.getHours()}:${date.getMinutes()}][${type}]:${message}`);
  4. }
  5. log(new Date(), 'DATA', '数据错误')
  6. log(new Date(), 'FORM', '表单错误')
  7. log(new Date(), 'PHOTO', '图片错误')
  8. // 会发现传入参数的时候,new Date() 写了很多次
  9. // 柯里化
  10. let log = date => type => message => {
  11. console.log(`[${date.getHours()}:${date.getMinutes()}][${type}]:${message}`)
  12. }
  13. // 对第一参数进行复用,其实也体现了参数定制化,比如修改第一个参数,后面的结果都会变
  14. let timeLog = log(new Date())
  15. timeLog('DATA')('数据错误')
  16. timeLog('FORM')('表单错误')
  17. timeLog('PHOTO')('图片错误')

手动实现柯里化

前面都是手动改写函数进行柯里化,我们也可以将这个过程封装成一个函数,实现自动柯里化其他函数。

  1. // 柯里化函数的实现hyCurrying
  2. function hyCurrying(fn) {
  3. function curried(...args) {
  4. // 判断当前已经接收的参数的个数, 可以参数本身需要接受的参数是否已经一致了
  5. // 1.当已经传入的参数 大于等于 需要的参数时, 就执行函数
  6. if (args.length >= fn.length) {
  7. // fn(...args)
  8. // fn.call(this, ...args)
  9. return fn.apply(this, args)
  10. } else {
  11. // 没有达到个数时, 需要返回一个新的函数, 继续来接收的参数
  12. function curried2(...args2) {
  13. // 接收到参数后, 需要递归调用curried来检查函数的个数是否达到
  14. return curried.apply(this, args.concat(args2))
  15. }
  16. return curried2
  17. }
  18. }
  19. return curried
  20. }
  21. var curryAdd = hyCurrying(add1)
  22. console.log(curryAdd(10, 20, 30))
  23. console.log(curryAdd(10, 20)(30))
  24. console.log(curryAdd(10)(20)(30))

柯里化的一个场景:vue3 源码的渲染器函数执行了柯里化。所以我们可以定制渲染器了。

组合函数

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

比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复;

那么可以将这两个函数组合起来,用函数封装一下,返回一个新的函数,新函数内部自动依次调用零散的函数。这个过程就是对函数的组合,我们称之为 组合函数(Compose Function);

  1. function add(n) {
  2. return n + 1
  3. }
  4. function square(n) {
  5. return n ** 2
  6. }
  7. //普通依次调用
  8. let result = square(add(3))
  9. console.log(result) // 16
  10. // 如果想要计算 n = 4 就又要手动按顺序执行一遍,比较麻烦
  11. // let result1 = square(add(4))
  12. // 组合函数
  13. function composeFn(fn1, fn2) {
  14. return function (count) {
  15. return fn1(fn2(count)) // 函数执行顺序可以自己设计
  16. }
  17. }
  18. let newFoo = composeFn(square, add)
  19. console.log(newFoo(4)) // 25

手动实现更通用的组合函数

  1. function hyCompose(...fns) {
  2. var length = fns.length
  3. // 边界值:非函数参数处理
  4. for (var i = 0; i < length; i++) {
  5. if (typeof fns[i] !== 'function') {
  6. throw new TypeError("Expected arguments are functions")
  7. }
  8. }
  9. // 依次执行参数函数
  10. function compose(...args) {
  11. var index = 0
  12. var result = length ? fns[index].apply(this, args): args
  13. while(++index < length) {
  14. result = fns[index].call(this, result)
  15. }
  16. return result
  17. }
  18. return compose
  19. }
  20. function double(m) {
  21. return m * 2
  22. }
  23. function square(n) {
  24. return n ** 2
  25. }
  26. var newFn = hyCompose(double, square)
  27. console.log(newFn(10))