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 的模式,变量值可以直接被修改了。

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

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

5000 次点击
所在节点    问与答
54 条回复
Mohanson
2021-02-26 10:53:12 +08:00
@LeeReamond 科普下 c 是弱类型
LeeReamond
2021-02-26 12:14:44 +08:00
@Mohanson 我觉得不要乱科普,弱检查跟弱类型是两个概念。char 能和 int 相加有轻易明确的底层逻辑,与 js 的 1+'1'不是一个概念
mxT52CRuqR6o5
2021-02-26 12:31:28 +08:00
nested function 和闭包还是差很多吧
yzqtdu
2021-02-26 13:06:17 +08:00
题外话,不知楼主有没有看过 Programming Language Pragmatics,书里还有更多不同语言奇奇怪怪的特性
Leviathann
2021-02-26 13:10:25 +08:00
我觉得闭包这个说法在不同的语言里表示的意义有点不一样啊
有的提法是能捕获外部变量的就是闭包,而不捕获外部变量的就是函数
而 js 好像强调的是函数返回以后可以使用返回对象中的函数操作获取闭包内部的变量
youxiachai
2021-02-26 16:49:06 +08:00
lz 是没接触过函数式概念吗.... 这个是语言设计范畴的东西啊....
要搞懂这个..还是看看一个编程语言是怎么设计的..
xumng123
2021-02-26 18:09:46 +08:00
@Mithril 讲的好,数据绑定操作,还是操作绑定数据,太精辟了
libook
2021-02-26 18:57:54 +08:00
每一种编程语言都是在特定的环境下,为了满足特定的需求而设计的;因为环境和需求不是统一的,那么自然不同编程语言的设计也并不必须是统一的。

那么要想了解一个编程语言的特性为什么现在是这个样子的,就得了解这个编程语言的发展历史,什么时期在什么环境下遇到了什么需求导致设计了这个特性。

JS 是从一个极其简陋的轻脚本语言,逐步发展为现在这样能胜任大多数领域的语言的。最初数值型就只提供了一种 Number 类型,而不是像其他语言一样会划分 Int 、Long 、Float 、Double 等甚至还区分 signed 和 unsigned,就是因为最初的需求并不需要如此细致地区分数值类型,一个 double 足矣被用来覆盖整数、小数、负数的常见计算需求了(虽然小数有精度损失问题,但总归是遵循 IEEE 的标准和其他主流语言保持一致的)。
在作用域上也一样,怎么简单怎么来,最简单的方案莫过于按照代码块的嵌套关系来界定作用域,最终的效果就是 JS 子块内的代码可以直接调用父块的变量。
对于赋值和返回机制,搞一些深 copy 、浅 copy 、指针什么的也会让语言比较复杂,简单一些就设计成了基本类型深 copy 、高级类型引用。
后来一些开发在使用 JS 的时候有了限制变量的可访问性和操作的需求,奈何 JS 本身设计得很简单,没有专门提供如此高级的功能,但在掌握了前面说的那些特性后发现可以通过一些特别的写法来达到目的,这个写法在其他一些语言中也有类似的用法,于是乎有人给这种用法起了个名字统称为“闭包”。

类似的还有,JS 不专门提供继承派生相关特性,但是为了节省存储空间和创建对象的开销所以设计了原型机制,而用原型链恰好可以模拟出来继承派生的机制,这个肯定也不是 JS 设计者的初衷,而是开发者们的智慧。
Sparetire
2021-02-27 00:43:02 +08:00
@LeeReamond 按照一般的定义,偏向允许隐式类型转换来说,C 的确是弱类型。。也建议不要乱反驳
参考 https://www.zhihu.com/question/19918532/answer/21647195
ipwx
2021-02-27 00:56:44 +08:00
只有我想吐槽楼主举的这种方法有点 anti-pattern 嘛。。。一不小心就会写错的亚子
LeeReamond
2021-02-27 01:16:46 +08:00
@ipwx 不熟悉设计模式,请教我如何写比较符合模式,谢谢。
supermao
2021-02-27 09:47:03 +08:00
肯定不是
其它语言也有闭包
ipwx
2021-02-27 17:16:14 +08:00
@LeeReamond 倒不是设计模式的问题。你想,如果你是一个大函数,外面和里面都很长的那种。甚至你是在循环体里面定义了一个闭包,然后把闭包存下来扔进一个列表之类的。

想象一下,你循环体外面的循环变量是 i,循环体内部也写了个循环,也是 i 。然后一不小心你用的都是:

i = 0
while i < 100:
...i = i + 1

然后就炸了不是?而且这种 bug 贼难查,毕竟是思维盲区。闭包内部默认不能改写外部的变量才能符合人的一般思考模式(局部思考)。
- - - -

js 的这个作用域覆盖问题由来已久了。不仅是闭包问题,你代码写长一点,if while for 作用块内部的变量不小心改写了外部变量也是灾难的。所以才有了 const 和 let 这两个关键字替代 var,避免这种作用域改写。

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/let
zhigewuwang
2021-08-30 14:57:00 +08:00
设计是一环影响一环的。
函数内部可以声明函数后,在内部函数访问外层空间的变量是很自然的。 函数作为第一类型,也是有生命周期的,然后函数可以被 return,就会出现函数的生命比引用的局部变量生命更长的情况,那自然那些局部变量也要一起存活。

python 使用变量不需要提前声明, 所以没办法区分 创建新变量,还是引用外部变量这两种情况。 所以是只能这么设计。

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

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

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

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

© 2021 V2EX