上下文规则

大括号

常见的大括号使用就是对象常量定义

var a = {
    foo: bar()
};

但是还有一种不是很常用的方式,就是标签

{
    foo: bar()
}

很多人以为这种写法是一种对象常量,只是没有赋值给变量,但是事实并不是这样。

{ ... }在这里只是一个普通的代码块。从语法上说这段代码是完全合法的,这里涉及到JavaScript中一个不太为人知的特性:标签语句;foo是bar()的标签,这种标签一般用来配合continue 和 break 语句进行跳出处理。

比如我们有一个三层的for循环,但是break和continue只能跳出一层循环,如果我们在第三层找到了需要的内容就可以通过标签跳出内层的两个循环,实现从内层循环跳转到外层循环。

// 标签为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

break foo 不是指“跳转到标签 foo 所在位置继续执行”,而是“跳出标签foo 所在的循环 / 代码块,继续执行后面的代码”。

标签也能用于非循环代码块,但只有 break 才可以。

// 标签为bar的代码块
function foo() {
    bar: {
        console.log("Hello");
        break bar;
        console.log("never runs");
    }
    console.log("World");
}

foo();
// Hello
// World

需要注意的是,标签的key不能是带引号的,多个值需要用分号分割。

{
    a: 1;
    b: 2
}

代码块

[] + {}; // "[object Object]"
{} + []; // 0

第一条数组转为空字符串+"[object Object]",这个结果毋庸置疑,大家都明白。

但是第二条就很奇怪,这里{}被识别为一个代码块,与后面的+ []其实是分割的,因为代码块结尾不需要分号,所以这里其实是两条语句,{}+ [],空数组转换为字符串,然后+运算符强制类型转换为数字,得到0。

对象解构

es6开始,大括号也可以用于对象的解构。

function getData() {
    // ..
    return {
        a: 42,
        b: "foo"
    };
}

var { a, b } = getData();
console.log(a, b); // 42 "foo"

也可以解构函数的参数

function foo({ a, b, c }) {
    // 不再需要这样:
    // var a = obj.a, b = obj.b, c = obj.c
    console.log(a, b, c);
}

else if和可选代码块

很多人误以为 JavaScript 中有 else if,因为我们可以这样来写代码:

if (a) {
    // ..
} else if (b) {
    // .. 
} else {
    // ..
}

事实上 JavaScript 没有 else if,但 if 和 else 只包含单条语句的时候可以省略代码块的 { }

if (a) doSomething( a );

else也能省略代码块,所以我们经常用到的else if实际上是这样的:

if (a) {
    // ..
} else {
    if (b) {
        // ..
    } else {
        // .. 
    }
}

else if 极为常见,能省掉一层代码缩进,所以很受青睐。但这只是我们自己发明的用法,并不属于JavaScript语法范畴。

自动分号

JavaScript会自动为代码补上缺失的分号,即自动分号插入(Automatic Semicolon Insertion,ASI)

部分代码如果缺失了分号会导致无法运行,ASI使得JavaScript的容错性更好,但是它也不是什么分号都能自动补的。

ASI 只在换行符处起作用,而不会在代码行的中间插入分号

var a = 42, b
c;

此时b后面会自动补上分号,c会处理成一个独立的表达式。如果c不存在,一般就会报Uncaught ReferenceError: c is not defined的错误。

语法规定do..while 循环后面必须带分号,而 while 和 for 循环后则不需要,事实上可能没几个人记得这个,但是代码写了也能跑,此时ASI就会自动补上分号。

其他涉及 ASI 的情况是 break、continue、return 和 yield(ES6)等关键字:

function foo(a) {
    if (!a) return
    a *= 2;
    // ..
}

由于 ASI 会在 return 后面自动加上 ;,所以这里 return 语句并不包括第二行的 a *= 2

return 语句的跨度可以是多行,但是其后必须有换行符以外的代码:

function foo(a) {
    return (
        a * 2 + 3 / 12
    );
}

由于ASI的存在,产生了一波讨论:是否应该完全依赖 ASI 来编码?

我们有时候能在一些项目中看到,他们的规则定义是不加分号,而有的则是必须加分号,不加的表示能省略掉那些不必要的 ;,让代码更简洁。此外,ASI 让许多 ; 变得可有可无,因此只要代码没问题,有没有 ; 都一样。

我个人是倾向于加分号的,因为ASI实际上是一个纠错机制,不加分号从代码最初就可以认为是不可运行的,是错误的代码,只是因为存在ASI使得代码可以被正确解析而已。

如果仅仅是为了追求代码美观和省去一些代码输入,有点得不偿失。

JavaScript 的作者 Brendan Eich 早在 2012 年就说过这样的话:

