JavaScript 函数式编程 -- 划重点了!!!

橘子小睿

函数是一等公民

在谈到函数式编程的时候,很多时候会听到这样一句话 "函数是一等公民"。那我们如何去理解这句话呢?

"一等" 这个术语通常用来描述值。所以当我们说 "函数是一等公民" 时,也就是说函数拥有值的一切特性,你可以像看待一个值一样来看待一个函数。举个例子,数字在 JavaScript 中是一等公民,那么数字拥有的特性,也同样被函数所拥有。

  • 函数可以像数字一样被存储为一个变量
const num = 10;
const fun = function() {
    return 10;
}
  • 函数可以像数字一样作为数组的一个元素
const a = [10, function() { return 20; } ]
  • 函数可以像数字一样存在于对象的插槽里
const b = {
    name: 'Tony',
    age: function() { 
        return 20; 
    }
}
  • 函数可以像数字一样在使用时直接创建出来
10 + (function() { return 20; })(); // 30
  • 函数可以像数字一样被另一个函数返回
const well = function() {
    return 10;
}

const good = function() {
    return function() {
        return 10;
    };
}
  • 函数可以像数字一样被传递给另一个函数

const fun = function(value) { 
    return value; 
}

const happy = function(func) {
    return func(5) * 10;
}

happy(fun); // 50

最后两条其实就是 高阶函数 的定义,如果你现在不理解也没有关系,我们在后面的部分会讲到它。

变量作用域和闭包

变量作用域

变量的作用域和闭包作为 JavaScript 的基础,在学习函数式编程中是非常重要的,只有理解了它们,你才能更好的去理解我们后面要讲到的高阶函数和部分应用等。

关于变量的作用域,你需要知道:

  • 全局作用域: JavaScript 中拥有最长生命周期 (一个变量的多长的时间内保持一定的值) 的变量,其变量的生命周期将跨越整个程序。
globalVariable = 'This is a global variable!';
  • 词法作用域: 词法作用域其实就是指一个变量的可见性,以及它文本表述的模拟值。
a = 'outter a';

function good() {
    a = 'middle a';
    return function() {
        a = 'inner a';
        return 'I am ' + a;
    }
}

good(); // I am inner a
PS:这里的示例代码仅仅是为了学习,你最好不要这样去写,因为它会让你的代码变得令人费解。

在上面的例子中,我们分别对 a 变量进行了三次赋值,那么为什么最后我们拿到 a 的值是 'inner a' 而非其他呢?

当我们声明 a = 'outter a' 时,程序会在栈中开辟一个空间去存储 a,当执行 good()函数时,我们声明了 a = 'middle a',这时候会将栈中 a 的值修改掉,变成 'middle a',最后在执行 return 语句时,我们又声明了 a = 'inner a',这时候会再次修改栈中的 a 的值,变成 'inner a'。因此得到了上面的结果。

在多次给同一变量赋值时,最后得到的值是离使用时最近的一次赋值。通过查找离使用时最近的一次赋值,我们可以快速的得出最后的结果。

  • 动态作用域

提到 JavaScript 的动态作用域,就不得不提到 this 了。this 相关的知识很多,之后有时间再详细来讲讲。现在我们先记住 this 所指向的值由调用者确定,如下代码所示:

function globalThis() { return this; }

globalThis.call('APPLE'); //=> 'APPLE'
globalThis.call('ORANGE'); //=> 'ORANGE'
  • 函数作用域

闭包

说起闭包,很多人都会觉得有点头疼,这的确是一个令人费解的概念,不过不要怕,它其实没有那么难以理解。

闭包的定义

闭包是一个函数和声明该函数的词法环境的组合

换句话说,闭包就是在使用时被作用域封闭的变量和函数。闭包可以捕获函数的参数和变量。

举个例子:

const fun = function() {
    const a = 10;
    return function(b) {
         return a + b;
    }
}
const myFunc = fun(); // 此时 myFunc 就变成一个闭包了,这个闭包可以捕获 fun 函数里的 a 变量,b 参数。

注意闭包是在使用时才会生成的,而非创建时。如上面的例子,如果只创建 fun 函数,而不执行最后一句 fun(),那么 fun 并不能称之为一个闭包。这里的闭包应该是 fun 运行时所产生的作用域,这个作用域捕获了fun 里面的变量和参数。

