什么是 JavaScript 語言?
JavaScript 是一種輕量級的腳本語言。所謂“腳本語言”(script language
),指的是它不具備開發操作系統的能力,而是只用來編寫控制其他大型應用程序(比如瀏覽器)的“腳本”。
JavaScript 也是一種嵌入式語言。它本身提供的核心語法不算很多,只能用來做一些數學和邏輯運算。JavaScript 本身不提供任何與 I/O(輸入/輸出)相關的 API,都要靠宿主環境提供,所以 JavaScript 只合適嵌入更大型的應用程序環境,去調用宿主環境提供的底層 API。
目前,已經嵌入 JavaScript 的宿主環境有多種,最常見的環境就是瀏覽器,另外還有服務器環境,也就是 Node 項目。
從語法角度看,JavaScript 語言是一種“對象模型”語言。各種宿主環境通過這個模型,描述自己的功能和操作接口,從而通過 JavaScript 控制這些功能。但是,JavaScript 并不是純粹的“面向對象語言”,還支持其他編程范式(比如函數式編程)。這導致幾乎任何一個問題,JavaScript 都有多種解決方法。
JavaScript 的核心語法部分相當精簡,只包括兩個部分:基本的語法構造(比如操作符、控制結構、語句)和標準庫(就是一系列具有各種功能的對象比如Array
、Date
、Math
等)。除此之外,各種宿主環境提供額外的 API(即只能在該環境使用的接口),以便 JavaScript 調用。以瀏覽器為例,它提供的額外 API 可以分成三大類。
- 瀏覽器控制類:操作瀏覽器
- DOM 類:操作網頁的各種元素
- Web 類:實現互聯網的各種功能
如果宿主環境是服務器,則會提供各種操作系統的 API,比如文件操作 API、網絡通信 API等等。這些都可以在 Node 環境中找到。
JavaScript 的基本語法
語句
JavaScript 程序的執行單位為行,也就是一行一行地執行。一般情況下,每一行就是一個語句。
語句(statement
)是為了完成某種任務而進行的操作,比如下面就是一行賦值語句。
var a = 1 + 3;
這條語句先用var
命令,聲明了變量a
,然后將1 + 3
的運算結果賦值給變量a
。
1 + 3
叫做表達式(expression
),指一個為了得到返回值的計算式。語句和表達式的區別在于,前者主要為了進行某種操作,一般情況下不需要返回值;后者則是為了得到返回值,一定會返回一個值。凡是 JavaScript 語言中預期為值的地方,都可以使用表達式。比如,賦值語句的等號右邊,預期是一個值,因此可以放置各種表達式。
語句以分號結尾,一個分號就表示一個語句結束。多個語句可以寫在一行內。
var a = 1 + 3 ; var b = 'abc';
分號前面可以沒有任何內容,JavaScript引擎將其視為空語句。
;;;
上面的代碼就表示3個空語句。
表達式不需要分號結尾。一旦在表達式后面添加分號,則JavaScript引擎就將表達式視為語句,這樣會產生一些沒有任何意義的語句。
1 + 3;
'abc';
上面兩行語句只是單純地產生一個值,并沒有任何實際的意義。
變量
概念
變量是對“值”的具名引用。變量就是為“值”起名,然后引用這個名字,就等同于引用這個值。變量的名字就是變量名。
var a = 1;
上面的代碼先聲明變量a
,然后在變量a
與數值1之間建立引用關系,稱為將數值1“賦值”給變量a
。以后,引用變量名a
就會得到數值1。最前面的var
,是變量聲明命令。它表示通知解釋引擎,要創建一個變量a
。
注意,JavaScript 的變量名區分大小寫,A
和a
是兩個不同的變量。
變量的聲明和賦值,是分開的兩個步驟,上面的代碼將它們合在了一起,實際的步驟是下面這樣。
var a;
a = 1;
如果只是聲明變量而沒有賦值,則該變量的值是undefined
。undefined
是一個特殊的值,表示“無定義”。
var a;
a // undefined
如果變量賦值的時候,忘了寫var
命令,這條語句也是有效的。
var a = 1;
// 基本等同
a = 1;
但是,不寫var
的做法,不利于表達意圖,而且容易不知不覺地創建全局變量,所以建議總是使用var
命令聲明變量。
如果一個變量沒有聲明就直接使用,JavaScript會報錯,告訴你變量未定義。
x
// ReferenceError: x is not defined
上面代碼直接使用變量x
,系統就報錯,告訴你變量x
沒有聲明。
可以在同一條var
命令中聲明多個變量。
var a, b;
JavaScript是一種動態類型語言,也就是說,變量的類型沒有限制,變量可以隨時更改類型。
var a = 1;
a = 'hello';
上面代碼中,變量a
起先被賦值為一個數值,后來又被重新賦值為一個字符串。第二次賦值的時候,因為變量a
已經存在,所以不需要使用var
命令。
如果使用var
重新聲明一個已經存在的變量,是無效的。
var x = 1;
var x;
x // 1
但是,如果第二次聲明的時候還進行了賦值,則會覆蓋掉前面的值。
var x = 1;
var x = 2;
// 等同于
var x = 1;
var x;
x = 2;
變量提升
JavaScript引擎的工作方式是,先解析代碼,獲取所有被聲明的變量,然后再一行一行地運行。這造成的結果,就是所有的變量的聲明語句,都會被提升到代碼的頭部,這就叫做變量提升(hoisting
)。
console.log(a);
var a = 1;
上面代碼首先使用console.log
方法,在控制臺顯示變量a
的值。這時變量a
還沒有聲明和賦值,所以這是一種錯誤的做法,但是實際上不會報錯。因為存在變量提升,真正運行的是下面的代碼。
var a;
console.log(a);
a = 1;
最后的結果是顯示undefined
,表示變量a
已聲明,但還未賦值。
標識符
標識符(identifier
)指的是用來識別各種值的合法名稱。最常見的標識符就是變量名,以及后面要提到的函數名。JavaScript語言的標識符對大小寫敏感,所以a
和A
是兩個不同的標識符。
標識符有一套命名規則,不符合規則的就是非法標識符。JavaScript引擎遇到非法標識符,就會報錯。
簡單說,標識符命名規則如下。
- 第一個字符,可以是任意Unicode字母(包括英文字母和其他語言的字母),以及美元符號(
$
)和下劃線(_
)。 - 第二個字符及后面的字符,除了Unicode字母、美元符號和下劃線,還可以用數字
0-9
。
下面這些都是合法的標識符。
arg0
_tmp
$elem
π
下面這些則是不合法的標識符。
1a // 第一個字符不能是數字
23 // 同上
*** // 標識符不能包含星號
a+b // 標識符不能包含加號
-d // 標識符不能包含減號或連詞線
中文是合法的標識符,可以用作變量名。
var 臨時變量 = 1;
JavaScript有一些保留字,不能用作標識符:
arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield
。
注釋
源碼中被JavaScript引擎忽略的部分就叫做注釋,它的作用是對代碼進行解釋。JavaScript 提供兩種注釋的寫法:一種是單行注釋,用//
起頭;另一種是多行注釋,放在/*
和*/
之間。
// 這是單行注釋
/*
這是
多行
注釋
*/
此外,由于歷史上 JavaScript 可以兼容 HTML 代碼的注釋,所以``也被視為合法的單行注釋。
x = 1; <!-- x = 2;
--> x = 3;
上面代碼中,只有x = 1
會執行,其他的部分都被注釋掉了。
需要注意的是,-->
只有在行首,才會被當成單行注釋,否則會當作正常的運算。
function countdown(n) {
while (n --> 0) console.log(n);
}
countdown(3)
// 2
// 1
// 0
上面代碼中,n --> 0
實際上會當作n-- > 0
,因此輸出2、1、0。
區塊
JavaScript使用大括號,將多個相關的語句組合在一起,稱為“區塊”(block
)。
對于var
命令來說,JavaScript的區塊不構成單獨的作用域(scope
)。
{
var a = 1;
}
a // 1
上面代碼在區塊內部,使用var
命令聲明并賦值了變量a
,然后在區塊外部,變量a
依然有效,區塊對于var
命令不構成單獨的作用域,與不使用區塊的情況沒有任何區別。在JavaScript語言中,單獨使用區塊并不常見,區塊往往用來構成其他更復雜的語法結構,比如for
、if
、while
、function
等。
條件語句
JavaScript 提供if
結構和switch
結構,完成條件判斷,即只有滿足預設的條件,才會執行相應的語句。
if 結構
if
結構先判斷一個表達式的布爾值,然后根據布爾值的真偽,執行不同的語句。
if (布爾值)
語句;
// 或者
if (布爾值) 語句;
上面是if
結構的基本形式。需要注意的是,“布爾值”往往由一個條件表達式產生的,必須放在圓括號中,表示對表達式求值。如果表達式的求值結果為true
,就執行緊跟在后面的語句;如果結果為false
,則跳過緊跟在后面的語句。
這種寫法要求條件表達式后面只能有一個語句。如果想執行多個語句,必須在if
的條件判斷之后,加上大括號,表示代碼塊(多個語句合并成一個語句)。
if (m === 3) {
m += 1;
}
建議總是在if
語句中使用大括號,因為這樣方便插入語句。
注意,if
后面的表達式之中,不要混淆賦值表達式(=
)、嚴格相等運算符(===
)和相等運算符(==
)。尤其是賦值表達式不具有比較作用。
var x = 1;
var y = 2;
if (x = y) {
console.log(x);
}
// "2"
上面代碼的原意是,當x
等于y
的時候,才執行相關語句。但是,不小心將嚴格相等運算符寫成賦值表達式,結果變成了將y
賦值給變量x
,再判斷變量x
的值(等于2)的布爾值(結果為true
)。
這種錯誤可以正常生成一個布爾值,因而不會報錯。為了避免這種情況,有些開發者習慣將常量寫在運算符的左邊,這樣的話,一旦不小心將相等運算符寫成賦值運算符,就會報錯,因為常量不能被賦值。
if (x = 2) { // 不報錯
if (2 = x) { // 報錯
if...else 結構
if
代碼塊后面,還可以跟一個else
代碼塊,表示不滿足條件時,所要執行的代碼。
if (m === 3) {
// 滿足條件時,執行的語句
} else {
// 不滿足條件時,執行的語句
}
對同一個變量進行多次判斷時,多個if...else
語句可以連寫在一起。
if (m === 0) {
// ...
} else if (m === 1) {
// ...
} else if (m === 2) {
// ...
} else {
// ...
}
else
代碼塊總是與離自己最近的那個if
語句配對。
var m = 1;
var n = 2;
if (m !== 1)
if (n === 2) console.log('hello');
else console.log('world');
上面代碼不會有任何輸出,else
代碼塊不會得到執行,因為它跟著的是最近的那個if
語句,相當于下面這樣。
if (m !== 1) {
if (n === 2) {
console.log('hello');
} else {
console.log('world');
}
}
如果想讓else
代碼塊跟隨最上面的那個if
語句,就要改變大括號的位置。
if (m !== 1) {
if (n === 2) {
console.log('hello');
}
} else {
console.log('world');
}
// world
switch 結構
多個if...else
連在一起使用的時候,可以轉為使用更方便的switch
結構。
switch (fruit) {
case "banana":
// ...
break;
case "apple":
// ...
break;
default:
// ...
}
上面代碼根據變量fruit
的值,選擇執行相應的case
。如果所有case
都不符合,則執行最后的default
部分。需要注意的是,每個case
代碼塊內部的break
語句不能少,否則會接下去執行下一個case
代碼塊,而不是跳出switch
結構。
var x = 1;
switch (x) {
case 1:
console.log('x 等于1');
case 2:
console.log('x 等于2');
default:
console.log('x 等于其他值');
}
// x等于1
// x等于2
// x等于其他值
上面代碼中,case
代碼塊之中沒有break
語句,導致不會跳出switch
結構,而會一直執行下去。正確的寫法是像下面這樣。
switch (x) {
case 1:
console.log('x 等于1');
break;
case 2:
console.log('x 等于2');
break;
default:
console.log('x 等于其他值');
}
switch
語句部分和case
語句部分,都可以使用表達式。
switch (1 + 3) {
case 2 + 2:
f();
break;
default:
neverHappens();
}
上面代碼的default
部分,是永遠不會執行到的。
需要注意的是,switch
語句后面的表達式,與case
語句后面的表示式比較運行結果時,采用的是嚴格相等運算符(===
),而不是相等運算符(==
),這意味著比較時不會發生類型轉換。
var x = 1;
switch (x) {
case true:
console.log('x 發生類型轉換');
break;
default:
console.log('x 沒有發生類型轉換');
}
// x 沒有發生類型轉換
上面代碼中,由于變量x
沒有發生類型轉換,所以不會執行case true
的情況。這表明,switch
語句內部采用的是“嚴格相等運算符”。
三元運算符 ?:
JavaScript 還有一個三元運算符(即該運算符需要三個運算子)?:
,也可以用于邏輯判斷。
(條件) ? 表達式1 : 表達式2
上面代碼中,如果“條件”為true
,則返回“表達式1”的值,否則返回“表達式2”的值。
var even = (n % 2 === 0) ? true : false;
上面代碼中,如果n
可以被2整除,則even
等于true
,否則等于false
。它等同于下面的形式。
var even;
if (n % 2 === 0) {
even = true;
} else {
even = false;
}
這個三元運算符可以被視為if...else...
的簡寫形式,因此可以用于多種場合。
var myVar;
console.log(
myVar ?
'myVar has a value' :
'myVar does not have a value'
)
// myVar does not have a value
var msg = '數字' + n + '是' + (n % 2 === 0 ? '偶數' : '奇數');
循環語句
循環語句用于重復執行某個操作,它有多種形式。
while 循環
While
語句包括一個循環條件和一段代碼塊,只要條件為真,就不斷循環執行代碼塊。
while (條件)
語句;
// 或者
while (條件) 語句;
while
語句的循環條件是一個表達式,必須放在圓括號中。代碼塊部分,如果只有一條語句,可以省略大括號,否則就必須加上大括號。
while (條件) {
語句;
}
for 循環
for
語句是循環命令的另一種形式,可以指定循環的起點、終點和終止條件。它的格式如下。
for (初始化表達式; 條件; 遞增表達式)
語句
// 或者
for (初始化表達式; 條件; 遞增表達式) {
語句
}
for
語句后面的括號里面,有三個表達式。
- 初始化表達式(
initialize
):確定循環變量的初始值,只在循環開始時執行一次。 - 條件表達式(
test
):每輪循環開始時,都要執行這個條件表達式,只有值為真,才繼續進行循環。 - 遞增表達式(
increment
):每輪循環的最后一個操作,通常用來遞增循環變量。
下面是一個例子。
var x = 3;
for (var i = 0; i < x; i++) {
console.log(i);
}
// 0
// 1
// 2
上面代碼中,初始化表達式是var i = 0
,即初始化一個變量i
;測試表達式是i < x
,即只要i
小于x
,就會執行循環;遞增表達式是i++
,即每次循環結束后,i
增大1。
所有for
循環,都可以改寫成while
循環。上面的例子改為while
循環,代碼如下。
var x = 3;
var i = 0;
while (i < x) {
console.log(i);
i++;
}
for
語句的三個部分(initialize、test、increment
),可以省略任何一個,也可以全部省略。
for ( ; ; ){
console.log('Hello World');
}
上面代碼省略了for
語句表達式的三個部分,結果就導致了一個無限循環。
do...while 循環
do...while
循環與while
循環類似,唯一的區別就是先運行一次循環體,然后判斷循環條件。
do
語句
while (條件);
// 或者
do {
語句
} while (條件);
不管條件是否為真,do...while
循環至少運行一次,這是這種結構最大的特點。另外,while
語句后面的分號注意不要省略。
break語句和continue語句
break
語句和continue
語句都具有跳轉作用,可以讓代碼不按既有的順序執行。
break
語句用于跳出代碼塊或循環。
var i = 0;
while(i < 100) {
console.log('i 當前為:' + i);
i++;
if (i === 10) break;
}
上面代碼只會執行10次循環,一旦i
等于10,就會跳出循環。
for
循環也可以使用break
語句跳出循環。
for (var i = 0; i < 5; i++) {
console.log(i);
if (i === 3)
break;
}
// 0
// 1
// 2
// 3
上面代碼執行到i
等于3,就會跳出循環。
continue
語句用于立即終止本輪循環,返回循環結構的頭部,開始下一輪循環。
var i = 0;
while (i < 100){
i++;
if (i % 2 === 0) continue;
console.log('i 當前為:' + i);
}
上面代碼只有在i
為奇數時,才會輸出i
的值。如果i
為偶數,則直接進入下一輪循環。
如果存在多重循環,不帶參數的break
語句和continue
語句都只針對最內層循環。
標簽(label)
JavaScript語言允許語句的前面有標簽(label
),相當于定位符,用于跳轉到程序的任意位置,標簽的格式如下。
label:
語句
標簽可以是任意的標識符,但不能是保留字,語句部分可以是任意語句。
標簽通常與break
語句和continue
語句配合使用,跳出特定的循環。
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) break top;
console.log('i=' + i + ', j=' + j);
}
}
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
上面代碼為一個雙重循環區塊,break
命令后面加上了top
標簽(注意,top
不用加引號),滿足條件時,直接跳出雙層循環。如果break
語句后面不使用標簽,則只能跳出內層循環,進入下一次的外層循環。
標簽也可以用于跳出代碼塊。
foo: {
console.log(1);
break foo;
console.log('本行不會輸出');
}
console.log(2);
// 1
// 2
上面代碼執行到break foo
,就會跳出區塊。
continue
語句也可以與標簽配合使用。
top:
for (var i = 0; i < 3; i++){
for (var j = 0; j < 3; j++){
if (i === 1 && j === 1) continue top;
console.log('i=' + i + ', j=' + j);
}
}
// i=0, j=0
// i=0, j=1
// i=0, j=2
// i=1, j=0
// i=2, j=0
// i=2, j=1
// i=2, j=2
上面代碼中,continue
命令后面有一個標簽名,滿足條件時,會跳過當前循環,直接進入下一輪外層循環。如果continue
語句后面不使用標簽,則只能進入下一輪的內層循環。