04 | export default function() {}:你无法导出一个匿名函数表达式
04 | export default function() {}:你无法导出一个匿名函数表达式
讲述:周爱民
时长24:10大小22.12M
导出的内容
我的代码去哪儿了呢?
解析 export
导出语句的处理逻辑
导出名字与导出值的差异
匿名函数表达式的执行结果
知识补充
思考题
赞 13
提建议
精选留言(38)
- weineel2019-11-18ESModule 根据 import 构建依赖树,所以在代码运行前名字就是已经存在于上下文,然后在运行模块最顶层代码,给名字绑定值,就出现了‘变量提升’的效果。
作者回复: Yes! 满分答案👍
50 - 海绵薇薇2019-11-22hello 老师好,感谢老师之前的回答,有醍醐灌顶之效。 下面是读完这篇文章和下面评论之后的观点,不知是否有误,望指正,一如既往的感谢:) 1. function a() {} // 函数声明,在六种声明内 function () {} // 报错,以function 开头应该是声明,但是又没有名字 (function() {}) // 函数表达式(这是一个正真的匿名函数(function() {}).name 为 “”),即使是具名函数(function a() {}),当前作用域也找不到a,因为这不是声明 var a = function() {} // 函数定义,这里的function() {} 也是表达式,只是赋给了变量a,所以有了区别,也有了名字a.name为a,称作函数定义 var b = function c() {} // 函数定义,函数function c() {} 也是表达式,只是赋值给了变量b,但是b.name却为c,和上面存在的区别,但也是函数定义 2. 导出的是"名字",我理解为名字就像一个绳子,后面拴的牛是会变的。这就是为什么import {a} from '../a.js' 这个a会变,虽然当前模块不能赋值给a。展开
作者回复: 对哒!赞+2
14 - Y2019-11-18老师,关于这边文章的中心,我能总结成这个意思吗。 export default function(){}。这个语法本身没有任何的问题。但是他看似导出一个匿名函数表达式。其实他真正导出的是一个具有名字的函数,名字的default。
作者回复: 是的。不过,这算是题解。中心还是模块装载执行和标识符绑定全过程来着😄 标识符和值绑定是“声明”语法处理的核心,而六种声明是js静态语法的核心。而静态语法,也就是这一整篇“语言如何构建”的核心了🤓
13 - 万籁无声2019-11-18感觉没有抓住主题思想在表达什么,可能是我层次太低了
作者回复: 正好,刚写完“Y”同学的留言,你不妨看看,应该正好能回答你的疑问。 (万恶的极客时间没有提供分留言链接的功能,产品同学要打手板心5次 🤔)
12 - 许童童2019-11-18为什么在 import 语句中会出现“变量提升”的效果? 如老师所说,在代码真正被执行前,会先进行模块的装配过程,也就是执行一次顶层代码。所以如果import了一个模块,就会先执行模块内部的顶层代码,看起来的现象就是“变量提升”了。
作者回复: 😃👍
10 - 🇧🇪 Hazard�...2020-03-23老师,有一句话不太明白。 " import 的名字与 export 的名字只是一个映射关系 "。 export 一个变量,比如 count,如果设一个定时器执行,每次count都加 1; import { count }, 这个count也会每次都改变。这就是所说的映射关系吗? 这个映射关系是怎么做到的?展开
作者回复: 验证这个映射关系很简单。 B模块中export一个let变量,然后在A模块中import它为x。然后你尝试在A模块中x++,你会发现提示为常量不可写。 所以A、B两个模块中的名字其实并不是同一个变量,它们名字相同(或者不同),但A模块中只是通过一个(类似于别名的)映射来指向B模块中的名字。 映射是通过创建一个专用的数据结构来实现的,访问该结构就跳到目标数据,但每个操作都有特定的限制(例如上面的只读)。——这整体上有些类似于属性中的get/setter的机制,但并不是用属性描述符来做到的。
9 - Marvin2019-11-20export default v=>v 这种,箭头函数是特例吗?
作者回复: 有点特殊,但就处理逻辑(以及目的)上来说,也并不算是特例。 其实“函数定义(Function Definition)”这个概念出现得比较奇怪。 仔细分析一下就明白了,你想,“函数声明(Function Declaration)”是静态语义的,它在执行期的结果是empty,所以它必须是具名的才能导出,因为“声明(6种)”的目的都是具名,而export原则上只能“导出一个名字”。所以,由于“函数定义(Function Definition)”没有名字,所以它不能按函数声明来处理。 然后,由于“函数表达式(Function Expression)”是动态语义的,有执行语义(也就是执行结果返回不是empty),得到一个运行期概念上的“闭包”。但这并不是最关键处,最关键的地方在于函数表达式没名字——即使是具名的函数表达式,它的名字也只能闭包内有影响。由于它没有名字一个可供导出的名字,所以也不能直接直接用作export的对象。 那么到底在概念上该怎么说这个东西呢?ECMAScript在这里就加了这么一层概念,叫“函数声明(Function Declaration)”,一方面它是有静态语义的,它声明了某个东西;另一方面,它的名字又是迟绑定的,需要到了执行期根据“name = FunctionExpression”中的`name`来确认。 在这种情况下,其实“函数定义(Function Definition)”就是“函数表达式”的一层概念封装:它又有在外层(或被关联的对象)中的名字,它又是表达式;它的执行结果又是闭包,又是实例。 所以箭头函数看起来是特例,但用在导出语法的“这个位置”时,概念上却仍然是“封装了一层的‘箭头函数表达式’”,仍然还是“函数定义”。
共 4 条评论6 - leslee2019-11-19第三个结论推导过程的中间语法定义的引用那里(markdown '>' 符号表示的引用)读得不是很通顺, 有点迷....
作者回复: 这是因为类似于: obj = { f: function() { }, ... } 这样位置中的匿名函数,在ECMAScript中都是称为“匿名函数定义”,而不是“匿名函数表达式”。所有在语法上记为“x = functionExpression”的,在处理上都与一般表达式有不同,这是一个非常非常小的细节,但在引擎层面,加入了好大一段逻辑呢。 真正的匿名函数表达式,是下面这样的: > (1 + function() {}) 就是:把它直接用在一个表达式计算过程中,而不是把它用来赋值(或绑定,或引用)给另一个东西。这种情况下,它才是按匿名函数表达式来处理的。 这几讲都是讲JavaScript的静态语言特性的,所以“词法分析以及对应的引擎处理”是要点,在词法分析阶段,关键在于“不能为它(函数、函数表达式、函数定义等等)创建闭包”。因为在静态处理阶段,还没有“闭包”这个概念,所以好多东西处理起来跟我们平常的理解不同,这就是根由了。
6 - 七月有风2020-02-22ECMAScript 6 模块是静态装配的,而传统的 Node.js 模块却是动态加载的。是不是说node是在执行阶段才会执行模块的顶层代码。
作者回复: nodejs中,是在require()函数执行过程中来执行模块的顶层代码的。 nodejs模块被封装在一个函数中(亦即是作为一个函数的函数体),由require()在加载完指定模块的文本代码之后,用普通的调用函数的方法调用,从而实现模块装载的。
5 - leslee2019-12-14是否可以理解为,一个具有了名字的函数表达式就可以称为函数定义
作者回复: Yes. 这样理解没错。
4 - Geek_8858492020-08-21"use strict"; (function a() { const a = 2; console.log(a); })(); 老师您好,这个函数名a 不是已经作为函数内部的标识符了吗,为什么还可以重新声明呢?展开
作者回复: 关于这个问题,在《JavaScript语言精髓与编程实践》的第“5.5.2.4 函数表达式的特殊性”中专门有讲过,主要是因为“函数名作为标识符所声明的位置”所导致的。 具体来说,如果是函数声明,那么函数名是声明在它“所在”上下文的,因此它是否能“重新声明”取决于它所在的(外部的)上下文的严格模式状态。例如: ``` function foo() { function f() { "use strict"; f = 1; // 可重写,因为`f`声明在foo()中 } f() console.log(typeof f); // number } foo() ``` 为了在函数表达式中达成类似的效果(语言的一致性),所以函数表达式中这个函数名,也不是声明在函数体(以及由函数体所决定的闭包)中的。它采用了“双层作用域”的特殊构造,也就是函数名声明在外部作用域中(outerScope),而闭包的parent再指向这个outerScope。——函数的"use strict"只影响到函数自己的闭包。 所以回到你的例子, ``` "use strict"; (function a() { const a = 2; console.log(a); })(); ``` 由于是函数表达式,所以`a()`作为名字其实是声明在一个outerScope中的——没错,这个scope也是strict模式的。接下来函数body中声明了`count a`,这个名字所在的作用域(闭包)中并没有`a`这个名字,所以无论其外部,或者内部是否是严格模式,这个名字`a`都是可以创建的。 与此不同的是,函数参数是声明在闭包中的,所以它表现得跟函数名不同:如果函数参数中有名字a,那么上例中的`const a`就无法声明了。这同样也证明了函数名`a()`需要一个outerScope的重要性,因为历史中下面这样的代码“总是”合法的(无论是函数声明还是函数表达式): ``` function a(a) { ... } ```
共 2 条评论4 - 晓小东2019-12-19老师,我又来了,怕您看不到我的问题,接上一个问题,函数声明标识符不应该放入词法环境用中,本来我想函数声明标识符放入词法环境,来验证函数声明提升优先级高于var ,因为标识符的查找先从词法环境中查找,再到变量环境,再到上级作用域,从而实现声明的优先级。老师对于函数声明的优先级,你怎么看。
作者回复: 关于这个问题,其实还挺好玩儿的,因为它涉及到`execute_context.VariableEnvironment`这个东西怎么用的问题。 首先,其实词法环境(LexicalEnvironment)与变量环境(VariableEnvironment)并没有一个所谓优先级的问题。在实现上,它们之间是一个使用env.outer来衔接起来的链,所以所谓查找顺序,本质上就是二者谁在链的外层的问题。——然而,从实际实现的角度上,二者并不需要强调谁在外层,这种关系不是必须的(它们只需要衔接在一起就可以了)。 除了在函数或全局初始化需要一个表来指示“哪些东西是var和函数名”之外,事实上区分var/let/const之间的必要性是不大的。并且即使是在这种情况下,引擎也并不需要VariableEnvironment这个东东的参与,因为在它们初始化时,引擎是可以访问来自源代码的ParserNode的。也就是说,它可以直接访问原始的信息,而不必依赖VariableEnvironment这个列表。 VariableEnvironment这个东西,以及LexicalEnvironment,它们都是给运行期的上下文用的,也只在运行期才有意义。——更进一步的,只有对全局和函数,在它们的执行期才有意义(对函数来说,是它被调用的时候)。 为什么呢?就目前而言,VariableEnvironment其实只在一种情况下被用到。——就是当全局或函数内出现eval('var x...')这样的代码的时候。因为只有在这种情况下,在相应的变量环境中,才会需要执行上下文去访问变量环境列表,并动态地向中间插入一个新的名字。由于事实上var变量只能全局和函数有用,所以四种执行上下文(Global/Function/Module/Eval)中,虽然都有这两个成员,但其实Module.VariableEnvironment是没有用的,而Eval.VariableEnvironment受限于是否是在严格模式(当处在非严格模式时,它指向外层的——例如函数的VariableEnvironment;当处在严格模式时,它将自己创建一个,以隔离开对外部环境的影响)。 所以,本质上你来看VariableEnvironment这个东西的时候,不是要去“检查”它有什么样的优先级,而是直接看到“它有什么用,它怎么用”。再一次强调,对于单向链表访问来说,所谓“优先级”就是谁在链尾的问题;但即使如此,它对VariableEnvironment的使用来说也没有什么意义,因为VariableEnvironment归ExecuteContext使用,而ExecuteContext根本不care这个顺序。
4 - 穿秋裤的男孩2019-11-29可以这样理解吗? 静态解析期:export只导出名字到某个名字表,import从名字表获取映射关系。 执行期:执行代码,为名字赋值。
作者回复: 是的。这个“执行期”在用户代码之前。
4 - 穿秋裤的男孩2019-11-29所谓模块的装配过程,就是执行一次顶层代码而已。 这边的顶层代码是指什么呢?模块装配不是在静态解析期进行的吗?为什么还会执行代码?还是这边指的执行并不是一般意义上的执行呢?
作者回复: ``` // t.mjs console.log("here =>", typeof f); import f from './f.mjs'; // f.mjs export default function() {} console.log('NOW'); // test > node --experimental-modules t.mjs NOW here => function ``` 想想, 1. 为什么`here`为什么是function呢?import语句还没有到呢。 2. 为什么`NOW`在`here`之前?这是哪个时候的执行过程?
共 2 条评论4 - zxcv2021-04-01在语言设计中,所谓“标识符”与“名字”是有语义差别的,export 将之称为名字,就意味着这是一个标识符的子集。类似的其它子集也是存在的,例如“保留字是标识符名,但不能用作标识符(A reserved word is an IdentifierName that cannot be used as an Identifier)”。 看不懂哇~展开
作者回复: 这部分在ECMAScript里面就是这么讲述的,那段E文是取自ECMAScript的原文。关于这些概念,你可以看ECMAScript的相关内容,有些中文的可以看,在这里: https://www.w3.org/html/ig/zh/wiki/ES5 有些内容是在后面会介绍到,比如这些概念为什么要这么讲。第5讲后面的加餐,以及第11讲后的加餐都很重要。不过,不用着急,慢慢学到那些章节,就明白了。
2 - 李李2020-08-14我认为知识点讲解是要深入浅出, 好难接受这种风格。 所以的知识点都事无巨细看似很全但是没有重点。 看着难受....3
- 孜孜2020-06-29是不是因为“import语句所在的模块中却是一个常量”,这样才能保证无论多少个import,它们始终都是指向的是export那个变量?
作者回复: 是的。 另外,在设计上这符合“谁创建谁负责”的原则。对于export出来的x,所有的importer只能读;如果source发布x的写行为,那么应该另外export一个写方法出来。这类似于在对象中使用get/setter来访问属性,其设计原则和风格是一致的。
2 - Gamehu2020-02-21所以当都是export default...,以default为名字,但是import xx from ...,其实xx是import 重命名了default是么?不然就没法使用了
作者回复: 你写得有点不通顺,我只能试着答复你了。 export default会导致当前模块在命名空间中有一个特殊的名字,这个名字被记为`*default*`。由于它不符合命名规则,所以用户代码中既不可能使用,也不能声明出来。这样处理,是当“缺省导出”的名字有唯一性。 在使用`import xx from ...`的语法时,`xx`与模块的命名空间中的`*default*`其实创建了一个映射。这个映射在引擎内部也就是一次访问的跳转,这个是基于“引用(规范类型)”来实现的。 你说它是“重命名了default”,宽泛地来说,是对的。因为基本上这与“重命名”的效果很接近。但,本质上来说,“名字访问”是一次跳转,而“(对名字空间中的)映射访问”其实是二次跳转。
3 - 海绵薇薇2019-11-28Hello 老师好:) 函数定义 var a = function foo() { console.log(foo) } 当前上下文没有标识符foo,但是foo函数内却可以拿到该标识符,所以foo这个标识符应该是声明了,但是不在当前作用域,那么可以简单理解为 var a = eval('\ let foo;\ foo = function (){\ console.log(foo)\ }\ ') 可以这么理解吗?展开
作者回复: 是的。这样没问题。除了缺一咪咪的严谨之外,你的理解是对的。^^.
2 - 柠柚不加冰2021-03-08老师,问一下 export default ()=>{……XXX}; 和 const f = ()=>{……XXX}; export default f; 这两种方式有没有本质区别呢?还有就是在webpack打包后这两种写法的打包后的产物是一样的吗? 我在umi官网的Fast Refresh章节看到是推荐用const存一下的写法,这个只是为了开发阶段的模块热替换有保持组件状态的功能,还是说有其他影响?平常开发中推荐用哪一种方式呢?展开
作者回复: 没有本质上的区别。最终都是处理成了default导出。 但是还是有细节上的差异。比如第二种写法中,这个函数其实是有名字的——不是“具名函数”,但它确实有一个称为'f'的名字,这是由赋值语句导致的。例如: > console.log(f.name) 而你的第一种写法中,函数的这个f.name是无值的。比如: > import x from 'source_module.js'; > console.log(x.name) x在这里是通过一个(在js引擎的模块处理机制中的)隐藏的中间引用来访问到原来导出的匿名函数的,而那个函数在source_module.js中没有被const/var/let等赋值过,因此也就没有名字(nodejs中会给它赋一个"default"字符串来作为名字)。 最后补充一点,在第2种写法中,如果是使用的var或let来声明 f ...,那么你有机会在source_module.js中修改变量值。而像第1种写法那样直接使用export default ...,就没有机会再修改了。 两种写法都挺好的,也许使用const f的形式,会给你修改代码时带来一点灵活性吧。就我的使用来说,我是习惯用第一种的,简单就很好。
2