相信不少人最开始时都有过这样的疑问
假如个人项目目录下有一个 index.html, index.js 因而我像这样写html
<script src="index.js"></script>
在浏览器之间打开index.html,发现node
这究竟是为何?为何连chrome浏览器居然还不彻底支持es6的语法?
其实,ES6以前已经出现了js模块加载的方案,最主要的是CommonJS和AMD规范。commonjs主要应用于服务器,实现同步加载,如nodejs。AMD规范应用于浏览器,如requirejs,为异步加载。同时还有CMD规范,为同步加载方案如seaJS。
ES6在语言规格的层面上,实现了模块功能,并且实现得至关简单,彻底能够取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。话有回到咱们刚才的问题 '为何chrome浏览器居然还不彻底支持es6的语法'es6
首先,JavaScript有两种源文件,一种叫作脚本,一种叫作模块。这个区分是在ES6引入了模块机制开始的,在ES5和以前的版本中,就只有一种源文件类型(就只有脚本)。web
脚本是能够由浏览器或者node环境引入执行的,而模块只能由JavaScript代码用import引入执行。面试
从概念上,咱们能够认为脚本具备主动性的JavaScript代码段,是控制宿主完成必定任务的代码;而模块是被动性的JavaScript代码段,是等待被调用的库。chrome
咱们对标准中的语法产生式作一些对比,不难发现,实际上模块和脚本之间的区别仅仅在因而否包含import 和 export。typescript
脚本是一种兼容以前的版本的定义,在这个模式下,没有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值发生改变。
说到这里,就来大体说说export和export default的区别
1.export与export default都可用于导出常量、函数、文件、模块等 2.在一个文件或模块中,export、import能够有多个,export default仅有一个 3.经过export方式导出,在导入时要加{ },export default则不须要
或者咱们能够这样理解,export default的本质其实就是讲后面的值付给default变量,而后你能够为它取你想要的变量
因此 export default 1 // 执行 export 1 // 报错
第二行报错正式是由于没有指定对外的接口,而第一句指定为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(){ // } 异步函数体,例如: async function foo(){ // } 生成器函数体,例如: function *foo(){ // } 异步生成器函数体,例如: async function *foo(){ // }
上面四种函数体的区别在于:可否使用await或者yield语句。
说完了三种语法结构,再来介绍下JavaScript语法的全局机制(非严格模式):预处理。
这对于咱们解释一些JavaScript的语法现象很是重要。不理解预处理机制咱们就没法理解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更符合直觉,并且在遇到一些比较奇怪的用法时,倾向于抛出错误。
按照现代语言设计的评价标准,及早抛错是好事,它可以帮助咱们尽可能在开发阶段就发现代码的可能问题。
针对以上问题以及一些不严谨的问题和一些引擎难以优化的错误,出现了严格模式
设立"严格模式"的目的,主要有如下几个:
- 消除Javascript语法的一些不合理、不严谨之处,减小一些怪异行为;
- 消除代码运行的一些不安全之处,保证代码运行的安全;
- 提升编译器效率,增长运行速度;
- 为将来新版本的Javascript作好铺垫。
其中 ES6 的模块自动采用严格模式,无论你有没有在模块头部加上"use strict";
至于日常开发时咱们到底要不要使用严格模式以及包括要不要使用typescript?每一个人都有每一个人的观点!那么,在开发中你是否推荐用严格模式来'约束'你的代码及风格呢?