1.语句和表达式
var a = 3 * 6;
var b = a;
b;
这里,3 * 6
是一个表达式(结果为18)。第二行的a也是一个表达式,第三行的b也是。表达式a和b的结果值都是18。
var a = 3 * 6
和var b = a
称为声明语句,因为它们声明了变量。
a = 3 * 6
和b = a
叫作赋值表达式。
第三行代码中只有一个表达式b,同时它也是一个语句。这样的情况通常叫作表达式语句。
1.1 表达式的副作用
最常见的有副作用的表达式是函数调用。
function foo(){
a = a + 1;
}
var a = 1;
foo();//副作用:a的值被改变
其他一些表达式也有副作用:
var a = 42;
var b = a++;
a;//43
b;//42
a++
首先返回变量a的当前值42(再将该值赋给b),然后将a的值加1。
var a = 42;
var b = (a++,a);
a;//43
b;//43
a++,a
中第二个表达式a在a++
之后执行,结果为43,并被赋值给b。
1.2 上下文规则
大括号
var a = {
foo: bar()
};
{...}
被赋值给a,因而它是一个对象常量。
foo: bar()
中的foo是语句bar()
的标签。foo: bar()
是标签语句。
JSON是JavaScript语法的一个子集,但是JSON本身并不是合法的JavaScript语法。
JSON属性名必须使用双引号,而标签不允许使用双引号。在控制台中输入{"a":42}
会报错,因为它会被当作一个带有非法标签的语句块来执行。
JSON-P(将JSON数据封装为函数调用,比如foo({"a",42})
)通过将JSON数据传递给函数来实现对其的访问。
代码块
[] + {};//"[object Object]"
{} + [];//0
第一行代码中,{}
出现在+
运算符表达式中,因此它被当作一个值(空对象)来处理。[]
会被强制类型转换为""
,而{}
会被强制类型转换为"[object Object]"
。
第二行代码中,{}
被当作一个独立的空代码块。代码块结尾不需要分号。最后+ []
将[]
显式强制类型转换为0
。
对象解构
从ES6开始,{..}
也可用于“解构赋值”,特别是对象的解构。
function getData(){
return {
a: 42,
b: "foo"
};
}
var {a,b} = getData();
console.log(a,b);//42 "foo"
{a, b}
实际上是{a: a, b: b}
的简化版本。
{..}
还可以用作函数命名参数的对象解构,方便隐式子地用对象属性赋值。
function foo({a,b,c}){
console.log(a,b,c);
}
foo({
c: [1,2,3],
a: 42,
b: "foo"
});//42 "foo" [1,2,3]
else if
我们可以这样来写代码:
if (a){
//...
}
else if (b){
//...
}
else{
//...
}
其实JavaScript没有else if
,上面只是省略代码块的{}
。我们经常用到的else if
实际上是这样的。
if(a){
//...
}
else{
if(b){
//...
}
else{
//...
}
}
2.运算符优先级
var a = 42,b;
b = (a++,a);
console.log(a);//43
console.log(b);//43
/*
var a = 42,b;
b = a++,a;
console.log(a);//43
console.log(b);//42
//b = a++,a可以理解为(b = a++),a。
*/
请务必记住,用,
来连接一系列语句的时候,它的优先级最低,其他操作数的优先级都比它高。
2.1 短路
对&&
和||
来说,如果从左边的操作数能够得出结果,就可以忽略右边的操作数。我们将这种现象称为“短路”。
function doSomething(opts){
//如果opts未赋值,opst.cool就不会执行。
if(opts && opts.cool){
//...
}
}
function doSomething(opts){
//如果opts.cache存在,就无需调用primeCache()。
if(opts.cache && primeCache()){
//...
}
}
&&
运算符的优先级高于||
,而||
的优先级又高于? :
。
a && b || c ? c || b ? a : c && b : a
//等价于(a && b || c) ? (c || b) ? a : (c && b) : a
2.2 关联
如果多个相同优先级的运算符同时出现,又该如何处理呢?
一般来说,运算符的关联(associativity)不是从左到右就是从右到左,这取决于组合(grouping)是从左开始还是从右开始。
请注意:关联和执行顺序不是一回事,原因是表达式可能会产生副作用,比如函数调用:
var a = foo() && bar();
这里foo()
首先执行,它的返回结果决定了bar()
是否执行。
&&
和||
运算符是左关联,而? :
运算符是右关联。
=
运算符是右关联:
var a,b,c;
a = b = c = 42;
a = b = c = 42
实际上是这样来处理的:a = (b = (c = 42))
。
建议:如果运算符优先级/关联规则能够令代码更为简洁,就使用运算符优先级/关联规则,而如果()
有助于提高代码可读性,就使用()
。
3.自动分号
有时JavaScript会自动为代码行补上缺失的分号,即自动分号插入(Automatic SemicolonInsertion,ASI)。
ASI只在换行符处起作用,而不会再代码行的中间插入分号。
如果JavaScript解析器发现代码行可能因为缺失分号而导致错误,那么它就会自动补上分号。并且,只有在代码行末尾与换行符之间除了空格和注释之外没有别的内容时,它才会这样做。
ASI的目的在于提高解析器的容错性。
解析器报错就意味着代码有问题,对ASI来说,解析器报错的唯一原因是代码中缺失了必要的分号。
我建议在所有需要的地方加上分号,将对ASI的依赖降到最低。
4.错误
JavaScript不仅有各种类型的运行时错误(TypeError、ReferenceError、SyntaxError等),它的语法中也定义了一些编译时错误。
5.函数参数
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
/*
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
*/
6.try...finally
finally
中的代码总是会在try之后执行,如果有catch的话则在catch之后执行。也可以将finally中的代码看作一个回调函数,即无论出现什么情况最后一定会被调用。
function foo(){
try{
return 42;
}finally{
console.log("Hello");
}
console.log("never runs");
}
console.log(foo());
//Hello
//42
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!
finally
中的return会覆盖try和catch中return的返回值:
function foo(){
try{
return 42;
}finally{
//...
}
}
function bar(){
try{
return 42;
}finally{
return;
}
}
function baz(){
try{
return 42;
}finally{
return "Hello";
}
}
foo();//42
bar();//undefined
baz();//Hello
7.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'
除简单值以外,case中还可以出现各种表达式,它会将表达式的结果值和true进行比较(这里是严格相等比较)。
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)
。
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
上面的代码是这样执行的,首先遍历并找到所有匹配的case,如果没有匹配则执行default中的代码。因为其中没有break,所以继续执行已经遍历过的case 3
代码块。直到break为止。
理论上来说,这种情况在JavaScript中是可能出现的,但在实际情况中,开发人员一般不会这样写编码。如果确实需要这样做,就应该仔细斟酌并做好注释。