[路飞]_企图讲透js函数式编程(下)

前置知识

下篇主要讲柯里化和组合,柯里化其实跟上篇讲的闭包藕断丝连,不可分割,闭包是实现柯里化的基础,不妨再详细说说闭包,不过这次我要很细很细的介绍一遍。但这里需要一写前置知识,容我简单介绍。

作用域

作用域指程序中定义变量的区域,它可以决定当前执行代码的变量的可访问权限。作用域就是代码中某些特定的变量、函数在特定的独立区域可以被访问到。 下面有一个小例子 可以加深对作用域的理解

function test(a,b){
    let innerVariable=1;
    return 0;
    }
    //下面这句肯定会出错,因为innerVariable作用域仅在函数内,外部无法访问
let outVariable=innerVariable;
复制代码

作用域的分类

在es5及之前,作用域可分为全局作用域和函数作用域,在es6及以后,作用域又多了块级作用域(let const重点体现了块级作用域)

这里感觉可以再强行加餐一下var 和let const的区别。

image.png 由图可见a,b存放在脚本区(块级作用域) c存放在全局区。

var可以上浮,也就是var定义的变量可以先使用后定义。const,let定义的变量其实也可以上浮,但是变量在被定义之前是无法访问的,所以会报错,如果细心的话可以发现,const,let变量先使用后定义报的错和只使用不定义是有区别的。

函数上下文

参考文章 (理解JavaScript的执行上下文 - 知乎 (zhihu.com))

闭包

综合上述知识,就更能深刻的剖析闭包。 简言之,能够访问其他内部函数变量的函数,成为闭包。

 function fun1() {
            var a = 1;
            return  function fun2 () {
            console.log(a);
            };
        }
        var fn = fun1();
        fn();//形成闭包,从外部获取内部作用域的信息。

复制代码

(1)编译阶段,变量和函数被声明,作用域就确定下来。

(2)运行函数fun1(),创建fun1()函数的执行上下文,内部存储fun1()中所有的变量函数的信息。

(3)函数fun1()执行完之后,把fun2的引用赋值给外部变量fn,要明确的是此时fn的指针指向的是fun2,此时fn位于全局作用域,fun2位于函数作用域,所以可以看见fn位于fun1() 作用域之外,但是访问到了fun1()的内部变量。

(4)fn在全局被执行,内部代码console.log(a)向作用域请求获取a变量,在本级的作用域没有找到,就向上父级作用域一层一层找父亲(找爸爸)。在fun1()找到了a变量,返回给console.log所以打印出来了1。

一些基于闭包的杂想

c++面向对象中class中成员分为public,private,protected,抛开继承来讲,private,protected基本一样,这里我把他们都视作private,private存放的成员是不想外界访问的东西,但js并没有private关键字。可能对于初学者来讲public,private有个啥用,通通public岂不是更方便,注意一点用就不会泄露隐私成员了,但我觉得一个成熟的编程语言,不仅要可以完成功能逻辑,还要从语言层次强行避免一些不该发生的事情出现,比如类中不想让外界看到的成员,不应该在功能逻辑这一阶段上避免它出现,而是要在写代码的时候,就把问题暴露出来。

js利用闭包这一概念,就能模拟出private的效果。

        function getImformation() {
            var name = "gaby";
            var age = 20;
            return function () {
                return {
                    getName: function () {
                        return name;
                    },
                    getAge: function () {
                        return age;
                    }
                };
            };
        }
        var obj = getImformation()();
        obj.getName();
        obj.getAge();
        obj.age;

复制代码

闭包也会带来一些问题,会带来一些问题垃圾回收的问题。(关于垃圾回收,会在之后的js性能优化这一章节再详细说说)主要是变量的引用带来的,如果了解c++ shared_ptr的话不难理解,问题十分相似,不了解也不用紧。

JavaScript内部有垃圾回收机制,用计数的方法 。当内存中的一个变量被引用一次,计数+1,垃圾回收机制会在固定的时间间隔内询问这些变量,将计数为0的变量标记为失效变量从而清除释放内存。

