Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

脚本和模块 #17

Open
K-Kevin opened this issue Mar 15, 2020 · 0 comments
Open

脚本和模块 #17

K-Kevin opened this issue Mar 15, 2020 · 0 comments

Comments

@K-Kevin
Copy link
Owner

K-Kevin commented Mar 15, 2020

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'是单引号,这不妨碍它仍然是指令序言。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant