先了解一下 JavaScript 语法的一些基本规则,首先,在 ES6 引入了模块机制后,JavaScript 可以分为两种源文件:一种叫做脚本、一种叫做模块;而在 ES5 和之前的版本中,就只有一种脚本源文件类型。然而脚本是可以由浏览器或者 node 环境引入执行的,而模块只能由 JavaScript 代码用 import 引入执行。
从概念上可以认为脚本是具有主动性的 JavaScript 代码段,是控制宿主完成一定任务的代码;模块是被动性的 JavaScript 代码段,是等待被调用的库;也不难发现,模块和脚本之间的区别仅仅在于是否包含 import 和 export。但是现代浏览器可以支持直接用 script 标签引入模块或脚本,在引入模块的前提条件就是必须给 script 标签添加 type=”module” 属性,而引入脚本不需要加 type 属性,它的默认值是 text/javascript;如下引入模块:
1 | <script type="module" src="****.js"></script> |
这样,题目中的问题:在 script 标签中写 export 为什么会报错? 就可以解释了,因为在默认条件下我们加载的文件会被认为是脚本文件而不是模块,所以在脚本文件中写了 export 就会报错。而脚本和模块它们可以包含的内容有什么区别呢?
- 脚本
- 语句
- 模块
- import 声明
- export 声明
- 语句
显然,语句是脚本和模块的共同部分,并且是代码中经常写的,所以不难理解,下面对 import 声明、export 声明进行学习。
import 声明
import 在代码中的用法有两种:一、直接 import 一个模块;二、带 from 的 import,它能引入模块里的一些信息。
1 | import 'one module' // 直接引入一个模块 |
而直接 import 一个模块,只能保证模块代码被执行,引用它的模块无法获得它的任何信息;
带 from 的 import 意思是引入模块中的一部分信息,可以把它变成本地变量,于是可以在该模块中使用,它细分的话又有三种用法,如下:
- import x from ‘a.js’ // 引入模块中导出的默认值
- import { a as x, b } from ‘a.js’ // 引入模块中的变量
- import * as x from ‘a.js’ // 把模块中所有的变量以类似对象属性的方式引入
另外,第一种方式还可以跟后两种方式组合使用,但是其中语法要求不带 as 的默认值永远在最前面,如下:
1 | import c, { a as x, b } from 'a.js' |
export 声明
export 声明和 import 声明相对,export 声明承担的是导出任务。而模块中导出变量的方式有两种:一、独立使用 export 声明;二、直接在声明型语句前添加 export 关键字。
1 | // 第一种 |
最后,export 还有一种特殊的用法,它可以跟 default 联合使用;export default 表示导出一个默认变量值,它可以用于 function 和 class。
1 | // 这里导出的变量没有名称,可以直接使用下面的语句来在模块中引入 |
JavaScript 引擎除了执行脚本和模块之外,还可以执行函数,而函数体跟脚本和模块有一定的相似之处,如下。
函数体
执行函数的行为通常是在 JavaScript 代码执行时,注册宿主环境的某些事件触发的,而执行的过程就是执行函数体(函数的花括号中间的部分)。
1 | setTimeout(fuction(){ |
这里通过 setTimeout 函数注册了一个函数给宿主,在 2s 之后宿主就会执行这个函数;而宿主也会为这样的函数创建宏任务,在宏任务中可以执行的代码包括“脚本”、“模块”和“函数体”。其实函数体也是一个语句列表,而它跟脚本和模块比,函数体中的语句列表多了 return 语句可以用;实际上函数体有四种,如下:
- 普通函数体
1 | function foo() {} |
- 异步函数体
1 | async function foo() {} |
- 生成器函数体
1 | function *foo() {} |
- 异步生成器函数体
1 | async function *foo() {} |
在学习了 import、export和函数体三种语法结构后,对 JavaScript 代码执行时的一些语法现象的理解也很重要,它就是 JavaScript 语法的全局机制:预处理和指令序言。
- 预处理 // 理解 var 等声明语句的行为
- 指令序言 // 理解严格模式
预处理
JavaScript 执行前会对脚本、模块和函数体中的语句进行预处理,预处理过程会提前处理 var、函数声明、class、const和let这些语句。
- var // var 声明永远作用于脚本、模块和函数体这个级别,预处理阶段,不关心赋值的部分,只管在当前作用域声明这个变量
- function // function声明的行为原本跟 var 很相似,但是在最新的 JavaScript 标准中,对它进行了一定的修改,变复杂了;在全局,function 声明表现跟 var 相似,不同在于 function 声明不但在作用域加入了变量,还会给它赋值
- class // class 声明的行为跟 function 和 var 都不一样,在 class 声明之前使用 class 名会报错,如下:
1 | // 第一种 |
这里的第一个例子中把 class 放在了打印之后,这里直接会报错。
这里的第二个例子中把 class 放进了一个函数体内,在函数体外面定义了和 class 名相同的变量 c,在打印时,它任然会报错,而在去掉 class 声明则会直接打印出 1,也就是说,出现在后面的 class 声明会声明影响了前面语句的结果;这说明,class 声明也是会被预处理的,它会在作用域中创建变量,并且访问它是抛出错误。
这里的 class 设计比 function 和 var 更符合直觉,在遇到奇怪的用法时,它更倾向于直接抛出错误;而提前抛出错误是有利于代码发现问题的。
指令序言
脚本和模块都支持一种叫指令序言的语法,它最早是为了 use strict 设计的,它规定了一种给 JavaScript 代码添加元信息的方式;如下:
1 |
|
在严格模式下,用 call 的方法调用 foo,传入 null 作为 this 值,在严格模式下它会原封不动的打印出来;如果去掉严格模式,打印的结果将会变成 global。
加入我们要声明一种文件不需要 lint 检查的指令,可以这样:
1 | 'no lint' |
这样就可以了,而 JavaScript 的指令序言是只有一个字符串直接量的表达式,它只能出现在脚本、模块和函数体的最前面。
总结
这里从JavaScript语法的全局结构开始,学习了 JavaScript 的两种源文件:脚本和模块;最后将脚本和模块中可以包含的语句类型 import、export 和 正常的语句进行了学习;在学习语句的同时,为了更好的理解 JavaScript 的一些语法情况,对预处理器和指令序言进行了了解。