JavaScript语法

到底要不要写分号呢?

JavaScript 语言提供了相对可用的分号自动补全规则

自动插入分号规则

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

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

第一条:意思是,如果下一行和上一行连起来不符合语法规范,就在换行的位置插入分号。通常自动插入的分号都是这种形式
第二条:这种情况与第一种情况的不同之处在于,这种情况下,有可能上下两行连起来符合语法规范,但是当前行的语法不允许有换行符,所以会自动插入分号

var a = 1, b = 1, c = 1;
a
++
b
++
c
//a 1
//b 2
//c 2

这里可以看到,a++是符合语法的,但是这一行不允许有换行符,所以会在a后加分号,使得变成a;++b;++c

no LineTerminator here 规则

在这里插入图片描述

不写分号需要注意的情况

以括号开头的语句

这段代码看似两个独立执行的函数表达式,但是其实第三组括号被理解为传参,导致抛出错误。

(function(a){
    console.log(a);
})()/*这里没有被自动插入分号*/
(function(a){
    console.log(a);
})()

所以我们要在行首的()之前加入;

;(function(a){
    console.log(a);
})()

以数组开头的语句

这段代码本意是一个变量 a 赋值,然后对一个数组执行 forEach,但是因为没有自动插入分号,被理解为下标运算符和逗号表达式,甚至不会抛出错误,这对于代码排查问题是个噩梦。

var a = [[]]/*这里没有被自动插入分号*/
[3, 2, 1, 0].forEach(e => console.log(e))

解决方案也是在[]开头的语句之前加入;

以正则表达式开头的语句

这段代码本意是声明三个变量,然后测试一个字符串中是否含有字母 a,但是因为没有自动插入分号,正则的第一个斜杠被理解成了除号,后面的意思就都变了。同样不会抛错,凡是这一类情况,都非常致命。

var x = 1, g = {test:()=>0}, b = 1/*这里没有被自动插入分号*/
/(a)/g.test("abc")
console.log(RegExp.$1)

以 Template(字符串模板) 开头的语句

这段代码本意是声明函数 f,然后赋值给 g,再测试 Template 中是否含有字母 a。但是因为没有自动插入分号,函数 f 被认为跟 Template 一体的,进而被莫名其妙地执行了一次。

var f = function(){
  return "";
}
var g = f/*这里没有被自动插入分号*/
`Template`.match(/(a)/);
console.log(RegExp.$1)

小结

按照通常的语法规则,可以利用自动添加分号的规则,不用在每行末尾都添加分号,只需按照正常代码格式书写即可
但是要在特殊情况时,主动添加分号,以避免代码出现歧义以引发难以发现的bug

  • ()
  • []
  • ``
  • //
    以这四种符号开头的代码行,需要在行首添加;以避免产生bug

在script标签写export为什么会抛错?

脚本和模块

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

  • 实际上模块和脚本之间的区别仅仅在于是否包含 import 和 export。
  • 现代浏览器可以支持用 script 标签引入模块或者脚本,如果要引入模块,必须给 script 标签添加 type=“module”。如果引入脚本,则不需要 type。

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

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

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导入变量其实还在模块中,当这个变量发生变化时,这个导入后的变量也同时产生改变。

export 声明

模块中导出变量的方式有两种,一种是独立使用 export 声明,另一种是直接在声明型语句前添加 export 关键字。

  • 独立使用 export 声明就是一个 export 关键字加上变量名列表
  • export {a, b, c};
  • export 可以加在任何声明性质的语句之前,包括:var,function (含 async 和 generator),class,let,const
  • export var a = 1;

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

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

函数体

宏任务中可能会执行的代码包括“脚本 (script)”“模块(module)”和“函数体(function body)”

就是函数的{}内的部分,同时这也是一个函数作用域,所以和脚本,模块的全局作用域同级

预处理

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

var 声明

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

var a = 1;

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

foo();// undefined

function 声明

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

console.log(foo); // ƒ foo(){}
function foo(){}

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

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

