《重学前端》学习笔记(8)

JavaScript 的文法:JavaScript语法(1):

分号

自动插入分号规则

自动插入分号规则其实独立于所有的语法产生式定义,它的规则说起来非常简单,只有三条。

  • 要有换行符,且下一个符号是不符合语法的,那么就尝试插入分号。
  • 有换行符,且语法中规定此处不能有换行符,那么就自动插入分号。
  • 源代码结束处,不能形成完整的脚本或者模块结构,那么就自动插入分号。

no LineTerminator here 规则

`

JavaScript语法(2):

脚本和模块

  1. 在 ES5 和之前的版本中,就只有一种源文件类型,就只有脚本。

  2. 在 ES6 引入了模块机制开始的,JavaScript 有两种源文件,一种叫做脚本,一种叫做模块。

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

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

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

  6. 脚本是一种兼容之前的版本的定义,在这个模式下,没有import就不需要处理加载“.js”文件问题。
    现代浏览器可以支持用 script标签引入模块或者脚本,如果要引入模块,必须给 script标签添加 type=“module”。如果引入脚本,则不需要 type。

    <script type="module" src="xxxxx.js"></script>
    
  7. script 标签如果不加type=“module”,默认认为我们加载的文件是脚本而非模块,如果我们在脚本中写了 export,当然会抛错。

脚本中可以包含语句。

模块中可以包含三种内容:import 声明,export 声明和语句。下面来讲讲import 声明和export声明。
在这里插入图片描述

import 声明

import声明有两种用法:

  • 一个是直接 import一个模块。只是保证了这个模块代码被执行,引用它的模块是无法获得它的任何信息的。
  • 另一个是带fromimport,它能引入模块里的一些信息。可以把它们变成本地的变量。
import "mod"; // 引入一个模块
import v from "mod"; // 把模块默认的导出值放入变量 v

fromimport 细分又有三种用法,我们可以分别看下例子:

  • import x from "./a.js"引入模块中导出的默认值。
  • import {a as x, modify} from "./a.js";引入模块中的变量。
  • import * as x from "./a.js" 把模块中所有的变量以类似对象属性的方式引入。

第一种方式还可以跟后两种组合使用:(语法要求不带 as的默认值永远在最前。)

  • import d, {a as x, modify} from "./a.js"
  • import d, * as x from "./a.js"

export 声明

import相对,export声明承担的是导出的任务。

模块中导出变量的方式有两种:

  • 一种是独立使用 export声明

    独立使用export声明就是一个export关键字加上变量名列表,例如:

    export {a, b, c};
    
  • 另一种是直接在声明型语句前添加 export关键字

    这里的 export可以加在任何声明性质的语句之前,整理如下:

    • var
    • function (含 asyncgenerator)
    • class
    • let
    • const
  • export 还有一种特殊的用法,就是跟 default联合使用。

    export default表示导出一个默认变量值,它可以用于 functionclass。这里导出的变量是没有名称的,可以使用import x from "./a.js"这样的语法,在模块中引入。

export default 还支持一种语法,后面跟一个表达式,

var a = {};
export default a;

但是,这里的行为跟导出变量是不一致的,这里导出的是值,导出的就是普通变量 a 的值,以后 a 的变化与导出的值就无关了,修改变量 a,不会使得其他模块中引入的 default 值发生改变。

函数体

执行函数的行为通常是在 JavaScript 代码执行时,注册宿主环境的某些事件触发的,而执行的过程,就是执行函数体(函数的花括号中间的部分)。

函数体其实也是一个语句的列表。跟脚本和模块比起来,函数体中的语句列表中多了return语句可以用。

函数体实际上有四种:

  • 普通函数体

  • 异步函数体

  • 生成器函数体,例如:

    function *foo(){
     //Function body
    }
    
  • 异步生成器函数体,例如:

    async function *foo(){
     //Function body
    }
    

    上面四种函数体的区别在于:能否使用 await或者 yield语句。

关于函数体、模块和脚本能使用的语句,有个表格,可以参考:
在这里插入图片描述

讲完了三种语法结构,下面介绍两个 JavaScript 语法的全局机制:预处理和指令序言。

这两个机制对于我们解释一些 JavaScript 的语法现象非常重要。不理解预处理机制我们就无法理解 var 等声明类语句的行为,而不理解指令序言,我们就无法解释严格模式。

预处理

JavaScript 执行前,会对脚本、模块和函数体中的语句进行预处理。预处理过程将会提前处理 var、函数声明、class、const 和 let 这些语句,以确定其中变量的意义。

var 声明

var 声明永远作用于脚本、模块和函数体这个级别,在预处理阶段,不关心赋值的部分,只管在当前作用域声明这个变量。

var 的作用能够穿透一切语句结构,它只认脚本、模块和函数体三种语法结构。

一个例子:

var a = 1;
function foo() {
 var o= {a:3}
 with(o) {
 var a = 2;
 }
 console.log(o.a);
 console.log(a);
}
foo();
  1. 这个例子中,引入了with语句,用with(o)创建了一个作用域,并把 o 对象加入词法环境,在其中使用了var a = 2;语句。

  2. 在预处理阶段,只认var中声明的变量,所以同样为 foo 的作用域创建了 a 这个变量,但是没有赋值。

  3. 在执行阶段,当执行到var a = 2时,作用域变成了with语句内,这时候的 a 被认为访问到了对象 o 的属性 a,所以最终执行的结果,我们得到了 2 和undefined。

    这个行为是 JavaScript 公认的设计失误之一,一个语句中的 a 在预处理阶段和执行阶段被当做两个不同的变量,严重违背了直觉,但是今天,在 JavaScript 设计原则“don’t break the web”之下,已经无法修正了,所以你需要特别注意。

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 在预处理阶段仍然发生了作用,在作用域中产生了变量,没有产生赋值,赋值行为发生在了执行阶段。

    出现在if 等语句中的function,在if创建的作用域中仍然会被提前,产生赋值效果。

class 声明

class声明在全局的行为跟 functionvar 都不一样。

  • 在 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设计比 functionvar 更符合直觉,而且在遇到一些比较奇怪的用法时,倾向于抛出错误。

指令序言机制

脚本和模块都支持一种特别的语法,叫做指令序言(Directive Prologs)。

这里的指令序言最早是为了use strict设计的,它规定了一种给 JavaScript 代码添加元信息的方式。

"use strict"是 JavaScript 标准中规定的唯一一种指令序言,但是设计指令序言的目的是,留给 JS 的引擎和实现者一些统一的表达方式,在静态扫描时指定 JS 代码的一些特性。

发布了33 篇原创文章 · 获赞 73 · 访问量 2770

猜你喜欢

转载自blog.csdn.net/weixin_46124214/article/details/104212212
今日推荐