Js中为什么我们不能直接使用export?

更新日期: 2019-08-21阅读: 1.4k标签: es6

相信很多人最开始时都有过这样的疑问
假如我的项目目录下有一个 index.html, index.js 于是我像这样写

<script src="index.js"></script>

浏览器之间打开index.html,发现

这到底是为什么?为什么连chrome浏览器竟然还不完全支持es6的语法?
其实,ES6之前已经出现了js模块加载的方案,最主要的是CommonJS和AMD规范。commonjs主要应用于服务器,实现同步加载,如nodejs。AMD规范应用于浏览器,如requirejs,为异步加载。同时还有CMD规范,为同步加载方案如seaJS。
ES6在语言规格的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。话有回到我们刚才的问题 '为什么chrome浏览器竟然还不完全支持es6的语法'

首先,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值发生改变。

说到这里,就来大致说说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?每个人都有每个人的观点!那么,在开发中你是否推荐用严格模式来'约束'你的代码及风格呢?

原文:https://segmentfault.com/a/1190000020123242

链接: https://www.fly63.com/article/detial/4827

es6 箭头函数的使用总结,带你深入理解js中的箭头函数

箭头函数是ES6中非常重要的性特性。它最显著的作用就是:更简短的函数,并且不绑定this,arguments等属性,它的this永远指向其上下文的 this。它最适合用于非方法函数,并且它们不能用作构造函数。

详解JavaScript模块化开发require.js

js模块化的开发并不是随心所欲的,为了便于他人的使用和交流,需要遵循一定的规范。目前,通行的js模块规范主要有两种:CommonJS和AMD

js解构赋值,关于es6中的解构赋值的用途总结

ES6中添加了一个新属性解构,允许你使用类似数组或对象字面量的语法将数组和对象的属性赋给各种变量。用途:交换变量的值、从函数返回多个值、函数参数的定义、提取JSON数据、函数参数的默认值...

ES6中let变量的特点,使用let声明总汇

ES6中let变量的特点:1.let声明变量存在块级作用域,2.let不能先使用再声明3.暂时性死区,在代码块内使用let命令声明变量之前,该变量都是不可用的,4.不允许重复声明

ES6的7个实用技巧

ES6的7个实用技巧包括:1交换元素,2 调试,3 单条语句,4 数组拼接,5 制作副本,6 命名参数,7 Async/Await结合数组解构

ES6 Decorator_js中的装饰器函数

ES6装饰器(Decorator)是一个函数,用来修改类的行为 在设计阶段可以对类和属性进行注释和修改。从本质上上讲,装饰器的最大作用是修改预定义好的逻辑,或者给各种结构添加一些元数据。

基于ES6的tinyJquery

Query作为曾经Web前端的必备利器,随着MVVM框架的兴起,如今已稍显没落。用ES6写了一个基于class简化版的jQuery,包含基础DOM操作,支持链式操作...

ES6 中的一些技巧,使你的代码更清晰,更简短,更易读!

ES6 中的一些技巧:模版字符串、块级作用域、Let、Const、块级作用域函数问题、扩展运算符、函数默认参数、解构、对象字面量和简明参数、动态属性名称、箭头函数、for … of 循环、数字字面量。

Rest/Spread 属性_探索 ES2018 和 ES2019

Rest/Spread 属性:rest操作符在对象解构中的使用。目前,该操作符仅适用于数组解构和参数定义。spread操作符在对象字面量中的使用。目前,这个操作符只能在数组字面量和函数以及方法调用中使用。

使用ES6让你的React代码提升到一个新档次

ES6使您的代码更具表现力和可读性。而且它与React完美配合!现在您已了解更多基础知识:现在是时候将你的ES6技能提升到一个新的水平!嵌套props解构、 传下所有props、props解构、作为参数的函数、列表解构

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!