class 声明

在 class 声明之前使用 class 名,会抛错

var c = 1;
function foo(){
    console.log(c);
    class c {}
}
foo(); //抛出错误

class 声明也是会被预处理的,它会在作用域中创建变量,并且要求访问它时抛出错误。

  • class声明在预处理时会提前,并且在声明之前使用会报错
  • class 的声明作用不会穿透 if 等语句结构,所以只有写在全局环境才会有声明作用

你知道哪些JavaScript语句?

在这里插入图片描述
在这里插入图片描述

普通语句

语句块

  • 语句块就是一对大括号。让我们可以把多行语句视为同一行语句
  • 语句块会产生作用域

空语句

  • 空语句就是一个独立的分号,实际上没什么大用。仅仅是为了允许插入多个分号而不抛出错误。

if 语句

  • if 语句是条件语句。if 语句的作用是,在满足条件时执行它的内容语句,这个语句可以是一个语句块

switch 语句

  • 其实 switch 原本的设计是类似 goto 的思维
  • 跳转到某个位置然后继续执行
  • 如果我们要把它变成分支型,则需要在每个 case 后加上 break。

循环语句

  • for in 循环枚举对象的属性,这里体现了属性的 enumerable 特征。
  • for of 循环背后的机制是 iterator 机制。
  • 给任何一个对象添加 iterator,使它可以用于 for of 语句
function* foo(){
    yield 0;
    yield 1;
    yield 2;
    yield 3;
}
for(let e of foo())
    console.log(e);
  • 异步生成器搭配for await of 循环
function sleep(duration) {
    return new Promise(function(resolve, reject) {
        setTimeout(resolve,duration);
    })
}
async function* foo(){
    i = 0;
    while(true) {
        await sleep(1000);
        yield i++;
    }
        
}
for await(let e of foo())
    console.log(e);

// 从0开始每隔一秒打印一个数字
  • 这个循环是异步的,并且有时间延迟,所以,这个无限循环的代码可以用于显示时钟等有意义的操作。

return

  • return 语句用于函数中,它终止函数的执行,并且指定函数的返回值

break 语句和 continue 语句

  • break 语句用于跳出循环语句或者 switch 语句,continue 语句用于结束本次循环并继续循环。
  • 他们都有带标签的用法

with 语句

  • with 语句把对象的属性在它内部的作用域内变成变量。
  • 不要使用with语句

try 语句和 throw 语句

  • throw 用于抛出异常,但是单纯从语言的角度,我们可以抛出任何值,也不一定是异常逻辑,但是为了保证语义清晰,不建议用 throw 表达任何非异常逻辑。
  • try 语句用于捕获异常,用 throw 抛出的异常,可以在 try 语句的结构中被处理掉:try 部分用于标识捕获异常的代码段,catch 部分则用于捕获异常后做一些处理,而 finally 则是用于执行后做一些必须执行的清理工作。
  • catch 结构会创建一个局部的作用域,并且把一个变量写入其中,需要注意,在这个作用域,不能再声明变量 e 了,否则会出错。
  • finally 语句一般用于释放资源,它一定会被执行

debugger 语句

  • debugger 语句的作用是:通知调试器在此断点。在没有调试器挂载时,它不产生任何效果。

声明型语句

let 和 const

  • let 和 const 声明虽然看上去是执行到了才会生效,但是实际上,它们还是会被预处理
  • 如果当前作用域内有声明,就无法访问到外部的变量。
const a = 2;
if(true){
    console.log(a); //抛错
    const a = 1;   
}
  • 在执行到 const 语句前,JavaScript 引擎就已经知道后面的代码将会声明变量 a,从而不允许访问外层作用域中的 a。(暂时性死区)

class 声明

  • 作用于块级作用域,预处理阶段则会屏蔽外部变量。
  • class 默认内部的函数定义都是 strict 模式的。

什么是表达式语句?