再来看第一个闭包代码,fun1()函数隔绝了外部的影响,所有变量在函数内部完成,fun1()执行后,理论上内部的变量就会被销毁,内存被回收。但是我们写了一个闭包,这就导致了全局作用域始终存在一个a变量,一直占用内存,造成内存泄漏。

柯里化

终于步入正题柯里化,其实对闭包深刻剖析后,柯里化的理解就是一个迎刃而解的事情。柯里化就是将函数多个参数压缩为一个,是对函数参数的降维,(突然想到高中数学老师老是说遇到高次函数先降次哈哈哈)这玩意不就是把正常的多参数函数一个一个,逐层逐层的当闭包写吗(可能形容不太恰当,看案例).

//正常多参数函数
function add(x,y,z){
    return x+y+z;
}
//柯里化后
function add() {
    return function(x) {
        return function(y) {
            return function(z) {
                return x + y + z;
            }
        }
    }
}
let f = add();
//测试用例
console.log(f(1)(2)(3));   

复制代码

有点洋葱一层一层剥下你的心那味了。这样将函数柯里化未免呆板,其实可以将柯里化单独写个方法,可以返回一个正常函数柯里化后的函数,lodash库中也提供了这个方法,方法名叫curry。 下面用lodash库中的curry给大家写几个柯里化案例(调库真方便)

//柯里化实现字符串匹配数字
const { method } = require('lodash');
const lodash = require('lodash');
let match = lodash.curry((arg, str) => str.match(arg));
//let findSpace = match(/\s+/g);
let findNumber = match(/\d+/g);
let filter = lodash.curry((method, str) => (lodash.filter(str, method)))
let filterNumber = filter(findNumber);
let str = "guaqiu52"
//看见了吧!是一个参数
console.log(filterNumber(str));

复制代码

柯里化方法通用实现如下

const curry=function(fn){
  return function curryFn(...args){
    if(args.length<fn.length){
      return function(){
        return curryFn(...args,...arguments)
      }
    }else{
      return fn(...args);
    }
  }
}
复制代码

vue.js源码使用柯里化的地方:src/platform/web/patch.js 日后我也会在vue源码讲解部分给大家说说。

组合

纯函数之间是可以合并成为一个更强大的函数,直接上案例!


function f1() {
    console.log(1);
}

function f2() {
    console.log(2);
}
//组合实际上是对多个纯函数执行的包装。
function compose(f1, f2) {
    f1();
    f2();
}
compose(f1, f2);
复制代码

ladash库中也提供了组合的方法:

flow 从左到右执行

flowRight 从右到左执行(更常用一些)

这里要提一下,如果使用flowRight 参与组合每个子纯函数可以有多个参数吗? 答案是只有最右边的函数可以有多个参数。 为什么?flowRight是从右到左执行函数,并且将前一个函数的执行结果作为参数给当前函数,大家可以将这一过程抽象为一个管道(即我的输出是你的输入),所以我们无法给他传参,(就像正常人也不能将自来水管的自来水变成可乐一样),最右边的函数(即第一个执行的子纯函数)他是没有函数给他传值,需要我们给它传值,那就可以随心所欲给他传多个参数了。(就像你如果负责给自来水管供水,如果有一天你偷偷把水加点可乐,那我们的水就变成可乐味了)

ok!丢个案例我就溜了,函数式编程我也是刚接触,需要大牛们勘误指导,我讲的我觉得也是浅浅的那一层,函子什么的也没介绍,或许等我彻底通透,心中自有丘壑的那一天才能真正讲透函数式编程。

const { method } = require('lodash');
const lodash = require('lodash');
const reverse = arr => arr.reverse();
const first = arr => arr[0]
const toUpper = s => s.toUpperCase();
const LastToUpper = lodash.flowRight(toUpper, first, reverse);
console.log(LastToUpper(['z', 'j', 'l']));
复制代码

猜你喜欢

转载自juejin.im/post/7031387498643357703