在script标签中写export为什么会报错

先了解一下 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
2
import 'one module' // 直接引入一个模块
import something from 'other module' // 把 other module 默认的导出值放入变量 something

而直接 import 一个模块,只能保证模块代码被执行,引用它的模块无法获得它的任何信息;
带 from 的 import 意思是引入模块中的一部分信息,可以把它变成本地变量,于是可以在该模块中使用,它细分的话又有三种用法,如下:

  • import x from ‘a.js’ // 引入模块中导出的默认值
  • import { a as x, b } from ‘a.js’ // 引入模块中的变量
  • import * as x from ‘a.js’ // 把模块中所有的变量以类似对象属性的方式引入

另外,第一种方式还可以跟后两种方式组合使用,但是其中语法要求不带 as 的默认值永远在最前面,如下:

1
2
import c, { a as x, b } from 'a.js'
import c, * as x from 'a.js'

export 声明

export 声明和 import 声明相对,export 声明承担的是导出任务。而模块中导出变量的方式有两种:一、独立使用 export 声明;二、直接在声明型语句前添加 export 关键字。

1
2
3
4
5
6
7
8
// 第一种
export { a, b, c}

// 第二种
export var a = 10;
export function() {
console.log('export');
}

最后,export 还有一种特殊的用法,它可以跟 default 联合使用;export default 表示导出一个默认变量值,它可以用于 function 和 class。

1
2
3
4
5
6
7
// 这里导出的变量没有名称,可以直接使用下面的语句来在模块中引入
import x from 'a.js'

// 另外 export default 还支持后面跟一个表达式
var a = {};
export default a;
// **但是这里的行为跟导出变量不一致,这里导出的是值,导出的就是一个普通的变量 a 的值,以后 a 的变化与导出的值就没有关系了,修改变量 a,不会使得其它模块中引入的 default 的值发生改变。**

JavaScript 引擎除了执行脚本和模块之外,还可以执行函数,而函数体跟脚本和模块有一定的相似之处,如下。

函数体

执行函数的行为通常是在 JavaScript 代码执行时,注册宿主环境的某些事件触发的,而执行的过程就是执行函数体(函数的花括号中间的部分)。

1
2
3
setTimeout(fuction(){
console.log('setTimeout');
}, 2000);

这里通过 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
2
3
4
5
6
7
8
9
10
11
12
// 第一种
console.log(c) // 报错
class c {}

// 第二种
var c = 1;
function foo () {
console.log(c) // 报错,如果删掉 class c {},就能正常打印 1
class c {}
}

foo()

这里的第一个例子中把 class 放在了打印之后,这里直接会报错。
这里的第二个例子中把 class 放进了一个函数体内,在函数体外面定义了和 class 名相同的变量 c,在打印时,它任然会报错,而在去掉 class 声明则会直接打印出 1,也就是说,出现在后面的 class 声明会声明影响了前面语句的结果;这说明,class 声明也是会被预处理的,它会在作用域中创建变量,并且访问它是抛出错误。
这里的 class 设计比 function 和 var 更符合直觉,在遇到奇怪的用法时,它更倾向于直接抛出错误;而提前抛出错误是有利于代码发现问题的。

指令序言

脚本和模块都支持一种叫指令序言的语法,它最早是为了 use strict 设计的,它规定了一种给 JavaScript 代码添加元信息的方式;如下:

1
2
3
4
5
6
"use strict"
function foo() {
console.log(this); // 直接输出 null
}

foo.call(null);

在严格模式下,用 call 的方法调用 foo,传入 null 作为 this 值,在严格模式下它会原封不动的打印出来;如果去掉严格模式,打印的结果将会变成 global。

加入我们要声明一种文件不需要 lint 检查的指令,可以这样:

1
2
3
'no lint'
'use strict'
function doSth() {}

这样就可以了,而 JavaScript 的指令序言是只有一个字符串直接量的表达式,它只能出现在脚本、模块和函数体的最前面。

总结

这里从JavaScript语法的全局结构开始,学习了 JavaScript 的两种源文件:脚本和模块;最后将脚本和模块中可以包含的语句类型 import、export 和 正常的语句进行了学习;在学习语句的同时,为了更好的理解 JavaScript 的一些语法情况,对预处理器和指令序言进行了了解。