ASI 是一个语法纠错机制。若将换行符当作有意义的字符来对待,就会遇到很多问题。多希望在 1995 年 5 月的那十天里(ECMAScript 规范制定期间),我让换行符承载了更多的意义。但切勿认为 ASI 真的会将换行符当作有意义的字符。

提前使用变量

ES6 规范定义了一个新概念,叫作 TDZ(Temporal Dead Zone,暂时性死区)

TDZ 指的是由于代码中的变量还没有初始化而不能被引用的情况

{
    a = 2; // ReferenceError
    let a;
}

有意思的是,对未声明变量使用 typeof 不会产生错误(参见第 1 章),但在 TDZ 中却会报错:

{
    typeof a; // undefined
    typeof b; // ReferenceError! (TDZ)
    let b;
}

函数参数

es6对函数参数增加了默认值

var b = 3;

function foo(a = 42, b = a + b + 5) {
    // ..
}

这里b参数其实会报错,当你在运行这个函数的时候:Uncaught ReferenceError: can't access lexical declaration 'b' before initialization

因为TDZ的原因,b参数和等于号后面的b参数,他们其实是同一个,这里就触发了暂时性死区,而访问a却没事,因为a已经声明了。

在 ES6 中,如果参数被省略或者值为 undefined,则取该参数的默认值:

function foo(a = 42, b = a + 1) {
    console.log(a, b);
}

foo(); // 42 43
foo(undefined); // 42 43
foo(5); // 5 6
foo(void 0, 7); // 42 7
foo(null); // null 1

但是省略参数和参数为undefined还是有一些区别的:

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

arguments在非严格模式下,会与函数的参数建立关联(linkage),这样就可以通过 arguments 获取到相同的值,如果不传惨,虽然赋值了默认值,但是不会进行关联,得到的也会是 undefined。

但是在严格模式中没有建立关联:

function foo(a) {
    "use strict";
    a = 42;
    console.log(arguments[0]);
}

foo(2); // 2 (not linked)
foo(); // undefined (not linked)

即便改变了a参数的值, arguments 中的也不会发生变化。

因此,在开发中不要依赖这种关联机制。实际上,它是 JavaScript 语言引擎底层实现的一个抽象泄漏(leaky abstraction),并不是语言本身的特性。

try..finally

finally 中的代码总是会在 try 之后执行,如果有 catch 的话则在 catch 之后执行。也可以将 finally 中的代码看作一个回调函数,即无论出现什么情况最后一定会被调用。

如果try中有return语句,并不会影响finally的运行,但是如果finally里面有return,那么就会覆盖try的return。

如果finally在函数中,且try已经return了,一定会等finally运行完函数才有return。

function foo() {
    try {
        return 42;
    } finally {
        console.log("Hello");
    }
    console.log("never runs");
}

console.log(foo());
// Hello
// 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

finally会在continue之后i++之前运行,所以每次打印的都是i++之前的值。

通常来说,在函数中省略 return 的结果和 return; 及 return undefined; 是一样的,但是在 finally 中省略 return 则会返回前面的 return 设定的返回值。

switch

switch可以看出简化版的if...else...if...else;它的语义更加清晰,如果存在多个else if,一般更推荐使用switch的方式。

switch (a) {
    case 2:
        // 执行一些代码
        break;
    case 42:
        // 执行另外一些代码
        break;
    default:
        // 执行缺省代码
}

这里 a 与 case 表达式逐一进行比较。如果匹配就执行该 case 中的代码,直到 break 或者switch 代码块结束。

虽然看上去没什么问题,我们看一个稍微复杂一点的例子:

var a = "42";

switch (true) {
    case a == 10:
        console.log("10 or '10'");
        break;
    case a == 42;
    console.log("42 or '42'");
    break;
    default:
        // 永远执行不到这里
} 
// 42 or '42'

你可能会以为这里判定生效的是a == 42,实际上真实的判定条件是true === ( a == 42)

虽然case后面可以出现各种表达式,但是最终都是拿表达式的结果与switch 接的参进行全等比较,所以使用的时候还是需要注意的。

最后,default 是可选的,并非必不可少(虽然惯例如此)。break 相关规则对 default 仍然适用:

var a = 10;
switch (a) {
    case 1:
    case 2:
        // 永远执行不到这里
    default:
        console.log("default");
    case 3:
        console.log("3");
        break;
    case 4:
        console.log("4");
} 
// default
// 3

switch会按顺序执行case,如果全部不命中,则走default,但是如果default里面没有break跳出,而后面又有case,会从default往下继续走case。

理论上来说,这种情况在 JavaScript 中是可能出现的,但在实际情况中,开发人员一般不会这样来编码。如果确实需要这样做,就应该仔细斟酌并做好注释。

分类: 你不知道的JavaScript 标签: 语法上下文规则提前使用变量TDZ暂时性死区函数参数函数默认参数try..finallyswitch

评论

暂无评论数据

暂无评论数据

目录