关于前端:在script标签写export为什么会抛错

38次阅读

共计 6874 个字符,预计需要花费 18 分钟才能阅读完成。

明天咱们进入到语法局部的学习。在解说具体的语法结构之前,这一堂课我首先要给你介绍一下 JavaScript 语法的一些根本规定。

脚本和模块

首先,JavaScript 有两种源文件,一种叫做脚本,一种叫做模块。这个辨别是在 ES6 引入了模块机制开始的,在 ES5 和之前的版本中,就只有一种源文件类型(就只有脚本)。

脚本是能够由浏览器或者 node 环境引入执行的,而模块只能由 JavaScript 代码用 import 引入执行。

从概念上,咱们能够认为脚本具备主动性的 JavaScript 代码段,是管制宿主实现肯定工作的代码;而模块是被动性的 JavaScript 代码段,是期待被调用的库。

咱们对规范中的语法产生式做一些比照,不难发现,实际上模块和脚本之间的区别仅仅在于是否蕴含 import 和 export。

脚本是一种兼容之前的版本的定义,在这个模式下,没有 import 就不须要解决加载“.js”文件问题。

古代浏览器能够反对用 script 标签引入模块或者脚本,如果要引入模块,必须给 script 标签增加 type=“module”。如果引入脚本,则不须要 type。

<script type="module" src="xxxxx.js"></script>

这样,就答复了咱们题目中的问题,script 标签如果不加 type=“module”,默认认为咱们加载的文件是脚本而非模块,如果咱们在脚本中写了 export,当然会抛错。

脚本中能够蕴含语句。模块中能够蕴含三种内容:import 申明,export 申明和语句。一般语句咱们会在下一课专门给你解说,上面咱们就来讲讲 import 申明和 export 申明。

import 申明

咱们首先来介绍一下 import 申明,import 申明有两种用法,一个是间接 import 一个模块,另一个是带 from 的 import,它能引入模块里的一些信息。

import "mod"; // 引入一个模块 import v from "mod";  // 把模块默认的导出值放入变量 v 

间接 import 一个模块,只是保障了这个模块代码被执行,援用它的模块是无奈取得它的任何信息的。

带 from 的 import 意思是引入模块中的一部分信息,能够把它们变成本地的变量。

带 from 的 import 细分又有三种用法,咱们能够别离看下例子:

  • ​import x from "./a.js"​​ 引入模块中导出的默认值。
  • ​import {a as x, modify} from "./a.js";​​ 引入模块中的变量。
  • ​import * as x from "./a.js"​​ 把模块中所有的变量以相似对象属性的形式引入。

第一种形式还能够跟后两种组合应用。

  • ​import d, {a as x, modify} from "./a.js"​
  • ​import d, * as x from "./a.js"​

语法要求不带 as 的默认值永远在最前。留神,这里的变量实际上依然能够受到原来模块的管制。

咱们看一个例子,假如有两个模块 a 和 b。咱们在模块 a 中申明了变量和一个批改变量的函数,并且把它们导出。咱们用 b 模块导入了变量和批改变量的函数。

模块 a:

export var a = 1;export function modify(){a = 2;}

模块 b:

import {a, modify} from "./a.js";console.log(a);modify();console.log(a);

当咱们调用批改变量的函数后,b 模块变量也跟着产生了扭转。这阐明导入与个别的赋值不同,导入后的变量只是扭转了名字,它依然与原来的变量是同一个。

export 申明

咱们再来说说 export 申明。与 import 绝对,export 申明承当的是导出的工作。

模块中导出变量的形式有两种,一种是独立应用 export 申明,另一种是间接在申明型语句前增加 export 关键字。

独立应用 export 申明就是一个 export 关键字加上变量名列表,例如:
export {a, b, c};

咱们也能够间接在申明型语句前增加 export 关键字,这里的 export 能够加在任何申明性质的语句之前,整顿如下:

  • var
  • function (含 async 和 generator)
  • class
  • let
  • const

export 还有一种非凡的用法,就是跟 default 联结应用。export default 示意导出一个默认变量值,它能够用于 function 和 class。这里导出的变量是没有名称的,能够应用​​import x from "./a.js"​​这样的语法,在模块中引入。

​export default​​ 还反对一种语法,前面跟一个表达式,例如:

var a = {};export default a;

然而,这里的行为跟导出变量是不统一的,这里导出的是值,导出的就是一般变量 a 的值,当前 a 的变动与导出的值就无关了,批改变量 a,不会使得其余模块中引入的 default 值产生扭转。

在 import 语句前无奈退出 export,然而咱们能够间接应用 export from 语法。

export a from "a.js"

JavaScript 引擎除了执行脚本和模块之外,还能够执行函数。而函数体跟脚本和模块有肯定的相似之处,所以接下来,给你讲讲函数体的相干常识。

