語句和表達(dá)式
開發(fā)人員常常將“語句”和“表達(dá)式”混為一談。
JavaScript中表達(dá)式可以返回一個結(jié)果值:
var a = 3 * 6;
var b = a;
b;
這里仰税,3*6是一個表達(dá)式(結(jié)果為18)图贸。第二行的a也是一個表達(dá)式遮糖,第三行的b也是。表達(dá)式a和b的結(jié)果值都是18速客。
這三行代碼都是包含表達(dá)式的語句戚篙。var a = 3 * 6 和var b = a稱為“聲明語句”,因為它們聲明了變量(還可以為其賦值)溺职。a = 3 * 6 和b = a(不帶var)叫作“賦值表達(dá)式”岔擂。
第三行代碼中只有一個表達(dá)式b,同時它也是一個語句(雖然沒有太大意義)浪耘。這樣的情況通常叫做“表達(dá)式語句”乱灵。
語句的結(jié)果值
很多人不知道,語句都有一個結(jié)果值(undefined也算)七冲。
獲得結(jié)果值最直接的方法是在瀏覽器開發(fā)控制臺中輸入語句阔蛉,默認(rèn)情況下控制臺會顯示所執(zhí)行的最后一條語句的結(jié)果值。
規(guī)范定義var的結(jié)果值是undefined癞埠,如果在控制臺中輸入var a=42會得到結(jié)果值undefined状原,而非42。
但我們在代碼中是沒有辦法獲得這個結(jié)果值的苗踪,具體解決辦法比較復(fù)雜颠区,首先得弄清楚為什么要獲得語句的結(jié)果值。
先來看看其他語句的結(jié)果值:
var b;
if (true) {
b = 4 + 38;
}
在控制臺中輸入以上代碼應(yīng)該會顯示42通铲。換句話說毕莱,代碼塊的結(jié)果值就如同一個隱式的返回,即返回最后一個語句的結(jié)果值颅夺。
但下面這樣的代碼無法運行:
var a, b;
a = if (true) {
b = 4 + 38;
};
因為語法不允許我們獲得語句的結(jié)果值并將其賦值給另一個變量(至少目前不行)朋截。
那應(yīng)該怎樣獲得語句的結(jié)果值呢?
可以使用萬惡的eval(..)來獲得結(jié)果值:
var a, b;
a = eval( "if (true) { b = 4 + 38; }" );
a; // 42
這并不是個好辦法吧黄,但確實管用部服。
ES7規(guī)范有一項“do表達(dá)式”提案,類似下面這樣:
var a, b;
a = do {
if (true) {
b = 4 + 38;
}
};
a; // 42
其目的是將語句當(dāng)做表達(dá)式來處理(語句中可以包含其他語句)拗慨,從而不需要將語句封裝為函數(shù)再調(diào)用return來返回值廓八。
表達(dá)式的副作用
最常見的有副作用(也可能沒有)的表達(dá)式是函數(shù)調(diào)用:
function foo() {
a = a + 1;
}
var a = 1;
foo(); // 結(jié)果值:undefined奉芦。副作用:a的值被改變
var a = 42;
var b = a++;
a; // 43
b; // 42
++在前面時,如++a剧蹂,它的副作用(將a遞增)產(chǎn)生在表達(dá)式返回結(jié)果值之前声功,而a++的副作用則產(chǎn)生在之后。
++a++會產(chǎn)生ReferenceError錯誤宠叼,因為運算符需要將產(chǎn)生的副作用賦值給一個變量先巴。以++a++為例,它首先執(zhí)行a++(根據(jù)運算符優(yōu)先級)冒冬,返回42筹裕,然后執(zhí)行++42,這時會產(chǎn)生ReferenceError錯誤窄驹,因為++無法直接在42這樣的值上產(chǎn)生副作用朝卒。
可以使用,語句系列逗號運算符將多個獨立的表達(dá)式語句串聯(lián)成一個語句:
var a = 42, b;
b = ( a++, a );
a; // 43
b; // 43
a++,a中第二個表達(dá)式a在a++之后執(zhí)行乐埠,結(jié)果為43抗斤,并被賦值給b。
再如delete運算符:
var obj = {
a: 42
};
obj.a; // 42
delete obj.a; // true
obj.a; // undefined
如果操作成功丈咐,delete返回true瑞眼,否則返回false。其副作用是屬性被從對象中刪除(或者單元從array中刪除)棵逊。
上下文規(guī)則
標(biāo)簽
JavaScript可以通過標(biāo)簽跳轉(zhuǎn)能夠?qū)崿F(xiàn)goto的部分功能伤疙。continue和break語句都可以帶一個標(biāo)簽,因此能夠像goto那樣進(jìn)行跳轉(zhuǎn):
// 標(biāo)簽為foo的循環(huán)
foo: for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
// 如果j和i相等辆影,繼續(xù)外層循環(huán)
if (j == i) {
// 跳轉(zhuǎn)到foo的下一個循環(huán)
continue foo;
}
// 跳過奇數(shù)結(jié)果
if ((j * i) % 2 == 1) {
// 繼續(xù)內(nèi)層循環(huán)(沒有標(biāo)簽的)
continue;
}
console.log(i, j);
}
}
// 1 0
// 2 0
// 2 1
// 3 0
// 3 2
continue foo并不是指“跳轉(zhuǎn)到標(biāo)簽foo所在位置繼續(xù)執(zhí)行”徒像,而是“執(zhí)行foo循環(huán)的下一輪循環(huán)”。
帶標(biāo)簽的循環(huán)跳轉(zhuǎn)一個更大的用處在于蛙讥,和break一起使用可以實現(xiàn)從內(nèi)層循環(huán)跳轉(zhuǎn)到外層循環(huán):
// 標(biāo)簽為foo的循環(huán)
foo: for (var i = 0; i < 4; i++) {
for (var j = 0; j < 4; j++) {
if ((i * j) >= 3) {
console.log("stopping!", i, j);
break foo;
}
console.log(i, j);
}
}
// 0 0
// 0 1
// 0 2
// 0 3
// 1 0
// 1 1
// 1 2
// 停止锯蛀! 1 3
break foo不是指“跳轉(zhuǎn)到標(biāo)簽foo所在位置繼續(xù)執(zhí)行”,而是“跳出標(biāo)簽foo所在的循環(huán)/代碼塊次慢,繼續(xù)執(zhí)行后面的代碼”旁涤。因此它并非傳統(tǒng)意義上的goto。
JSON被普遍認(rèn)為是JavaScript語言的一個真子集迫像,{"a":42}
這樣的JSON字符串會被當(dāng)做合法的JavaScript代碼(請注意JSON屬性名必須使用雙引號E蕖)。其實不是闻妓!如果在控制臺中輸入{"a":42}會報錯菌羽。
因為標(biāo)簽不允許使用雙引號,所以“a”并不是一個合法的標(biāo)簽纷闺,因此后面不能帶:算凿。
JSON的確是JavaScript語法的一個子集,但是JSON本身并不是合法的JavaScript語法犁功。
這里存在一個十分常見的誤區(qū)氓轰,即如果通過<script src=..>標(biāo)簽加載JavaScript文件,其中只包含JSON數(shù)據(jù)(比如某個API返回的結(jié)果)浸卦,那它就會被當(dāng)做合法的JavaScript代碼來解析署鸡,只不過其內(nèi)容無法被程序代碼訪問到。JSON-P(將JSON數(shù)據(jù)封裝為函數(shù)調(diào)用限嫌,比如foo({"a":42}))通過將JSON數(shù)據(jù)傳遞給函數(shù)來實現(xiàn)對其的訪問靴庆。
代碼塊
[] + {}; // "[object Object]"
{} + []; // 0
表面上看+運算符根據(jù)第一個操作數(shù)([]或{})的不同會產(chǎn)生不同的結(jié)果,實則不然怒医。
第一行代碼中炉抒,{}出現(xiàn)在+運算符表達(dá)式中,因此它被當(dāng)做一個值(空對象)來處理稚叹。[]會被強制類型轉(zhuǎn)換為""焰薄,而{}會被強制類型轉(zhuǎn)換為“[object Object]”。
但在第二行代碼中扒袖,{}被當(dāng)做一個獨立的空代碼塊(不執(zhí)行任何操作)塞茅。代碼塊結(jié)尾不需要分號,所以這里不存在語法上的問題季率。最后+[]將[]顯示強制類型轉(zhuǎn)換為0野瘦。
對象解構(gòu)
從ES6開始,{..}也可用于“解構(gòu)賦值”飒泻,特別是對象的解構(gòu):
function getData() {
// ..
return {
a: 42,
b: "foo"
};
}
var { a, b } = getData();
console.log(a, b); // 42 "foo"
{a,b}=..就是ES6中的解構(gòu)賦值鞭光,相當(dāng)于下面的代碼:
var res = getData();
var a = res.a;
var b = res.b;
{..}還可以用作函數(shù)命名參數(shù)的對象解構(gòu),方便隱式地用對象屬性賦值:
function foo({ a, b, c }) {
// 不再需要這樣:
// var a = obj.a, b = obj.b, c = obj.c
console.log(a, b, c);
}
foo({
c: [1, 2, 3],
a: 42,
b: "foo"
}); // 42 "foo" [1, 2, 3]
在不同的上下文中{..}的作用不盡相同泞遗,這也是詞法和語法的區(qū)別所在衰猛。
~else if和可選代碼塊~
很多人誤以為JavaScript中有else if,因為我們可以這樣來寫代碼:
if (a) {
// ..
}
else if (b) {
// ..
}
else {
// ..
}
事實上JavaScript沒有else if刹孔,但if和else只包含單條語句的時候可以省略代碼塊的{}啡省。if(b){..}else{..}實際上是跟在else后面的一個單獨的語句,所以帶不帶{}都可以髓霞。換句話說卦睹,else if不符合前面介紹的編程規(guī)范,else中時一個單獨的if語句方库。
運算符優(yōu)先級
var a = 42, b;
b = ( a++, a );
a; // 43
b; // 43
如果去掉()會出現(xiàn)什么情況结序?
var a = 42, b;
b = a++, a;
a; // 43
b; // 42
原因是,運算符的優(yōu)先級比=低纵潦。所以b=a++徐鹤,a其實可以理解為(b=a++),a垃环。前面說過a++有后續(xù)副作用,所以b的值是++對a做遞增之前的值42返敬。
這只是一個簡單的例子遂庄。請務(wù)必記住,用劲赠,來連接一系列語句的時候涛目,它的優(yōu)先級最低,其他操作數(shù)的優(yōu)先級都比它高凛澎。
短路
對&&和||來說霹肝,如果從左邊的操作數(shù)能夠得出結(jié)果,就可以忽略右邊的操作數(shù)塑煎。我們將這種現(xiàn)象稱為“短路”(即執(zhí)行最短路徑)沫换。
“短路”很方便,也很常用:
function doSomething(opts) {
if (opts && opts.cool) {
// ..
}
}
opts&&opts.cool中的opts條件判斷如同一道安全保護(hù)最铁,因為如果opts未賦值(或者不是一個對象)苗沧,表達(dá)式opts.cool會出錯。通過使用短路特性炭晒,opts條件未通過時opts.cool就不會執(zhí)行待逞,也就不會產(chǎn)生錯誤!
||運算符也一樣:
function doSomething(opts) {
if (opts.cache || primeCache()) {
// ..
}
}
這里首先判斷opts.cache是否存在网严,如果是則無需調(diào)用primeCache()函數(shù)识樱,這樣可以避免執(zhí)行不必要的代碼。
更強的綁定
a && b || c ? c || b ? a : c && b : a
執(zhí)行順序是這樣的:
(a && b || c) ? (c || b) ? a : (c && b) : a
因為&&運算符的優(yōu)先級高于||震束,而||的優(yōu)先級又高于?:怜庸。
關(guān)聯(lián)
&&和||運算符先于?:執(zhí)行,那么如果多個相同優(yōu)先級的運算符同時出現(xiàn)垢村,又該如何處理呢割疾?它們的執(zhí)行順序是從左到右還是從右到左?
一般說來嘉栓,運算符的關(guān)聯(lián)不是從左到右就是從右到左宏榕,這取決于組合是從左開始還是從右開始。
請注意:關(guān)聯(lián)和執(zhí)行順序不是一回事侵佃。
但它為什么又和執(zhí)行順序相關(guān)呢麻昼?原因是表達(dá)式可能會產(chǎn)生副作用,比如函數(shù)調(diào)用:
var a = foo() && bar();
這里foo()首先執(zhí)行馋辈,它的返回結(jié)果決定了bar()是否執(zhí)行抚芦。所以如果bar()在foo()之前執(zhí)行,這個結(jié)果會完全不同。
這里遵循從左到右的順序(JavaScript的默認(rèn)執(zhí)行順序)叉抡,與&&的關(guān)聯(lián)無關(guān)尔崔。因為上例中只有一個&&運算符,所以不涉及組合和關(guān)聯(lián)褥民。
而a&&b&&c
這樣的表達(dá)式就涉及組合(隱式)季春,這意味著a&&b或b&&c會先執(zhí)行。
從技術(shù)角度來說轴捎,因為&&運算符是左關(guān)聯(lián)(||也是)鹤盒,所以a&&b&&c
會被處理為(a&&b)&&c
蚕脏。不過右關(guān)聯(lián)a&&(b&&c)
的結(jié)果也一樣侦副。
如果&&是右關(guān)聯(lián)的話會被處理為
a&&(b&&c)
。但這并不意味著c會在b之前執(zhí)行驼鞭。右關(guān)聯(lián)不是指從右往左執(zhí)行秦驯,而是指從右往左組合。任何時候挣棕,不論是組合還是關(guān)聯(lián)译隘,嚴(yán)格的執(zhí)行順序都應(yīng)該是從左到右,a洛心,b固耘,然后c。
?:是右關(guān)聯(lián):
a ? b : c ? d : e;
組合順序如下:
a ? b : (c ? d : e)词身。
掌握了優(yōu)先級和關(guān)聯(lián)等相關(guān)知識之后厅目,就能夠根據(jù)組合規(guī)則將以下的復(fù)雜代碼分解如下:
a && b || c ? c || b ? a : c && b : a;
組合順序:
((a && b) || c) ? ((c || b) ? a : (c && b)) : a
錯誤
JavaScript不僅有各種類型的運行時錯誤(TypeError、ReferenceError法严、SyntaxError等)损敷,它的語法中也定義了一些編譯時錯誤。
在編譯階段發(fā)現(xiàn)的代碼錯誤叫做“早期錯誤”深啤。語法錯誤是早起錯誤的一種(如a=,)拗馒。另外,語法正確但不符合語法規(guī)則的情況也存在溯街。
正則表達(dá)式常量中的語法诱桂。這里JavaScript語法沒有問題,但非法的正則表達(dá)式也會產(chǎn)生早期錯誤:
var a = /+foo/; // 錯誤呈昔!
語法規(guī)定賦值對象必須是一個標(biāo)識符访诱,因此下面的42會報錯:
var a;
42 = a; // 錯誤!
ES5規(guī)范的嚴(yán)格模式定義了很多早期錯誤韩肝。比如在嚴(yán)格模式中触菜,函數(shù)的參數(shù)不能重名:
function foo(a,b,a) { } // 沒問題
function bar(a,b,a) { "use strict"; } // 錯誤!
再如哀峻,對象常量不能包含多個同名屬性:
(function () {
"use strict";
var a = {
b: 42,
b: 43
}; // 錯誤涡相!
})();
從語義角度來說哲泊,這些錯誤并非詞法錯誤,而是語法錯誤催蝗,因為它們在詞法上是正確的切威。只不過由于沒有GrammarError類型,一些瀏覽器選擇用SyntaxError來代替丙号。
提前使用變量
ES6規(guī)范定義了一個新概念先朦,叫做TDZ(暫時性死區(qū))。
TDZ指的是由于代碼中的變量還沒有初始化而不能被引用的情況犬缨。
對此喳魏,最直觀的例子是ES6規(guī)范中的let塊作用域:
{
a = 2; // ReferenceError!
let a;
}
a=2試圖在let a初始化a之前使用該變量(其作用域在{..}內(nèi)),這里就是a的TDZ怀薛,會產(chǎn)生錯誤刺彩。
有意思的是,對未聲明變量使用typeof不會產(chǎn)生錯誤枝恋,但在TDZ中卻會報錯:
{
typeof a; // undefined
typeof b; // ReferenceError! (TDZ)
let b;
}
函數(shù)參數(shù)
另一個TDZ違規(guī)的例子是ES6中的參數(shù)默認(rèn)值:
var b = 3;
function foo( a = 42, b = a + b + 5 ) {
// ..
}
b=a+b+5
在參數(shù)b(=右邊的b创倔,而不是函數(shù)外的那個)的TDZ中訪問b,所以會出錯焚碌。而訪問a卻沒有問題畦攘,因為此時剛好跨出了參數(shù)a的TDZ。
在ES6中十电,如果參數(shù)被省略或者值為undefined知押,則取該參數(shù)的默認(rèn)值:
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
對ES6中的參數(shù)默認(rèn)值而言,參數(shù)被省略或被復(fù)制為undefined效果都一樣摆出,都是取該參數(shù)的默認(rèn)值朗徊。然而某些情況下,它們之間還是有區(qū)別的:
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
try..finally
finally中的代碼總是會在try之后執(zhí)行偎漫,如果有catch的話則在catch之后執(zhí)行爷恳。也可以將finally中的代碼看做一個回調(diào)函數(shù),即無論出現(xiàn)什么情況最后一定會被調(diào)用象踊。
如果try中有return語句會出現(xiàn)什么情況呢温亲?return會返回一個值,那么調(diào)用該函數(shù)并得到返回值的代碼是在finally之前還是之后執(zhí)行呢杯矩?
function foo() {
try {
return 42;
}
finally {
console.log("Hello");
}
console.log("never runs");
}
console.log(foo());
// Hello
// 42
這里return 42先執(zhí)行栈虚,并將foo()函數(shù)的返回值設(shè)置為42。然后try執(zhí)行完畢史隆,接著執(zhí)行finally魂务。最后foo()函數(shù)執(zhí)行完畢,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中拋出異常(無論是有意還是無意)粘姜,函數(shù)就會在此處終止鬓照。如果此前try中已經(jīng)有return設(shè)置了返回值,則該值會被丟棄:
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在每次循環(huán)之后孤紧,會在i++執(zhí)行之后執(zhí)行console.log(i)豺裆,所以結(jié)果是0..9而非1..10。
ES6中新加入了yield号显,可以將其視為return的中間版本臭猜。然而與return不同的是,yield在generator重新開始時才結(jié)束押蚤,這意味著try{..yield..}并未結(jié)束蔑歌,因此finally不會在yield之后立即執(zhí)行。
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
通常來說俐芯,在函數(shù)中省略return的結(jié)果和return;及return undefined;是一樣的霎肯,但是在finally中省略return則會返回前面的return設(shè)定的返回值。
事實上匣砖,還可以將finally和帶標(biāo)簽的break混合使用:
function foo() {
bar: {
try {
return 42;
}
finally {
// 跳出標(biāo)簽為bar的代碼塊
break bar;
}
}
console.log("Crazy");
return "Hello";
}
console.log(foo());
// Crazy
// Hello
但切勿這樣操作偎肃。利用finally加帶標(biāo)簽的break來跳過return只會讓代碼變得晦澀難懂煞烫,即使加上注釋也是如此。
switch
有時可能會需要通過強制類型轉(zhuǎn)換來進(jìn)行相等比較(即==)累颂,這時就需要做一些特殊處理:
var a = "42";
switch (true) {
case a == 10:
console.log("10 or '10'");
break;
case a == 42;
console.log("42 or '42'");
break;
default:
// 永遠(yuǎn)執(zhí)行不到這里
}
// 42 or '42'
除簡單值以外滞详,case中還可以出現(xiàn)各種表達(dá)式,它會將表達(dá)式的結(jié)果值和true進(jìn)行比較紊馏。因為a==42的結(jié)果為true料饥,所以條件成立。
盡管可以使用==朱监,但switch中true和true之間仍然是嚴(yán)格相等比較岸啡。即如果case表達(dá)式的結(jié)果為真值,但不是嚴(yán)格意義上的true赫编,則條件不成立巡蘸。所以,在這里使用||和&&等邏輯運算符就很容易掉進(jìn)坑里:
var a = "hello world";
var b = 10;
switch (true) {
case (a || b == 10):
// 永遠(yuǎn)執(zhí)行不到這里
break;
default:
console.log("Oops");
}
// Oops
因為(a||b==10)
的結(jié)果是“hello world”而非true擂送,所以嚴(yán)格相等比較不成立悦荒。此時可以通過強制表達(dá)式返回true或false,如case !!(a || b == 10)
嘹吨。