JavaScript 有两种源文件,一种叫作脚本,一种叫作模块。这个区分是在 ES6 引入了模块机制开始的,在 ES5 和以前的版本中,就只有一种源文件类型(就只有脚本)。javascript
脚本是能够由浏览器或者 node 环境引入执行的,而模块只能由 JavaScript 代码用 import 引入执行。java
实际上模块和脚本之间的区别仅仅在因而否包含 import 和 export。node
import 声明 咱们首先来介绍一下 import 声明,import 声明有两种用法,一个是直接 import 一个模块,另外一个是带 from 的 import,它能引入模块里的一些信息。面试
import "mod"; // 引入一个模块
import v from "mod"; // 把模块默认的导出值放入变量 v
复制代码
带 from 的 import 细分又有三种用法,咱们能够分别看下例子:浏览器
import x from "./a.js" 引入模块中导出的默认值。
import {a as x, modify} from "./a.js"; 引入模块中的变量。
import * as x from "./a.js" 把模块中全部的变量以相似对象属性的方式引入。
复制代码
咱们看一个例子,假设有两个模块 a 和 b。咱们在模块 a 中声明了变量和一个修改变量的函数,而且把它们导出。咱们用 b 模块导入了变量和修改变量的函数。 模块 a:app
export var a = 1;
export function modify(){
a = 2;
}
复制代码
模块 b:异步
import {a, modify} from "./a.js";
console.log(a);
modify();
console.log(a);
复制代码
当咱们调用修改变量的函数后,b 模块变量也跟着发生了改变。这说明导入与通常的赋值不一样,导入后的变量只是改变了名字,它仍然与原来的变量是同一个。async
咱们再来讲说 export 声明。与 import 相对,export 声明承担的是导出的任务。 模块中导出变量的方式有两种,一种是独立使用 export 声明,另外一种是直接在声明型语句前添加 export 关键字。函数
export {a, b, c};
复制代码
咱们也能够直接在声明型语句前添加 export 关键字,这里的 export 能够加在任何声明性质的语句以前,整理以下:ui
var
function (含 async 和 generator) class let const 复制代码
export 还有一种特殊的用法,就是跟 default 联合使用。export default 表示导出一个默认变量值,它能够用于 function 和 class。这里导出的变量是没有名称的,可使用import x from "./a.js"这样的语法,在模块中引入。
var a = {};
export default a;
复制代码
可是,这里的行为跟导出变量是不一致的,这里导出的是值,导出的就是普通变量 a 的值,之后 a 的变化与导出的值就无关了,修改变量 a,不会使得其余模块中引入的 default 值发生改变。 在 import 语句前没法加入 export,可是咱们能够直接使用 export from 语法。
export a from "a.js"
复制代码
函数体实际上有四种,下面,我来分别介绍一下。 普通函数体,例如:
function foo(){
//Function body
}
复制代码
异步函数体,例如:
async function foo(){
//Function body
}
复制代码
生成器函数体,例如:
function *foo(){
//Function body
}
复制代码
异步生成器函数体,例如:
function *foo(){
//Function body
}
复制代码
上面四种函数体的区别在于:可否使用 await 或者 yield 语句。 关于函数体、模块和脚本能使用的语句,我整理了一个表格,你能够参考一下:
不理解预处理机制咱们就没法理解 var 等声明类语句的行为,而不理解指令序言,咱们就没法解释严格模式。
JavaScript 执行前,会对脚本、模块和函数体中的语句进行预处理。预处理过程将会提早处理 var、函数声明、class、const 和 let 这些语句,以肯定其中变量的意义。 由于一些历史包袱,这一部份内容很是复杂,首先咱们看一下 var 声明。 var 声明 var 声明永远做用于脚本、模块和函数体这个级别,在预处理阶段,不关心赋值的部分,只管在当前做用域声明这个变量。
var a = 1;
function foo() {
console.log(a);
var a = 2;
}
foo(); // undefined
复制代码
这段代码声明了一个脚本级别的 a,又声明了 foo 函数体级别的 a,咱们注意到,函数体级的var出如今 console.log 语句以后。 可是预处理过程在执行以前,因此有函数体级的变量 a,就不会去访问外层做用域中的变量 a 了,而函数体级的变量 a 此时尚未赋值,因此是 undefined。咱们再看一个状况:
var a = 1;
function foo() {
console.log(a);
if(false) {
var a = 2;
}
}
foo(); // undefind
复制代码
这段代码比上一段代码在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。
这是js的设计失误, 由于早年 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。
"use strict";
function f(){
console.log(this);
};
f.call(null);
复制代码
这段代码展现了严格模式的用法,我这里定义了函数 f,f 中打印 this 值,而后用 call 的方法调用 f,传入 null 做为 this 值,咱们能够看到最终结果是 null 原封不动地被当作 this 值打印了出来,这是严格模式的特征。 若是咱们去掉严格模式的指令须要,打印的结果将会变成 global。