闭包的特点

  • 闭包会捕获一个值(或引用),并多次返回相同的值
  • 每一个新的闭包都会捕获不一样的值

再来看一个例子:

const fun = function() {
    return function() {
        return 10;
    }
}

const myFunc = fun(); // myFunc 不是一个闭包

const fun2 = function(value) {
    return function() {
        return value;
    }
}
const myFunc2 = fun2('AWESOME'); // myFunc2 是一个闭包
myFunc2(); // AWESOME
myFunc2(); // AWESOME 多次执行 myFunc2 闭包,返回的值相同

const myFunc3 = fun2('HAPPY'); // myFunc3 是一个新的闭包
myFunc3(); // HAPPY

这里 myFunc 严格意义上并不能叫作一个闭包,因为它并没有捕获 fun 任何的变量或者是函数的传参。而 myFunc2 是一个闭包,因为它捕获了 fun2 的传参。

闭包的销毁

闭包延续了变量的生命周期,如果不手动销毁,闭包里面的变量会一直存在内存中。比如当我们手动将 myFunc = null 时,闭包里面的变量才会被垃圾回收。

实用的闭包

说了这么多,你可能会有这样的疑问,闭包真的有用吗?闭包一般都会用到什么地方?

*1. 用闭包模拟私有方法, 使公共函数能够访问私有函数和变量,实现数据的隐藏和封装。私有方法有利于限制对代码的访问,并且提供了强大的管理命名空间的能力,避免了非核心代码对公共接口的干扰。

const Counter = () => {
    let count = 0;
    
    const change = (a) => {
        count = count + a;
    }
    
    return {
        increase: () => {
            change(1);
        },
        decrease: () => {
            change(- 1);
        },
        value: () => {
            return count;
        }
    }
}

const func1 = new Counter();
func1.value();  // 0

func1.increase(); 
func1.value(); // 1

func1.decrease();
func1.value(); // 0

*2. 通过一个高阶函数,生成不同的闭包,从而得到多个保存不同环境的新函数。

如下面的例子:

const makeAdder = function(x) {
    return function(y) {
        return x + y;
    }
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

makeAdder 其实是一个函数工厂,用于创建将制定的值和它的参数求和的函数。通过它我们又创建了两个新函数 add5 和 add10。add5 和 add10 都是闭包。它们共享着相同的函数定义,但是却保存了不同的环境。在 add5 的环境中,x 为 5,但是在 add10 中,x 则为10。

高阶函数

定义

满足以下任意条件之一即可称之为高阶函数:

  • 以一个或者多个函数作为参数
  • 以一个函数作为返回结果

我们常见的 map,find,reduce 都是以函数作为入参的函数,所以它们都是高阶函数。

以函数作为参数的函数

使用函数作为函数的参数,可以让我们创建出更灵活的函数。通过将参数从值替换为函数,我们可以得到更多的可能性。因为在调用的时候,我们可以通过传入不同的函数来完成不同的需求。

正如下面的例子:

const finder = function(val, func) {
    return val.reduce(function(prev, current) {
        return func(prev, current);
    });
}

const a = [1, 2, 3, 5, 8];
finder(a, Math.max); // 8
finder(a, Math.min); // 1

在使用 finder 函数时,通过传入不同的函数,最后得到了完全不同的结果。这也是为什么我们强调 "使用函数,而不是值" 的原因。

以函数作为返回结果的函数

以函数作为返回结果的函数,可以构建强大的函数。还记得我们前面提到的闭包吗? 通过高阶函数 makeAdder,我们生成了 add5 和 add10 两个新的函数。能够生成闭包的函数,其实都是高阶函数。

到这里,第一部分重点内容就讲完了。在下一部分中,我们会讲到函数式编程中剩下的几个重要部分:

  • 柯里化和组合
  • 部分应用
  • 递归
  • 基于流的编程
阅读 2.4k

橘子小睿的前端杂记
专注前端开发

前端开发者

657 声望
73 粉丝
0 条评论

前端开发者

657 声望
73 粉丝
文章目录
宣传栏