Skip to content

脚本和模块 #17

@K-Kevin

Description

@K-Kevin

ref:《重学前端》

脚本和模块

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

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

从概念上,我们可以认为脚本具有主动性的 JavaScript 代码段,是控制宿主完成一定任务的代码;而模块是被动性的JavaScript代码段,是等待被调用的库。

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

现代浏览器可以支持用script标签引入模块或者脚本,如果要引入模块,必须给script标签添加type=“module”。如果引入脚本,则不需要type。

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

脚本中可以包含语句。模块中可以包含三种内容:import声明,export声明和语句。

import声明

import声明有两种用法,一个是直接import一个模块,另一个是带from的import,它能引入模块里的一些信息。

import "mod"; // 引入一个模块
import v from "mod";  // 把模块默认的导出值放入变量 v
import {a as x, modify} from "./a.js";
  • 语法要求不带as的默认值永远在最前。注意,这里的变量实际上仍然可以受到原来模块的控制
  • 导入后只会改变名字,它仍然与原来的变量是同一个

export声明

我们也可以直接在声明型语句前添加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值发生改变。

预处理和指令序言

理解了预处理机制我们就理解var等声明类语句的行为,理解指令序言,就理解了严格模式。

预处理

JavaScript执行前,会对脚本、模块和函数体中的语句进行预处理。

预处理过程将会提前处理var、函数声明、class、const和let这些语句,以确定其中变量的意义。

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 设计失误,我们只能记住。

因为早年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);
}

这个是个经典问题,通过 IIFE 巧妙的产生作用域。如果不用 IIFE,那么结果就是点击每个 div 都打印 20,因为全局只有一个 i,执行完循环,i变成了 20。

function声明

function声明的行为原本跟var非常相似,但是在最新的JavaScript标准中,对它进行了一定的修改,这让情况变得更加复杂了。

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

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

}

这里声明了函数foo,在声明之前,我们用console.log打印函数foo,我们可以发现,已经是函数foo的值了。

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

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

    }
}

这段代码得到undefined。如果没有函数声明,则会抛出错误。

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

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等语句结构,所以只有写在全局环境才会有声明作用。

指令序言机制

脚本和模块都支持一种特别的语法,叫做指令序言(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'是单引号,这不妨碍它仍然是指令序言。

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions