作为一名前端小白,不知道大家是否遇到和我一样的问题。看了一道面试题的解析,当时觉得会了,可是过两天以后再看又不会了;盲目追求各种新技术,感觉什么都会点,但是一上手就不行了... 痛定思痛后,我终于认识到了问题所在,开始专注于基本功的修炼。近半年来通读了(其实是囫囵吞枣)《JavaScript高级程序设计》、《你不知道的JavaScript上、中、下》等书籍,本系列文章是我读书过程中对知识点的一些总结。喜欢的同学记得帮我点个赞😁。
懂王系列(一)之彻底搞懂JavaScript函数执行机制
懂王系列(二)之彻底搞懂JavaScript作用域
懂王系列(三)之彻底搞懂JavaScript对象
懂王系列(四)之彻底搞懂JavaScript类
懂王系列(五)之彻底搞懂JavaScript原型
懂王系列(六)之彻底搞懂JavaScript中的this
懂王系列(七)之彻底搞懂JavaScript数据类型
懂王系列(八)之彻底搞懂JavaScript语句
懂王系列(九)之彻底搞定JavaScript类型转换
1. 语句和表达式
执行特定任务的一组单词、数字和运算符被称为语句。
a = b * 2;
字符 a 和 b 称为变量,2本身就是一个值,称为字面值,= 和 * 是运算符
一个表达式是对一个变量或值的引用,或者是一组值和变量与运算符的组合,a = b * 2; 这个语句中有四个表达式
• 2 是一个字面值表达式。
• b 是一个变量表达式,表示获取它的当前值。
•b * 2是一个算术表达式,表示进行乘法运算。
•a = b * 2是一个赋值表达式,意思是将表达式 b * 2 的结果赋值给变量 a
JavaScript 中表达式可以返回一个结果值。例如:
var a = 3 * 6;
var b = a;
b;
这里,3 * 6 是一个表达式(结果为 18)。第二行的 a 也是一个表达式,第三行的 b 也是。表达式 a 和 b 的结果值都是 18。
这三行代码都是包含表达式的语句。var a = 3 * 6 和 var b = a 称为“声明语句”(declaration statement),因为它们声明了变量(还可以为其赋值)。a = 3 * 6 和 b = a(不带 var)叫作“赋值表达式”。
第三行代码中只有一个表达式 b,同时它也是一个语句(虽然没有太大意义)。这样的情况通常叫作“表达式语句”(expression statement)。
1.1 语句的结果值
语句都有一个结果值(statement completion value),undefined 也算
如果在控制台中输入 var a = 42 会得到结果值 undefined,而非 42。
var b;
if (true) {
b = 4 + 38;
}
在控制台 /REPL 中输入以上代码应该会显示 42,即最后一个语句 / 表达式 b = 4 + 38 的结值。
换句话说,代码块的结果值就如同一个隐式的返回,即返回最后一个语句的结果值。
但下面这样的代码无法运行:
var a, b;
a = if (true) {
b = 4 + 38;
};
因为语法不允许我们获得语句的结果值并将其赋值给另一个变量(至少目前不行)。
可以使用万恶的 eval(..)(又读作“evil”)来获得结果值:
var a, b;
a = eval( "if (true) { b = 4 + 38; }" );
a; // 42
ES7 规范有一项 do 表达式(do expression)提案,类似下面这样:
var a, b;
a = do {
if (true) {
b = 4 + 38;
}
};
a; // 42
上例中,do { .. } 表达式执行一个代码块(包含一个或多个语句),并且返回其中最后一个语句的结果值,然后赋值给变量 a
1.2 表达式的副作用
常有人误以为可以用括号 () 将 a++ 的副作用封装起来,例如:
var a = 42;
var b = (a++);
a; // 43
b; // 42
() 本身并不是一个封装表达式,不会在表达式 a++ 产生副作用之后执行。即便可以,a++ 会首先返回 42,除非有表达式在 ++ 之后再次对 a 进行运算,否则还是不会得到 43,也就不能将 43 赋值给 b
可以使用 , 语句系列逗号运算符(statement-series comma operator)将多个独立的表达式语句串联成一个语句:
var a = 42, b;
b = (a++, a);
a; // 43
b; // 43
a++, a 中第二个表达式 a 在 a++ 之后执行,结果为 43,并被赋值给 b。
delete 用来删除对象中的属性和数组中的单元。它通常以单独一个语句的形式出现:
var obj = {
a: 42
};
obj.a; // 42
delete obj.a; // true
obj.a; // undefined
如果操作成功,delete 返回 true,否则返回 false。其副作用是属性被从对象中删除(或者单元从 array 中删除)。
var a;
a = 42; // 42
a; // 42
a = 42 中的 = 运算符看起来没有副作用,实际上它的结果值是 42,它的副作用是将 42 赋值给 a。
var a, b, c;
a = b = c = 42;
这里 c = 42 的结果值为 42(副作用是将 c 赋值 42),然后 b = 42 的结果值为 42(副作用是将 b 赋值 42),最后是 a = 42(副作用是将 a 赋值 42)。
function vowels(str) {
var matches;
if (str) {
// 提取所有元音字母
matches = str.match( /[aeiou]/g );
if (matches) {
return matches;
}
}
}
vowels( "Hello World" ); // ["e","o","o"]
上面的代码没问题,很多人也喜欢这样做。其实我们可以利用赋值语句的副作用将两个 if 语句合二为一:
function vowels(str) {
var matches;
// 提取所有元音字母
if (str && (matches = str.match( /[aeiou]/g ))) {
return matches;
}
}
vowels( "Hello World" ); // ["e","o","o"]
1.3 上下文规则
1.3.1 大括号
{
foo: bar()
}
很多人以为这里的 { .. } 只是一个孤立的对象常量,没有赋值。事实上不是这样。{ .. } 在这里只是一个普通的代码块。foo 是语句 bar() 的标签
// 标签为foo的循环
foo: for (var i=0; i<4; i++) {
for (var j=0; j<4; j++) {
// 如果j和i相等,继续外层循环
if (j == i) {
// 跳转到foo的下一个循环
continue foo;
}
// 跳过奇数结果
if ((j * i) % 2 == 1) {
// 继续内层循环(没有标签的)
continue;
}
console.log( i, j );
}
}
// 1 0
// 2 0
// 2 1
// 3 0
// 3 2
contine foo 并不是指“跳转到标签 foo 所在位置继续执行”,而是“执行 foo 循环的下一轮循环”。所以这里的 foo 并非 goto。
1.3.2 代码块
[] + {}; // "[object Object]"
{} + []; // 0
第一行代码中,{} 出现在 + 运算符表达式中,因此它被当作一个值(空对象)来处理。 [] 会被强制类型转换为 "",而 {} 会被强制类型转换为 [object Object]。
但在第二行代码中,{} 被当作一个独立的空代码块(不执行任何操作)。代码块结尾不需要分号,所以这里不存在语法上的问题。最后 + [] 将 [] 显式强制类型转换为 0。
1.3.2 else if
事实上js没有else if,我们经常用到的 else if 实际上是这样的:
if (a) {
// ..
}
else {
if (b) {
// ..
}
else {
// ..
}
}
if (b) { .. } else { .. } 实际上是跟在 else 后面的一个单独的语句,所以带不带 { } 都可以。换句话说,else if 不符合前面介绍的编码规范,else 中是一个单独的 if 语句。
2. 运算符优先级
&& 运算符先于 || 执行
2.1 短路
以 a && b 为例,如果 a 是一个假值,足以决定 && 的结果,就没有必要再判断 b 的值。同样对于 a || b,如果 a 是一个真值,也足以决定 || 的结果,也就没有必要再判断 b 的值。
2.2 更强的绑定
a && b || c ? c || b ? a : c && b : a
(a && b || c) ? (c || b) ? a : (c && b) : a
因为 && 运算符的优先级高于 ||,而 || 的优先级又高于 ? :
2.3 关联
关联和执行顺序不是一回事。
但它为什么又和执行顺序相关呢?原因是表达式可能会产生副作用,比如函数调用:
var a = foo() && bar();
这里 foo() 首先执行,它的返回结果决定了 bar() 是否执行。所以如果 bar() 在 foo() 之前执行,整个结果会完全不同。
这里遵循从左到右的顺序(JavaScript 的默认执行顺序),与 && 的关联无关。因为上例中只有一个 && 运算符,所以不涉及组合和关联。
而 a && b && c 这样的表达式就涉及组合(隐式),这意味着 a && b 或 b && c 会先执行。
从技术角度来说,因为 && 运算符是左关联(|| 也是),所以 a && b && c 会被处理为 (a && b) && c。不过右关联 a && (b && c) 的结果也一样。
如果 && 是右关联的话会被处理为 a && (b && c)。但这并不意味着 c 会在 b 之前执行。右关联不是指从右往左执行,而是指从右往左组合。任何时候,不论是组合还是关联,严格的执行顺序都应该是从左到右,a,b,然后 c。
如 ? :(即三元运算符或者条件运算符):
a ? b : c ? d : e;
答案是 a ? b : (c ? d : e)。和 && 以及 || 运算符不同,右关联在这里会影响返回结果,因为 (a ? b : c) ? d : e 对有些值(并非所有值)的处理方式会有所不同。
举个例子:
true ? false : true ? true : true; // false
true ? false : (true ? true : true); // false
(true ? false : true) ? true : true; // true
另一个右关联(组合)的例子是 = 运算符。本章前面介绍过一个串联赋值的例子:
var a, b, c;
a = b = c = 42;
它首先执行 c = 42,然后是 b = ..,最后是 a = ..。因为是右关联,所以它实际上是这样来处理的:a = (b = (c = 42))。
2.3 错误
在编译阶段发现的代码错误叫作“早期错误”(early error)。语法错误是早期错误的一种(如 a = ,)。另外,语法正确但不符合语法规则的情况也存在。
这些错误在代码执行之前是无法用 try..catch 来捕获的,相反,它们还会导致解析 / 编译失败。
2.4 函数参数
ES6 规范定义了一个新概念,叫作 TDZ(Temporal Dead Zone,暂时性死区)。
TDZ 指的是由于代码中的变量还没有初始化而不能被引用的情况。
对此,最直观的例子是 ES6 规范中的 let 块作用域:
{
a = 2; // ReferenceError!
let a;
}
a = 2 试图在 let a 初始化 a 之前使用该变量(其作用域在 { .. } 内),这里就是 a 的TDZ,会产生错误。
有意思的是,对未声明变量使用 typeof 不会产生错误,但在 TDZ 中却会报错:
{
typeof a; // undefined
typeof b; // ReferenceError! (TDZ)
let b;
}
var b = 3;
function foo( a = 42, b = a + b + 5 ) {
// ..
}
b = a + b + 5 在参数 b(= 右边的 b,而不是函数外的那个)的 TDZ 中访问 b,所以会出错。而访问 a 却没有问题,因为此时刚好跨出了参数 a 的 TDZ。
function foo( a = 42, b = a + 1 ) {
console.log(
arguments.length, a, b,
arguments[0], arguments[1]
);
}
foo(); // 0 42 43 undefined undefined
foo( 10 ); // 1 10 11 10 undefined
foo( 10, undefined ); // 2 10 11 10 undefined
foo( 10, null ); // 2 10 null 10 null
虽然参数 a 和 b 都有默认值,但是函数不带参数时,arguments 数组为空。
相反,如果向函数传递 undefined 值,则 arguments 数组中会出现一个值为 undefined 的单元,而不是默认值
ES6 参数默认值会导致 arguments 数组和相对应的命名参数之间出现偏差,ES5 也会出现这种情况:
function foo(a) {
a = 42;
console.log(arguments[0]);
}
foo(2); // 42 (linked)
foo(); // undefined (not linked)
向函数传递参数时,arguments 数组中的对应单元会和命名参数建立关联(linkage)以得到相同的值。相反,不传递参数就不会建立关联。但是,在严格模式中并没有建立关联这一说:
function foo(a) {
"use strict";
a = 42;
console.log(arguments[0]);
}
foo(2); // 2 (not linked)
foo(); // undefined (not linked)
2.5 try...finally
function foo() {
try {
return 42;
}
finally {
console.log( "Hello" );
}
console.log( "never runs" );
}
console.log( foo() );
// Hello
// 42
这里 return 42 先执行,并将 foo() 函数的返回值设置为 42。然后 try 执行完毕,接着执行 finally。最后 foo() 函数执行完毕,console.log(..) 显示返回值。
try 中的 throw 也是如此:
function foo() {
try {
throw 42;
}
finally {
console.log( "Hello" );
}
console.log( "never runs" );
}
console.log( foo() );
// Hello
// Uncaught Exception: 42
如果 finally 中抛出异常(无论是有意还是无意),函数就会在此处终止。如果此前 try 中已经有 return 设置了返回值,则该值会被丢弃:
function foo() {
try {
return 42;
}
finally {
throw "Oops!";
}
console.log( "never runs" );
}
console.log( foo() );
// Uncaught Exception: Oops!
continue 和 break 等控制语句也是如此:
for (var i=0; i<10; i++) {
try {
continue;
}
finally {
console.log( i );
}
}
// 0 1 2 3 4 5 6 7 8 9
continue 在每次循环之后,会在 i++ 执行之前执行 console.log(i),所以结果是 0..9 而非1..10。
finally 中的 return 会覆盖 try 和 catch 中 return 的返回值
function foo() {
try {
return 42;
}
finally {
// 没有返回语句,所以没有覆盖
}
}
function bar() {
try {
return 42;
}
finally {
// 覆盖前面的 return 42
return;
}
}
function baz() {
try {
return 42;
}
finally {
// 覆盖前面的 return 42
return "Hello";
}
}
foo(); // 42
bar(); // undefined
baz(); // Hello
通常来说,在函数中省略 return 的结果和 return; 及 return undefined; 是一样的,但是
在 finally 中省略 return 则会返回前面的 return 设定的返回值。
2.6 switch
switch (a) {
case 2:
// 执行一些代码
break;
case 42:
// 执行另外一些代码
break;
default:
// 执行缺省代码
}
a 和 case 表达式的匹配算法与 === 相同。通常 case 语句中的 switch 都是简单值
var a = "hello world";
var b = 10;
switch (true) {
case (a || b == 10):
// 永远执行不到这里
break;
default:
console.log( "Oops" );
}
// Oops
因为 (a || b == 10) 的结果是 "hello world" 而非 true,所以严格相等比较不成立。此时可以通过强制表达式返回 true 或 false,如 case !!(a || b == 10):