作者回复: 非常赞。这个答案其实比我想的还要好。^^.
作者回复: 先把你的第一个问题回复了,你试着再推演一下其它的。——你的第一个问题问得非常到位,是你思考上下文中的关键点。赞!
“闭包是函数表达式的值?”答案即“是”,且“非”。
首先说“是”的部分。单看ECMAScript这里:
https://tc39.es/ecma262/#sec-function-definitions-runtime-semantics-evaluation
你会发现,一个函数表达式执行之后,就是“Return closure”。所以,这个说法在ECMAScript层面是对的。
但是我通常在这里说,这个过程得到的其实还只是一个“函数实例”。为什么呢,比如说:
```
for (var arr = [], i = 0; i<10; i++) arr.push(function() {});
```
这里有几个匿名函数在arr[]里面?答案是有10个。所以函数表达式每执行一次,就得到一个函数实例。你的示例`let c = function() {}`也一样,右操作数每执行一次就得到一个新的。
但是这是函数实例。函数闭包其实是在它调用的时候才创建和初始化的。——只不过,所有的、因为函数调用而产生的闭包,都是上面这个实例的一个“副本”。所以,一个实例其实也可以有多个闭包。这里的意思是说,ECMAScript在上面把它叫闭包:既没错,它是所有其它闭包的“母本”;也不太对,因为它不是你在执行过程看到的任何一个闭包。
——一个函数实例可以有多个闭包,这很少见,但的确是存在的。比如递归,其实是一个函数的多次调用,那这个函数(实例)只有一个,闭包却是有多个了。
作者回复: 他的设计便是如此。使用当前上下文中的this和arguments,有些时候是很有用的,对现有的normal function的设计也是补充。所以,除了开始使用的时候不习惯之外,在语言功能的设计上还是不错的。
另外,箭头函数没有this,也意味着它不支持this(和arguments)的隐式传入,这在函数式语言特性上是更纯粹的,更接近lamada函数的概念。不过由于JavaScript语言自己的特殊性,它做不到无副作用罢了。
作者回复: "函数可以访问函数作用域外标识符的能力",这个与闭包没关系,而是scope-chains的事情。不过现在这个概念也改了,在ES6之后,这里被称为环境/作用域。所以,如果仅仅是在概念层面讨论这个话题,比较容易变形。
回到提出这个概念的早期,在ES3~ES5的那个时代,概念比较简单清晰一些。有一个东西称为“对象闭包(object closure)”,这个东西写在spidermonkey的代码里面,也写在一个称为narcissus的开源项目里面,尤其是narcissus,它是JS之父本人Eich一直在维护的一个项目,也就是说,是一种正统的“概念”。——我说这一段是想说明,我下面的说法不是心血来潮随口胡诌。
Ok。什么是对象闭包呢?所谓对象闭包,就是“with (x) ...”中打开的那个用`x`的属性表来形成的闭包。所以,如果(注意这个推论哟),如果对象闭包是一种闭包,那么要么是概念上属性表等义于闭包,要么是闭包是一种属性表。
OVER。^^.
作者回复: 非常赞的思考!
表达式和函数并没有本质不同,它们的抽象形式是一样的。事实上,如果你理解“当运算符等义于某个函数”时,那么你其实就看到了“函数式语言”范式的根本逻辑,关于这一点,我之前也讲过,在《JavaScript语言精髓与编程实践》中也是拿这个来引入函数式语言的。
当运算符等义于函数,那么操作数就是入口参数,而运算结果就是函数返回。运算在本质上就是一个IO和一个处理。这样的理解会让你看到计算理论中最基础的那些模型。
谢谢你提出这一点,真的很赞!
作者回复: 赞的。^^.
```
// f是函数x的一个实例
f = function x() {}
// 创建f的一个闭包、初始化该闭包并在其中执行代码
f()
```
作者回复: 你得让函数先进入“非简单参数模式”,例如在参数中声明缺省参数。对于箭头函数来说也是如此。
```
const test = (x=0) => {
"use strict";
a=1;
}
```
> SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list
作者回复: Oh... 这是你误解了。
这里说的是“解除绑定关系”,而不是没有arguments。
如下:
```
// 非简单参数
function foo(x = 2) {
x = 100; // reset
console.log(x, arguments[0]);
}
// 简单参数
function foo2(x) {
x = 100; // reset
console.log(x, arguments[0]);
}
// 测试
foo();
foo2();
```
另外,箭头函数是没有arguments的。如果你能访问到,那么应该是该箭头函数所在的(父级的)函数的。
作者回复: 是的。
它是“引用(规范类型)”。它不是语言层面理解的“值与引用”,也不是JavaScript的typeof可以识别的类型,它是ECMAScript在规范层面设计的“引用(规范类型)”。这是在第1讲里一再强调的。
"a"是标识符,一个标识符从环境中被resolve到的时候,它就是一个“引用(规范类型)”。当它作为lhs时,它保持“作为引用(例如ref)”的特性不变;当它作为rhs的时候,它将被取值,即等义于GetValue(ref)。
作者回复: fObj不存在引用。
函数中通过return来返回时,是取值的。所以函数并不能返回“引用(规范类型)”。所以在函数中`return obj.foo`时,得到的将是函数foo,而不是对象方法foo。
不过我觉得你的这个问题,可能更多的讨论的是对“闭包”的理解。如果是这样,那么闭包就是一个名字表,只不过这个名字表是在`f()`这个调用的时候才创建得到的,如果不调用而只是引用函数`f`,例如`x = f`,那么这个过程中就根本没闭包什么事儿。
作者回复: 赞!这是极难得的进展!
值得再赞一次!!
作者回复: 新书还没出呢。😂
不过可以先看《程序原本》啊,免费开放的电子书。在这里:
https://github.com/aimingoo/my-ebooks
与本课程相关的章节,以及对应关系我在加餐里面已经讲过了。另外,JavaScript基础有一些是最好,没有的话也不用太紧张,这门课程主要不是讲JavaScript应用的,所以语言就算不太熟悉,也不算特别大的障碍。所以我一般只强调“要有语言经验”,以及“最好JavaScript已经入门”。
作者回复: 这里你是按传统的语言来理解的。并且,即使按“传统的语言”来理解,也并不太正确。
首先,并不是函数(或闭包)的变量存储在堆里。在一些语言中(例如Delphi),函数内的局部变量也是可以存放在栈上的,并且多数情况下就是如此。所以一些传统的语言概念和实现思路,并不见得总是对的。
其次,JavaScript是解释执行的,闭包(这里反过来说,或者函数)中的变量与标识符并不是使用堆/栈来管理的。所以它的回收也不是堆与栈的机制,通常这基于引用计数,但在不同的引擎中可能存在区别,是难以一概而论的。但是其中所谓“闭包”,才是回收所基于的一些基础组件,闭包代表了对对象、数据以及其它东西的引用,并且闭包自身也被作为引用对象——所以“函数每次调用完后函数内部的变量”其实在JavaScript中并不总是会被GC掉,因为闭包可能被别的地方引用了。例如简单的想法:你在函数内部return arguments.callee,那么函数(作为闭包)就被外部的环境引用了,那么显然变量之类的也不能被GC掉了。
总之,JavaScript的函数与传统语言中的函数有着很大的区别。从语言特性上来说,它的动态语言特性和函数式语言特性,都会给传统语言背景下的学习者带来很大的困扰。这也是我在开篇词里面一再强调“要放下既有知识包袱”的原因。
作者回复: let与var都是可变绑定,它们的不同不在于自身的数据结构(它们在引擎层面都是一样的)。
然而var是有初值的,而let是无初值的,这个是它们在创建的时候(或者在创建之后“随即”)发生的变化。进一步的,规范约定“不能访问一个没有初值的绑定”。因此,let就不能在它的赋值代码之前访问,而var则可以(因为它有初值),这是他们之间的差异的由来。
对于函数参数这个例子。简单参数是赋初值的,所以表现起来跟var一样。——对照起来,非简单参数也是一个“可变绑定”,但是没有赋初值(所以表现起来跟let一样)。文章中在这里的叙述,是想强调“使这个参数表现得像var/let”并不是原始的目的,原始的状况是这里本来没有什么目的性,只不过由于undefined这个值(作为缺省值)被缺省参数占用了,所以没办法用作初值而已。