函数体

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

咱们先看一个例子,理性地了解一下:

setTimeout(function(){console.log("go go go");
}, 10000)

这段代码通过 setTimeout 函数注册了一个函数给宿主,当肯定工夫之后,宿主就会执行这个函数。

你还记得吗,咱们后面曾经在运行时这部分讲过,宿主会为这样的函数创立宏工作。

当咱们学习了语法之后,咱们能够认为,宏工作中可能会执行的代码包含“脚本 (script)”“模块(module)”和“函数体(function body)”。正因为这样的相似性,咱们把函数体也放到本课来解说。

函数体其实也是一个语句的列表。跟脚本和模块比起来,函数体中的语句列表中多了 return 语句能够用。

函数体实际上有四种,上面,我来别离介绍一下。

一般函数体,例如:

function foo(){    //Function body}

异步函数体,例如:

async function foo(){    //Function body}

生成器函数体,例如:

function \*foo(){    //Function body}

异步生成器函数体,例如:

async function \*foo(){    //Function body}

下面四种函数体的区别在于:是否应用 await 或者 yield 语句。

对于函数体、模块和脚本能应用的语句,我整顿了一个表格,你能够参考一下:

讲完了三种语法结构,我再来介绍两个 JavaScript 语法的全局机制:预处理和指令序言。

这两个机制对于咱们解释一些 JavaScript 的语法景象十分重要。不了解预处理机制咱们就无奈了解 var 等申明类语句的行为,而不了解指令序言,咱们就无法解释严格模式。

预处理

JavaScript 执行前,会对脚本、模块和函数体中的语句进行预处理。预处理过程将会提前解决 var、函数申明、class、const 和 let 这些语句,以确定其中变量的意义。

因为一些历史包袱,这一部分内容非常复杂,首先咱们看一下 var 申明。

var 申明

var 申明永远作用于脚本、模块和函数体这个级别,在预处理阶段,不关怀赋值的局部,只管在以后作用域申明这个变量。

咱们还是从实例来进行学习。

var a = 1;function foo() {    console.log(a);    var a = 2;
}foo();

这段代码申明了一个脚本级别的 a,又申明了 foo 函数体级别的 a,咱们留神到,函数体级的 var 呈现在 ​​console.log​​ 语句之后。

然而预处理过程在执行之前,所以有函数体级的变量 a,就不会去拜访外层作用域中的变量 a 了,而函数体级的变量 a 此时还没有赋值,所以是 undefined。咱们再看一个状况:

var a = 1;function foo() {    console.log(a);    if(false) {var a = 2;}
}foo();

这段代码比上一段代码在 var a = 2 之外多了一段 if,咱们晓得 if(false) 中的代码永远不会被执行,然而预处理阶段并不论这个,var 的作用可能穿透所有语句构造,它只认脚本、模块和函数体三种语法结构。所以这里后果跟前一段代码齐全一样,咱们会失去 undefined。

咱们看下一个例子,咱们在运行时局部讲过相似的例子。

var a = 1;function foo() {    var o= {a:3}    with(o) {var a = 2;}    console.log(o.a);    console.log(a);
}foo();

在这个例子中,咱们引入了 with 语句,咱们用 with(o) 创立了一个作用域,并把 o 对象退出词法环境,在其中应用了 var a = 2; 语句。

在预处理阶段,只认 var 中申明的变量,所以同样为 foo 的作用域创立了 a 这个变量,然而没有赋值。

在执行阶段,当执行到 var a = 2 时,作用域变成了 with 语句内,这时候的 a 被认为拜访到了对象 o 的属性 a,所以最终执行的后果,咱们失去了 2 和 undefined。

这个行为是 JavaScript 公认的设计失误之一,一个语句中的 a 在预处理阶段和执行阶段被当做两个不同的变量,重大违反了直觉,然而明天,在 JavaScript 设计准则“don’t break the web”之下,曾经无奈修改了,所以你须要特地留神。

因为早年 JavaScript 没有 let 和 const,只能用 var,又因为 var 除了脚本和函数体都会穿透,人民大众创造了“立刻执行的函数表达式(IIFE)”这一用法,用来产生作用域,例如:

for(var i = 0; i < 20; i ++) {void function(i){var div = document.createElement("div");
        div.innerHTML = i;
        div.onclick = function(){            console.log(i);
        }        document.body.appendChild(div);
    }(i);
}

这段代码十分经典,经常在理论开发中见到,也常常被用作面试题,为文档增加了 20 个 div 元素,并且绑定了点击事件,打印它们的序号。

咱们通过 IIFE 在循环内结构了作用域,每次循环都产生一个新的环境记录,这样,每个 div 都能拜访到环境中的 i。

如果咱们不必 IIFE:

for(var i = 0; i < 20; i ++) {var div = document.createElement("div");
    div.innerHTML = i;
    div.onclick = function(){        console.log(i);
    }    document.body.appendChild(div);
}

这段代码的后果将会是点每个 div 都打印 20,因为全局只有一个 i,执行完循环后,i 变成了 20。

function 申明
function 申明的行为本来跟 var 十分类似,然而在最新的 JavaScript 规范中,对它进行了肯定的批改,这让状况变得更加简单了。

在全局(脚本、模块和函数体),function 申明体现跟 var 类似,不同之处在于,function 申明岂但在作用域中退出变量,还会给它赋值。

咱们看一下 function 申明的例子:

console.log(foo);function foo(){}

这里申明了函数 foo,在申明之前,咱们用 console.log 打印函数 foo,咱们能够发现,曾经是函数 foo 的值了。

function 申明呈现在 if 等语句中的状况有点简单,它依然作用于脚本、模块和函数体级别,在预处理阶段,依然会产生变量,它不再被提前赋值:

console.log(foo);if(true) {function foo(){}}

这段代码失去 undefined。如果没有函数申明,则会抛出谬误。

这阐明 function 在预处理阶段依然产生了作用,在作用域中产生了变量,没有产生赋值,赋值行为产生在了执行阶段。

呈现在 if 等语句中的 function,在 if 创立的作用域中依然会被提前,产生赋值成果,咱们会在下一节课持续探讨。

class 申明

class 申明在全局的行为跟 function 和 var 都不一样。

在 class 申明之前应用 class 名,会抛错:

console.log(c);class c{}

这段代码咱们试图在 class 前打印变量 c,咱们失去了个谬误,这个行为很像是 class 没有预处理,然而实际上并非如此。

咱们看个简单一点的例子:

var c = 1;function foo(){    console.log(c);    class c {}}foo();

这个例子中,咱们把 class 放进了一个函数体中,在外层作用域中有变量 c。而后试图在 class 之前打印 c。

执行后,咱们看到,依然抛出了谬误,如果去掉 class 申明,则会失常打印出 1,也就是说,呈现在前面的 class 申明影响了后面语句的后果。

这阐明,class 申明也是会被预处理的,它会在作用域中创立变量,并且要求拜访它时抛出谬误。

class 的申明作用不会穿透 if 等语句构造,所以只有写在全局环境才会有申明作用,这部分咱们将会在下一节课解说。

这样的 class 设计比 function 和 var 更合乎直觉,而且在遇到一些比拟奇怪的用法时,偏向于抛出谬误。

依照古代语言设计的评估规范,及早抛错是坏事,它可能帮忙咱们尽量在开发阶段就发现代码的可能问题。

指令序言机制

脚本和模块都反对一种特地的语法,叫做指令序言(Directive Prologs)。

这里的指令序言最早是为了 use strict 设计的,它规定了一种给 JavaScript 代码增加元信息的形式。

"use strict";function f(){    console.log(this);
};
f.call(null);

这段代码展现了严格模式的用法,我这里定义了函数 f,f 中打印 this 值,而后用 call 的办法调用 f,传入 null 作为 this 值,咱们能够看到最终后果是 null 一成不变地被当做 this 值打印了进去,这是严格模式的特色。

如果咱们去掉严格模式的指令须要,打印的后果将会变成 global。

"use strict" 是 JavaScript 规范中规定的惟一一种指令序言,然而设计指令序言的目标是,留给 JavaScript 的引擎和实现者一些对立的表达方式,在动态扫描时指定 JavaScript 代码的一些个性。

例如,假如咱们要设计一种申明本文件不须要进行 lint 查看的指令,咱们能够这样设计:

"no lint";"use strict";function doSth(){    //......}//......

JavaScript 的指令序言是只有一个字符串间接量的表达式语句,它只能呈现在脚本、模块和函数体的最后面。

咱们看两个例子:

function doSth(){    //......}"use strict";var a = 1;//......

这个例子中,”use strict” 没有呈现在最前,所以不是指令序言。

'use strict';function doSth(){    //......}var a = 1;//......

这个例子中,’use strict’ 是单引号,这不障碍它依然是指令序言。

结语

明天,咱们一起进入了 JavaScript 的语法局部,在开始学习之前,我先介绍了一部分语法的根本规定。

咱们首先介绍了 JavaScript 语法的全局构造,JavaScript 有两种源文件,一种叫做脚本,一种叫做模块。介绍完脚本和模块的根底概念,咱们再来把它们往下分,脚本中能够蕴含语句。模块中能够蕴含三种内容:import 申明,export 申明和语句。

最初,我介绍了两个 JavaScript 语法的全局机制:预处理和指令序言。

最初,给你留一个小工作,咱们试着用 babel,剖析一段 JavaScript 的模块代码,并且找出它两头的所有 export 的变量。

更多内容: 👉开发者网站 – 技术前沿

正文完
 0