到底要不要写分号呢?
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<2为true
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