Javascript 的闭包特性是否来源于某种设计失误?或是某种设计领先?

2021-02-26 00:34:10 +08:00
 LeeReamond

如题,最近听前端同事讨论 js 的闭包有感,这个帖子想讨论一下语言的命名空间设计,并非单纯局限在闭包问题。标题里写闭包是因为感觉这样比较容易被大家理解。

以下正文

=============

我个人学习 js 的过程中没怎么在意过闭包这个特性,我感觉命名空间逐级继承是一个再自然不过的设计,其他语言中不都是如此设计的?但是旁听别人讨论后发现 js 的命名空间确实跟其他语言不太一样,从设计原因上,似乎 js 的作者希望设计出一种特性能保存函数执行过程中的栈状态,不过我个人感觉上闭包这种使用方法又没有其他语言中生成器之类的特性描述能力强。

从使用角度来说,js 中命名空间的一个特性是子空间可以修改父空间内容,比如

var data = 0
var func = function(){
    data = 1
}
func()
console.log(data)

在这段代码中很显然 data 属于“全局变量”而被单个函数的调用过程所修改了。以前在写 js 的时候没注意,感觉自然也没什么奇怪之处。但是仔细想想,平时写其他语言时并不是这么处理的。比如 C 语言中要达到类似效果通常是通过传入指针的方式

#include<stdio.h>

void main(){
    int data = 0;
    void func(int *p){
        *p = 1;
    }
    func(&data)
    printf()
}

类似这样,即函数显然可以访问同级或上级作用域中的变量,但通过一个函数直接修改全局变量的指向是不被允许的。

在 python 当中可能用一些变通的结构

data = [0]
def func(data):
	data[0] = 1

或者使用 global 将全局变量暴露在函数中

data = 0
def func(data):
	global
	data = 2

不过说实话这种描述方式感觉比较局限,虽然理论上使用起来与 js 相当,但我在实际程序中几乎没有使用过 global 或者 nonlocal 之类的特性。

java 当中又回到类似 js 的模式,变量值可以直接被修改了。

============

不禁让人打起一个大大的问号,即为什么这种设计并不统一,有的语言仅可以读取,有的语言可以直接修改。他们都是出于什么目的设计这种特性的?

4725 次点击
所在节点    问与答
54 条回复
Zhuzhuchenyan
2021-02-26 05:42:24 +08:00
即使 JAVA 中闭包想要用到类似题干中的 data 变量也是需要把 data 变量预先标记为 final 的吧。

这个更多感觉是语言设计上的取舍,对于较为基本数据类型,闭包中到底捕获的是值还是引用。JAVA 就强迫让你写出不会犯错的代码,但同时要达成类似的事情(妄图在闭包中修改捕获的变量的值)就需要用引用类型包装一下这个变量。
nlzy
2021-02-26 06:27:55 +08:00
> Javascript 的闭包特性是否来源于某种设计失误?或是某种设计领先?
这个特性来源于 Scheme,JavaScript 的设计很大程度上受到了 Scheme 的影响。这个特性是 JavaScript 这门狗屎语言里少数不多的“抄对了”的地方,这个特性再加上 JavaScript 对 first-class function 的支持,使得 JavaScript 有了不错的“函数式”编程能力,语言的表达力和灵活性都大大提高了。
但也不能说领先,因为它只是在照抄 Scheme,而 Scheme 是 1973 年的编程语言了。

> 其他语言中不都是如此设计的?
现代的支持 first-class function 的语言很多都是这样的。不支持 first-class function 的语言,比如 C,就不会这么设计。
nlzy
2021-02-26 06:47:48 +08:00
@nlzy #22 修正:C 也支持函数指针,也能算 first-class function,只是这玩意比起别的语言里的函数,它实在是太难用了。
Cbdy
2021-02-26 06:58:03 +08:00
Java 也有闭包,很正常
lcwylxx921
2021-02-26 08:42:34 +08:00
正如上面所讨论的,JS 的闭包来源于函数式语言,在函数式语言中,函数作为 first-class 成员,需要具备一种能力使其可以方便地访问其定义范围之外的数据,并且这种访问在好的语言设计中应该是遵循 lexical scope 的。此外,关于你纠结的是否允许修改闭包数据的问题,这个首先在许多纯函数式语言(比如 SML )中,所有数据都是 immutable 的,也就谈不上是否允许修改闭包了,但是,出于灵活性的考虑,部分语言放开了这个限制, 比如在 Scheme 中加入了 set !关键字可以做一些 assignment 以及一些 mutable 的数据结构,但都还是有一定限制的,比如对 top-level scope 的修改,这种限制是非常合理的,可以避免在程序的各个地方做顶层的 assignment 然后对其他地方产生副作用。至于 JS 为什么毫无限制,只能说 JS 太灵活了吧。
Mutoo
2021-02-26 08:44:20 +08:00
现代语言大多都使用静态作用域:即函数对引用的变量有一个独立的静态表。而如何构建这个表决定了函数能访问的变量的范围。

