JS语法基本规则

735 阅读9分钟

这里有一份简洁的前端知识体系等待你查收,看看吧,会有惊喜哦~如果觉得不错,恳求star哈~


前言

语法分析,就是在词法分析的基础上,组合语法短语,构建词法树。

词法部分的内容非常丰富,我们先从最宏观的基本规则说起~


全局结构


脚本和模块

JavaScript有两种源文件,一种叫做脚本,一种叫做模块(ES6才有的)。

  • 脚本是具有主动性的JavaScript代码段,是控制宿主完成一定任务的代码。
  • 模块是被动性的JavaScript代码段,只能由import引入执行,是等待被调用的库。

实际上模块和脚本之间的区别仅仅在于是否包含 import 和 export。

脚本跟模块的关系如下:

images

现代浏览器可以支持用script标签引入模块或者脚本,如果要引入模块,必须给script标签添加type="module",而且还需要在服务器环境,否则抛错。

这里先讲import声明和export声明,语句会有专门的讲解


import声明


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

import "mod"; //引入一个模块,但无法引用它的任何信息
import v from "mod"; //把模块默认的导出值放入变量v

带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的默认值永远在最前。


export声明


与import相对, export 声明承担的是导出的任务。模块中导出变量的方式有两种:

  • 独立使用export声明,也就是一个export关键字加上变量名列表,例如:export {a, b, c};
  • 直接在声明型语句前添加export关键字,这里的export可以加在任何声明性质的语句之前,整理如下:var、function (含async和generator)、class、let、const

export 有两种特殊用法: export default 、 export from 。


export default

export default 表示导出一个默认变量值,它可以用于function和class。

这里导出的变量是没有名称的,可以使用import x from "./a.js"这样的语法,在模块中引入。

本质上, export default 就是输出一个叫做default的变量或方法,所以export default后面不能再跟变量声明语句,但可以将一个值写在export default之后,import时系统允许你为它取任意名字。(参考资料)


export from


在import语句前无法加入export,但是我们可以直接使用 export from 语法。

export a from "a.js"

函数体


JavaScript引擎除了执行脚本和模块之外,还可以执行函数。而函数体跟脚本和模块有一定的相似之处,所以将函数体放在这里讲。

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

函数体实际上有四种,分别是普通函数体、异步函数体、生成器函数体、异步生成器函数体。

普通函数体,例如:

function foo(){
  //Function body
}

异步函数体,例如:

async function foo(){
  //Function body
}

生成器函数体,例如:

function *foo(){
  //Function body
}

异步生成器函数体,例如:

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

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

关于函数体、模块和脚本能使用的语句如下:

类型 yield await return import & export
普通函数体 × × ×
异步函数体 × ×
生成器函数体 × ×
异步生成器函数体 ×
脚本 × × × ×
模块 × × ×

全局机制

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


预处理

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


var声明

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

来看个例子:

var a = 1;
function foo() {
    console.log(a); // undefined
    if(false) {
        var a = 2;
    }
}
foo();

预处理过程在执行之前,所以有函数体级的变量a,就不会去访问外层作用域中的变量a了,而函数体级的变量a此时还没有赋值,所以打印出undefined。

值得注意的是,我们知道if(false)中的代码永远不会被执行,但是预处理阶段并不管这个,var的作用能够穿透一切语句结构,它只认脚本、模块和函数体三种语法结构。所以这里才打印出undefined。

特殊情况

var 声明要注意with语句,with语句会让变量在预处理阶段和执行阶段被当做两个不同变量,这也是为什么建议不要使用with语句的原因。

看个例子:

var a = 1;
function foo () {
  var o = {a: 3};
  with(o) {
    var a = 2;
  }
  console.log (o.a); // 2
  console.log (a); // undefined
}
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”之下,已经无法修正了,所以你需要特别注意。


function声明

在全局(脚本、模块和函数体),function声明表现跟var相似,但有两点不同。

第一个不同:函数声明会被提升,并且优先于变量提升

来看个例子:

console.log(a) // ƒ a() {}
function a() {}
var a = 1

对于上述代码,打印结果会是ƒ a() {} ,即使变量声明在函数之后,这也说明了函数会被提升,并且优先于变量提升

其实你仔细看你会发现,函数声明不但被提升,还被赋值了,赋值到底是发生在预处理阶段,还是执行阶段呢?

这就引起第二个不同:函数声明在预处理阶段被提升,从而在作用域中产生了变量,没有产生赋值,赋值行为发生在了执行阶段

对比下面两个例子:

// 例子1
console.log(foo); // ƒ foo(){}
if(true) {
    function foo(){}
}

// 例子2
console.log(foo);  // undefined
if(false) {
    function foo(){}
}

if为true时,打印出函数声明,if为false,打印出undefined,这就说明,出现在if等语句中的function,在if创建的作用域中仍然会被提前,但赋值发生在执行过程。


class声明

class声明在全局的行为跟function和var都不一样。

首先,class声明也是会被预处理的

var c = 1;
function foo(){
  console.log(c); // Uncaught ReferenceError: Cannot access 'c' before initialization
  class c {}
}
foo()

可以看到,在class声明之前打印c会报错,这说明class声明也会预处理,之所以抛错,是因为暂时性死区。

其次,class的声明作用不会穿透if等语句结构,所以只有写在全局环境才会有声明作用

看个例子:

if (true) {
  class B{}
}
console.log(B); //  Uncaught ReferenceError: B is not defined

可见,class设计比function和var更符合直觉,并且在遇到一些比较奇怪的用法时,倾向于抛出错误。按照现代语言设计的评价标准,及早抛错是好事,它能够帮助我们尽量在开发阶段就发现代码的可能问题。


指令序言机制

JS的指令序言是只有一个字符串直接量的表达式语句,它只能出现在脚本、模块和函数体的最前面

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

看下例子:

// 例子1
function f(){
  console.log(this); // Window
}
f.call(null);
// 例子2
"use strict"
function f(){
  console.log(this); // null
}
f.call(null);

这段代码展示了严格模式的用法,我这里定义了函数f,f中打印this值,然后用call的方法调用f,传入null作为this值,我们可以看到最终结果是null原封不动地被当做this值打印了出来,这是严格模式的特征。如果我们去掉严格模式的指令需要,打印的结果将会变成global。

特别注意的是:指令序言只能出现在脚本、模块和函数体的最前面,否则就不构成指令序言。


总结

这篇文章中,我们介绍了一部分语法的基本规则。

我们首先介绍了 JS 语法的全局结构,JavaScript有两种源文件,一种叫做脚本,一种叫做模块。介绍完脚本和模块的基础概念,我们再来把它们往下分,脚本中可以包含语句。模块中可以包含三种内容:import声明,export声明和语句。

最后,我介绍了两个 JS 语法的全局机制:预处理和指令序言。