表达式语句实际上就是一个表达式,它是由运算符连接变量或者直接量构成的

  • 一般来说,我们的表达式语句要么是函数调用,要么是赋值,要么是自增、自减,否则表达式计算的结果没有任何意义。
a = b, b = 1, null;

逗号分隔的表达式会顺次执行,就像不同的表达式语句一样。“整个表达式的结果”就是“最后一个逗号后的表达式结果”。比如我们文中的例子,整个“a = b, b = 1, null;”表达式的结果就是“,”后面的null。


新加入的**运算符,哪里有些不一样呢?

乘方表达式也是由更新表达式构成的。它使用**号。

++i ** 30
2 ** 30 //正确
-2 ** 30 //报错
  • ** 运算是右结合的,相当于从右往左形成表达式,再进行下一步计算

这跟其它正常的运算符(也就是左结合运算符)都不一样。

4 ** 3 ** 2 //262144

4 ** (3 ** 2) // 262144

(4 ** 3) ** 2 // 4096

移位表达式

<< 向左移位  乘以2的n次方
>> 向右移位  除以 2 取整 n 次
>>> 无符号向右移位

移位运算把操作数看做二进制表示的整数,然后移动特定位数。所以左移 n 位相当于乘以 2 的 n 次方,右移 n 位相当于除以 2 取整 n 次。

  • 在 JavaScript 中,二进制操作整数并不能提高性能,移位运算这里也仅仅作为一种数学运算存在,这些运算存在的意义也仅仅是照顾 C 系语言用户的习惯了。

关系表达式

  • <= 和 >= 关系运算,完全是针对数字的,所以 <= 并不等价于 < 或 ==
  • <=或>=在判断时,是取反后进行判断再取反得到答案的。
1 >= 2 的判断过程
1.判断1<2true
2.return !true
所以得到false

同时,在判断引用数据类型时,==>,<,>=,<=的意义不同
==判断的是引用类型的地址是否相同,
其他四种是先使用valueof方法,如果valueof得到的两边不都是数字,则使用toString方法得到结果后,进行对比

{} == {} //false	两个对象地址不同
{} != {} //true
{} >= {} //true		对比时都是"[object Object]" ,"[object Object]"<"[object Object]"是false,所以结果为true
{} <= {} //true
{} > {} //false		对比时都是"[object Object]" ,"[object Object]">"[object Object]"是false,所以结果为false
{} < {} //false

相等表达式

类型不同的变量比较时==运算只有三条规则:

  • undefined 与 null 相等;
  • 字符串和 bool 都转为数字再比较;
  • 对象转换成 primitive 类型再比较。

尽量用===,仅在确认 == 发生在 Number 和 String 类型之间时使用

位运算表达式 & ^ |

&

  • 按位与表达式由按位与运算符(&)连接按位异或表达式构成,按位与表达式把操作数视为二进制整数,然后把两个操作数按位做与运算。

^

  • 按位异或表达式由按位异或运算符(^)连接按位与表达式构成,按位异或表达式把操作数视为二进制整数,然后把两个操作数按位做异或运算。异或两位相同时得 0,两位不同时得 1。

  • 异或运算有个特征,那就是两次异或运算相当于取消。所以有一个异或运算的小技巧,就是用异或运算来交换两个整数的值。

let a = 102, b = 324;

a = a ^ b;
b = a ^ b;
a = a ^ b;

console.log(a, b); // 324 102

|

  • 按位或表达式由按位或运算符(|)连接相等表达式构成,按位或表达式把操作数视为二进制整数,然后把两个操作数按位做或运算。

逻辑与表达式和逻辑或表达式 && ||

这两种表达式都不会做类型转换,所以尽管是逻辑运算,但是最终的结果可能是其它类型。

false || 1; // 1

true && undefined; // undefined

逻辑表达式具有短路的特性

true || foo(); //后面的foo不会执行

总结:

  • 对于||,前面是false,直接返回后面的,前面是true,就不会执行后面的,直接返回true
  • 对于&&,前面是true,直接返回后面的,前面是false,就不会执行后面的,直接返回true
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值