闭包只是对「函数引用了不由它所定义的自由变量」形成的一种联系的学术命名。

ref:
代码之髓: 编程语言核心概念
7.2 作用域
11.5 闭包
lcwylxx921
2021-02-26 08:52:21 +08:00
@lcwylxx921 另外, 你应该是对 namespace 有什么误解,一般来说 namespace 是用来做模块化的划分的,在闭包这个语境下,更合适的词是 scope 。
EKkoGG
2021-02-26 08:54:25 +08:00
@wzb0909 经典谜语人
cheng6563
2021-02-26 08:58:01 +08:00
c 语言里,}之后局部变量就没了,如果还有指向局部变量的指针就会变成野指针。
EPr2hh6LADQWqRVH
2021-02-26 08:59:33 +08:00
你的问题不是为什么这么设计,而是为什么设计不统一?

时代不一样,优化方向不一样,理论积累不一样。

外部条件不停在变,程序语言使用者偏好多种多样,根本不存在一种普世的不变的『好的』设计,这是世界观问题。

具体到这个问题,是动态语言思潮出现后函数可以现场生成并在程序内部传来传去,这需要原本的栈内存有时需要被迁移到堆里,这才是闭包的核心,而栈上部的程序访问栈底部的内存,原本就是天经地义的,和闭包无关。
renmu123
2021-02-26 09:09:33 +08:00
js 中的变量提升和函数提升才更奇怪,变量提升好像是由于当时虚拟机的 bug,函数提升是特意设计。所以 es6 就用了 let 关键字来限定命名空间,所以少用 var,从现在开始。
lcwylxx921
2021-02-26 09:14:02 +08:00
@avastms 不太认同你的观点喔,首先“函数可以现场生成并在程序内部传来传去”这个与动态语言的思潮无关,这个是 first-class function 的特性,与语言的静态或者动态无关。其次,你说的内存问题是闭包这个机制实现上的细节,而闭包是一个语言设计上的问题,设计上的问题与实现无关。
EPr2hh6LADQWqRVH
2021-02-26 09:43:31 +08:00
@lcwylxx921 我认为是先演化出这种实现机制,之后再被总结成专有名词叫做闭包,再被后人借鉴引用成为一种设计,而不是从一开始就想好的,秀才造反十年不成
EPr2hh6LADQWqRVH
2021-02-26 09:58:29 +08:00
闭包和作用域都是小点,是服务于更大目标的,更多是一种必经之路而不是匠心独具,而且都是演化出来的,最后被起了专门的名字而已
misaka19000
2021-02-26 10:03:21 +08:00
这种问题不存在优劣,只存在好恶

就好像有人喜欢强类型有人喜欢弱类型
hazardous
2021-02-26 10:07:24 +08:00
@liprais 错了,发明 javascript 只用了 10 天。21 天的是《 21 天精通***》系列
Anshi
2021-02-26 10:20:56 +08:00
虽然但是,LZ 讨论的是闭包吗,我怎么觉得只是词法作用域链的设计?闭包不是在函数外部使用了函数内部的变量等....(?)
ylrshui
2021-02-26 10:30:18 +08:00
@LeeReamond
都只是内存魔法而已,只要你知道那个数据在哪,都能修改。只是有些语言告诉你数据在哪,并给你设计了方法,有些语言不告诉你,也不给你方法。C 语言给了你一个通用方法,可以修改任何地方的数据,但它不告诉你数据在哪。
LeeReamond
2021-02-26 10:37:57 +08:00
@misaka19000 题外话,我觉得喜欢动态类型的人不少,但应该不会有人喜欢弱类型
DOLLOR
2021-02-26 10:42:23 +08:00
JS 一般没有“命名空间”( namespace )这种说法,在你这语境下应该叫“作用域”( scope )。
在 JS 引入块级作用域之前,闭包通常是用两个嵌套函数来实现的。而引入块级作用域之后,利用{}就能实现闭包:

let getA;
{
let a = 10;
getA = () => console.log(a);
}
getA();// 10

所以不要觉得闭包是什么高深莫测的东西,它的其实就是“内部作用域访问了外部作用域的变量”这么简单的事。
作用域可以层层嵌套,并且内部作用域可以访问外部作用域的变量,这在许多编程语言看来,不是很天经地义的事情吗?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/756350

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX