【玩转 JS 函数式编程_007】第三章 一切从函数开始:核心概念剖析 + 3.1.1 λ 表达式与函数

写在前面
上一章,我们通过一个实际问题,相当于让 JS 函数式编程来了一段“才艺展示”。要想让它真正成为瑞士军刀级的好工具,还得扎扎实实锤炼 FP 基本功——这就是第三章的主要任务——带您重新认识 JS 中的函数相关概念,为后续高级话题的引入做好铺垫。

第三章 从函数开始——核心概念剖析


上一章我们用了一个案例来介绍函数式编程思想,本章来看看函数式编程的基础知识,并回顾一下函数的相关知识点。第一章提过,JavaScript 的两个重要特性:

  1. 函数作一等对象;
  2. 函数作闭包。

本章涉及要点:

  • JavaScript 中的函数,包括如何定义(尤其是箭头函数);
  • 函数科里化,以及作一等对象的函数;
  • 以函数式编程的方式使用函数的几种方法。

学完这些内容后,您将了解与函数相关的通用及特定概念。它们是函数式编程的核心。

3.1. 函数面面观 All about functions

首先简要回顾一下 JavaScript 中函数的相关知识,厘清函数与函数式编程之间的联系。在第一章的重点论述、以及第二章的多处介绍中,我们曾提到函数可以作为一等对象,进一步考察了它们在实际编码中的使用。本节重点关注以下三个方面:

  • 关于 λ 算子的一些重要的基础概念——它们是函数式编程的理论基础;
  • 箭头函数——是 Lambda 算子在 JavaScript 语言的最直接的诠释;
  • 视函数为一等对象——函数式编程中的一个关键概念。

3.1.1. Lambda 表达式与函数 Of lambdas and functions

一个函数按 Lambda 演算的术语要求,可以表示为:λx.2 * x

其中字母 λ 后的变量相当于函数的参数,句点后的表达式,是可以替换作为参数传递的任意值的地方。稍后您将看到本例按 JavaScript 箭头函数的语法,可以写作:x => 2 * x,形式上十分类似。

提示:arguments 与 parameter

分清参数 arguments 与参数 parameter 之间的区别,可以借助一些押韵的顺口溜来强化记忆:Parameters are Potential, Arguments are Actual(参数是潜在的,参数是实际的)。parameter 是要传递的潜在值的占位符,而 arguments 是实际传给函数的值。换句话说,定义函数时,您列出的参数是 parameter;而当调用它时,需要提供 arguments

应用一个函数,是指如同平时写代码那样,使用括号向其提供实际的参数(arguments)。例如,(λx.2 * x ) (3) 的值为 6。这些 Lambda 函数在 JavaScript 下的等效形式是什么样的呢?这是个值得探讨的问题,因为定义函数的方法有好几种,并且它们在含义上并不完全相同。

拓展阅读

有篇不错的文章介绍了函数及方法定义的多种不同方式,不妨了解一下,具体详见 Leo BalterRick Waldron 的文章:The Many Faces of Functions in JavaScriptJavaScript 中函数的多种表现形式)。

您能用多少种方式定义一个 JavaScript 函数呢?答案可能比您原以为的要多。至少可以写出以下几种:

  1. 具名函数声明:function first(...) {...};
  2. 匿名函数表达式:var second = function(...) {...};
  3. 具名函数表达式:var third = function someName(...) {...};
  4. IIFE 立即引用表达式:var fourth = (function() { ...; return function(...) {...}; })();
  5. 构造函数:var fifth = new Function(...);
  6. 箭头函数:var sixth = (...) => {...};

此外,你还也可以添加对象方法声明,因为它们也隐含了函数的成分。不过上述清单已经够用了。

那么,这些函数定义方法的区别在哪儿?为什么值得关注呢?让我们一个一个来考察:

提示

JavaScript 还可以定义生成器函数(generator function),形如 function*(...) {...},返回一个生成器对象 Generator;以及定义异步函数(async function),其实质为 generatorpromise 的结合。本节不使用这三种函数,更多详情参考 MDN 这两篇文档:

  1. 第一种定义,以关键字 function 开头的独立声明形式,应该是 JavaScript 中最常见的定义方式。这里定义了一个名为 first 的函数(即 first.name=="first")由于变量提升效应,该函数将在其定义的作用域内可随处任意访问。关于变量提升效应,详见 MDN 文档,只需记住一点:该效应只作用于变量声明,而非变量初始化。

  2. 第二种定义,将函数赋给一个变量,也能得到一个函数,只不过是个匿名函数(即没有命名)。然而不少 JavaScript 引擎是可以推断出函数名称的,例如设定本例中的 second.name === "second"。(观察下面的示例代码,函数名称没有赋给匿名函数)鉴于变量提升效应不提升变量赋值,该函数只能在赋值之后的代码位置方可访问;再者,您可能更倾向于使用 const 而非 var 来定义该变量,因为不太会(也不应该)改变该函数:

    var second = function() {};
    console.log(second.name);
    // "second"
    
    var myArray = new Array(3);
    myArray[1] = function() {};
    console.log(myArray[1].name);
    // ""
    
  3. 第三种定义,与第二种相同,只是此时的函数拥有其自己的名称:third.name === "someName"

提示

函数的名称与您调用的时机有关,在执行递归调用时更是如此。第九章《函数设计——递归》中还会详细论述这一点。如果只想要一个回调函数,您可以使用一个没有名称的函数;但要注意,在错误回溯中,当您试图了解代码报错时使用的列表类型,以及哪个函数调用了什么的时候,命名函数更容易被识别。

  1. 第四种定义的 IIFE 立即引用表达式使用了闭包的知识。内部函数可以以完全私有、被封装的方式,访问在其外部函数中定义的变量或其他函数。回顾第一章闭包一节定义的计次函数,可以编写如下代码:

    var myCounter = (function(initialValue = 0) {
        let count = initialValue;
        return function() {
            count++;
            return count;
        };
    })(77);
    
    myCounter(); // 78
    myCounter(); // 79
    myCounter(); // 80
    

    仔细钻研这段代码,外部函数接收一个参数 77 作为 count 的初值(默认为 0)。由于闭包的缘故,内部函数可以访问到 count。从各方面看,返回的函数都是一个通用函数——唯一的区别在于它访问了私有元素。 这也是模块型设计模式(module pattern)的基础。

  2. 第五类定义由于不安全,还是不用的好。这种方式首先传入参数列表,然后是一个字符串形式的函数体,用到了与调用 eval() 相同的方式创建函数。这可能招致很多危险的黑客攻击,所以不要用这类定义。为满足读者的好奇心,这里给出一个根据第一章展开运算符小节重写后的 sum3 函数示例:

    var sum3 = new Function("x", "y", "z", "var t = x + y + z; return t;");
    sum3(4, 6, 7); // 17
    

    拓展

    这类定义不仅不安全,还有其他坑——不会在其创建函数的上下文中创建闭包,因此这些私有变量都是全局的。详见 MDN 文档。切记,用这种方式创建函数是下下策。

    续:MDN 原文为:使用 Function 构造函数创建的函数不会为其创建上下文(creation context)创建闭包; 它们总是在全局范围内创建。 运行它们时,它们将只能访问自己的局部变量和全局变量,而不能访问创建 Function 构造函数的作用域内的变量。 这不同于将 eval() 与函数表达式的代码一起使用。

    以下是文档提供的示例代码:

    var x = 10;
    
    function createFunction1() {
        var x = 20;
        return new Function('return x;'); // this |x| refers global |x|
    }
    
    function createFunction2() {
        var x = 20;
        function f() {
            return x; // this |x| refers local |x| above
        }
        return f;
    }
    
    var f1 = createFunction1();
    console.log(f1());          // 10
    var f2 = createFunction2();
    console.log(f2());          // 20
    
  3. 最后一种定义,使用带 => 符号的箭头函数定义,是代码最为紧凑的一种定义方式,也是我们将尽可能尝试使用的方式。

至此,我们已经考察了定义函数的若干种方式,接下来重点关注箭头函数,这也是全书力荐的一种代码风格。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

安冬的码畜日常

您的鼓励是我持续优质内容的动力

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

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

打赏作者

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

抵扣说明:

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

余额充值