《JavaScript函数式编程思想》——名称

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/starrow/article/details/86713079

第1章  名称

一般对函数式编程的介绍都会从一等值和纯函数等概念开始,本书却准备在那之前先花些篇章讨论两个通常未得到足够重视的主题:名称和类型系统。前者包括名称绑定、作用域和闭包等内容,后者包括类型的含义和划分、强类型和弱类型、静态类型和动态类型以及多态性的内容。理解这些概念对编程很有意义,无论使用的是哪种语言,采用的是什么范式。具体到本书的核心,使用JavaScript进行函数式编程,在对以上普适概念理解的基础上,掌握它们在JavaScript中的特定表现和行为,又有格外的重要性。这一方面是因为JavaScript长期以来被认为是一种简单的脚本语言,缺少在通用知识背景下对其特性和行为的分析,以致对其行为的认识往往是零碎的、实用的。另一方面是因为名称和类型系统与JavaScript的函数式编程有着紧密的关联。嵌套函数和闭包是JavaScript的函数式编程离不开的技术,鸭子类型是JavaScript藉以实现函数式编程通常具备的参数多态性特征的机制。这些内容都将在下面两章中得到充分的讨论。

1.1  名称绑定    
1.1.1  常量和变量    
1.2  作用域    
1.2.1  包块作用域与就近声明    
1.2.2  静态作用域和动态作用域    
1.2.3  前向引用和提升    

1.3  闭包

名称绑定和作用域这两个概念看上去有些平凡,远没有闭包(Closure)引起的兴趣和疑问多。没有函数式编程经验的人,在初次接触到JavaScript的闭包概念时,多会觉得这是一个很新奇的东西,一时无法理解它的效果,也体会不了有经验的程序员所说的它带来的好处和运用它的技巧。而实际上如果掌握了名称绑定和作用域,就会发现闭包的出现是水到渠成的。

在程序运行中的某一刻或代码中的某一处,所有当前有效的名称组成的集合被称为此刻或此处的引用环境(Referencing environment)。当不针对某个名称时,我们把代码中引用环境保持不变的区域也称为作用域。这个意义上的作用域与前面讨论的名称的作用域是息息相关的,假如用前者来解释后者,它就是代码中所有作用域相同的名称所在的区域。根据名称作用域的规则,在全局代码中的某一处,引用环境就是全部全局名称组成的集合。在一个全局函数内,引用环境包括所有的局部名称、参数和全局名称。JavaScript的函数与C的一个巨大区别就是前者可以嵌套,也就是说一个函数可以声明在另一个函数内。一个内嵌函数的引用环境包括它自身所有的局部名称和参数、外套函数的局部名称和参数以及所有的全局名称。我们在本书后面会看到,嵌套函数是JavaScript编程中必不可少的写法,许多模式和技巧都是赖之以成立的。

内嵌函数需要能访问外套函数的引用环境,当内嵌函数在它的作用域内被直接调用时,满足这个要求是很平凡的。但是JavaScript中的函数还可以作为参数和返回值,这时从内嵌函数的声明到调用它的代码,引用环境发生了改变,若还要访问原来的引用环境,就必须以某种方式将内嵌函数的引用环境和它捆绑在一起,这个整体就称为函数的闭包。很多有关JavaScript的文章在介绍闭包时,都把它定义为从某个函数返回的函数所记住的上下文信息。一个函数可能成为返回值,确实是建立闭包的有力理由。因为函数的局部名称都存在于调用堆栈中,若没有闭包,外套函数返回内嵌函数后,外套函数的堆栈帧被删除,返回的内嵌函数所能引用的外套函数中的局部名称也将消失。

function createClosure() {
    let i = 1;
    return function () {
        console.log(i);
    }
}

const fn = createClosure();
//若没有闭包,fn将无法引用createClosure的局部变量i。
fn();
//=> 1

但实际上闭包并不是只在函数被返回时才创建的,任何JavaScript的函数都是同它的闭包一同创建的。下面的代码不涉及返回函数,却显示了闭包的效果。  

function f(fn, x) {
    if (x < 1) {
        f(g, 1);
    } else {
        fn();
    }

    function g() {
        console.log(x);
    }
}

function h() {
}

//假如没有闭包,此处的结果将会是1。
f(h, 0);
//=> 0

当函数g最终被调用时,参数x的值为1,但是g输出的x为0。这就是因为函数g使用的是它闭包中的x,而它的闭包是在声明函数时创建的,在第一次调用函数f时获得值0。等到f调用自身后再次进入其代码时,g的引用环境已经与声明它时的不同,参数x虽然名称相同,但与g闭包中的x是身份不同的值。

以上行为也可以用另一对概念来解释。上一节指出,在代码中遇到某个名称时,静态作用域使用的是空间上最近的声明,动态作用域使用的是时间上最近的声明。略加推敲会发现,在没有调用函数的情况下,代码的文本顺序和程序的执行顺序是一致的,空间上最近的声明就是时间上最近的声明,两种作用域方式的效果是相同的。假如所有的函数都在调用处声明,或者说在调用前内联化(Inline),也会导致同样的结果。两种作用域差别的关键就在于,函数的引用环境建立的时间,静态作用域是在函数声明时建立的,称为深绑定(Deep binding)【注:有些文献中将深绑定定义为在函数被作为参数传递时绑定引用环境,JavaScript中的函数可以作为返回值被赋值给变量,也可以作为对象的方法被动态调用,所以函数被作为参数传递时才绑定引用环境是不能满足所有情况的需求的。】;动态作用域是在函数执行时建立的,称为浅绑定(Shallow binding)。回看上面的代码,假如JavaScript采用的是浅绑定,函数g使用的就将是它执行时包围它的函数f的参数x,输出的结果将会是1。

在一个函数中,引用环境包括它的局部名称、参数和外套作用域中的名称(可能存在的外套函数的局部名称和参数以及所有的全局名称)。容易看出,闭包只需记住外套作用域的部分,因为函数自身的局部名称在每次运行时都会重新创建。

总而言之,闭包对于静态作用域来说并不是什么新的概念,而是以函数为中心的视角来看待静态作用域,或者说是在函数可以被传递、返回的语言中为了贯彻静态作用域的理念而采取的一种实现层面的技术。如果不关心关于静态作用域如何实现的细节,完全可以忽略闭包的概念。因为仅仅就理念上理解和分析代码中名称的含义而言,掌握静态作用域的理论就足够了。本书后面有用到闭包术语的地方,也只是为了强调它反映出的视角和语境。

1.3.1  包块作用域与闭包

1.4  小结    

更多内容,请参看拙著:

《JavaScript函数式编程思想》(京东)

《JavaScript函数式编程思想》(当当)

《JavaScript函数式编程思想》(亚马逊)

《JavaScript函数式编程思想》(天猫)

展开阅读全文

没有更多推荐了,返回首页