這是 JavaScript 語法的初探。別擔心,如果有些東西現在還看不懂。本書稍後會詳細說明。
這個概觀也不是詳盡的。它著重於重點。
// single-line comment
/*
Comment with
multiple lines
*/
布林值
true
false
數字
1.141
-123
基本數字類型用於浮點數(雙精度)和整數。
大整數
17n
-49n
基本數字類型只能正確表示符號位元長度在 53 位元內的整數。大整數可以任意大。
字串
'abc'
"abc"
`String with interpolated values: ${256} and ${true}`
JavaScript 沒有額外的字元類型。它使用字串來表示字元。
斷言描述計算結果預期會是什麼,如果預期不正確,就會擲回例外。例如,以下斷言指出計算結果 7 加 1 必須是 8
.equal(7 + 1, 8); assert
assert.equal()
是方法呼叫(物件是 assert
,方法是 .equal()
),有兩個參數:實際結果和預期結果。它是 Node.js 斷言 API 的一部分,本書稍後會說明。
還有 assert.deepEqual()
,用於深入比較物件。
記錄到瀏覽器或 Node.js 的主控台
// Printing a value to standard out (another method call)
console.log('Hello!');
// Printing error information to standard error
console.error('Something went wrong!');
// Operators for booleans
.equal(true && false, false); // And
assert.equal(true || false, true); // Or
assert
// Operators for numbers
.equal(3 + 4, 7);
assert.equal(5 - 1, 4);
assert.equal(3 * 4, 12);
assert.equal(10 / 4, 2.5);
assert
// Operators for bigints
.equal(3n + 4n, 7n);
assert.equal(5n - 1n, 4n);
assert.equal(3n * 4n, 12n);
assert.equal(10n / 4n, 2n);
assert
// Operators for strings
.equal('a' + 'b', 'ab');
assert.equal('I see ' + 3 + ' monkeys', 'I see 3 monkeys');
assert
// Comparison operators
.equal(3 < 4, true);
assert.equal(3 <= 4, true);
assert.equal('abc' === 'abc', true);
assert.equal('abc' !== 'def', true); assert
JavaScript 也有 ==
比較運算子。我建議避免使用它,原因說明於§13.4.3「建議:總是使用嚴格相等」。
const
建立不可變變數繫結:每個變數都必須立即初始化,我們不能在稍後指定不同的值。不過,值本身可能是可變的,我們可以變更它的內容。換句話說:const
沒有讓值變為不可變。
// Declaring and initializing x (immutable binding):
const x = 8;
// Would cause a TypeError:
// x = 9;
let
建立可變變數繫結
// Declaring y (mutable binding):
let y;
// We can assign a different value to y:
= 3 * 5;
y
// Declaring and initializing z:
let z = 3 * 5;
// add1() has the parameters a and b
function add1(a, b) {
return a + b;
}// Calling function add1()
.equal(add1(5, 2), 7); assert
箭頭函式表示式特別用於函式呼叫和方法呼叫的參數
const add2 = (a, b) => { return a + b };
// Calling function add2()
.equal(add2(5, 2), 7);
assert
// Equivalent to add2:
const add3 = (a, b) => a + b;
前一個程式碼包含以下兩個箭頭函式(術語表示式和陳述式會在本章稍後說明)
// An arrow function whose body is a code block
, b) => { return a + b }
(a
// An arrow function whose body is an expression
, b) => a + b (a
// Creating a plain object via an object literal
const obj = {
first: 'Jane', // property
last: 'Doe', // property
getFullName() { // property (method)
return this.first + ' ' + this.last;
,
};
}
// Getting a property value
.equal(obj.first, 'Jane');
assert// Setting a property value
.first = 'Janey';
obj
// Calling the method
.equal(obj.getFullName(), 'Janey Doe'); assert
// Creating an Array via an Array literal
const arr = ['a', 'b', 'c'];
.equal(arr.length, 3);
assert
// Getting an Array element
.equal(arr[1], 'b');
assert// Setting an Array element
1] = 'β';
arr[
// Adding an element to an Array:
.push('d');
arr
.deepEqual(
assert, ['a', 'β', 'c', 'd']); arr
條件式陳述式
if (x < 0) {
= -x;
x }
for-of
迴圈
const arr = ['a', 'b'];
for (const element of arr) {
console.log(element);
}// Output:
// 'a'
// 'b'
每個模組都是一個單一檔案。例如,考慮以下兩個包含模組的檔案
file-tools.mjs
main.mjs
file-tools.mjs
中的模組匯出其函式 isTextFilePath()
export function isTextFilePath(filePath) {
return filePath.endsWith('.txt');
}
main.mjs
中的模組匯入整個模組 path
和函式 isTextFilePath()
// Import whole module as namespace object `path`
import * as path from 'path';
// Import a single export of module file-tools.mjs
import {isTextFilePath} from './file-tools.mjs';
class Person {
constructor(name) {
this.name = name;
}describe() {
return `Person named ${this.name}`;
}static logNames(persons) {
for (const person of persons) {
console.log(person.name);
}
}
}
class Employee extends Person {
constructor(name, title) {
super(name);
this.title = title;
}describe() {
return super.describe() +
` (${this.title})`;
}
}
const jane = new Employee('Jane', 'CTO');
.equal(
assert.describe(),
jane'Person named Jane (CTO)');
function throwsException() {
throw new Error('Problem!');
}
function catchesException() {
try {
throwsException();
catch (err) {
} .ok(err instanceof Error);
assert.equal(err.message, 'Problem!');
assert
} }
注意
try-finally
和 try-catch-finally
也受支援。Error
及其子類別支援。變數名稱和屬性名稱的語法範疇稱為識別碼。
識別碼允許包含下列字元
A
–Z
、a
–z
(等)$
, _
0
–9
(等)
有些字詞在 JavaScript 中具有特殊意義,稱為保留字。範例包括:if
、true
、const
。
保留字不能用作變數名稱
const if = 123;
// SyntaxError: Unexpected token if
但它們可以用作屬性名稱
> const obj = { if: 123 };
> obj.if123
串接字詞的常見大小寫樣式為
threeConcatenatedWords
three_concatenated_words
three-concatenated-words
一般而言,JavaScript 使用駝峰式大小寫,常數除外。
小寫
myFunction
obj.myMethod
special-class
specialClass
大寫
MyClass
MY_CONSTANT
myConstant
下列命名慣例在 JavaScript 中很受歡迎。
如果參數名稱以底線開頭(或為底線),表示此參數未被使用,例如
.map((_x, i) => i) arr
如果物件屬性名稱以底線開頭,則該屬性被視為私人屬性
class ValueWrapper {
constructor(value) {
this._value = value;
} }
在陳述式的結尾
const x = 123;
func();
但如果該陳述式以大括號結尾則不適用
while (false) {
// ···
// no semicolon
}
function func() {
// ···
// no semicolon }
不過,在這種陳述式後加上分號並非語法錯誤,它會被解釋為空陳述式
// Function declaration followed by empty statement:
function func() {
// ···
; }
測驗:基礎
請參閱 測驗應用程式。
本章節中其餘所有部分皆為進階。
第一個字元
é
和 ü
,以及非拉丁字母字元,例如 α
)$
_
後續字元
範例
const ε = 0.0001;
const строка = '';
let _tmp = 0;
const $foo2 = true;
保留字不能作為變數名稱,但可以用作屬性名稱。
await
break
case
catch
class
const
continue
debugger
default
delete
do
else
export
extends
finally
for
function
if
import
in
instanceof
let
new
return
static
super
switch
this
throw
try
typeof
var
void
while
with
yield
下列代碼也是關鍵字,但目前未用於語言中
enum
implements
package
protected
interface
private
public
下列字面值為保留字
true
false
null
技術上來說,這些字詞並非保留字,但您也應避免使用它們,因為它們實際上是關鍵字
Infinity
NaN
undefined
async
您也不應將全域變數名稱(String
、Math
等)用於自己的變數和參數。
在本節中,我們將探討 JavaScript 如何區分兩種語法結構:陳述式 和 表達式。之後,我們將看到這可能會造成問題,因為相同的語法在不同的使用位置可能表示不同的意義。
我們假設只有陳述式和表達式
為了簡化起見,我們假設 JavaScript 中只有陳述式和表達式。
陳述式 是可以執行並執行某種動作的程式碼片段。例如,if
是陳述式
let myStr;
if (myBool) {
= 'Yes';
myStr else {
} = 'No';
myStr }
另一個陳述式範例:函式宣告。
function twice(x) {
return x + x;
}
表達式 是可以評估為產生值的程式碼片段。例如,括號中的程式碼是一個表達式
let myStr = (myBool ? 'Yes' : 'No');
括號中使用的運算子 _?_:_
稱為三元運算子。它是 if
敘述的表達式版本。
讓我們來看更多表達式的範例。我們輸入表達式,而 REPL 會為我們評估它們
> 'ab' + 'cd''abcd'
> Number('123')123
> true || falsetrue
JavaScript 原始碼中的目前位置決定了你可以使用哪種語法結構
函數的主體必須是一系列敘述
function max(x, y) {
if (x > y) {
return x;
else {
} return y;
} }
函數呼叫或方法呼叫的引數必須是表達式
console.log('ab' + 'cd', Number('123'));
不過,表達式可以用作敘述。然後它們稱為表達式敘述。反之則不然:當內容需要表達式時,你不能使用敘述。
以下程式碼示範任何表達式 bar()
可以是表達式或敘述,這取決於內容
function f() {
console.log(bar()); // bar() is expression
bar(); // bar(); is (expression) statement
}
JavaScript 有幾個語法上含糊的程式結構:相同的語法會以不同的方式詮釋,這取決於是在敘述內容或表達式內容中使用。本節探討這種現象及其造成的陷阱。
函數宣告是一個敘述
function id(x) {
return x;
}
函數表達式是一個表達式(=
的右側)
const id = function me(x) {
return x;
; }
在以下程式碼中,{}
是物件文字:建立空物件的表達式。
const obj = {};
這是空的程式碼區塊(一個敘述)
{ }
含糊性只在敘述內容中會造成問題:如果 JavaScript 解析器遇到含糊的語法,它不知道這是一個單純的敘述還是表達式敘述。例如
function
開頭:這是一個函數宣告還是函數表達式?{
開頭:這是一個物件文字還是程式碼區塊?為了消除含糊性,以 function
或 {
開頭的敘述永遠不會被詮釋為表達式。如果你想要一個表達式敘述以其中一個代幣開頭,你必須用括號將它包起來
function (x) { console.log(x) })('abc');
(
// Output:
// 'abc'
在這個程式碼中
我們首先透過函數表達式建立一個函數
function (x) { console.log(x) }
然後我們呼叫那個函數:('abc')
顯示在 (1) 中的程式碼片段只會被詮釋為表達式,因為我們用括號將它包起來。如果我們沒有這麼做,我們會得到一個語法錯誤,因為 JavaScript 會期待一個函數宣告,並抱怨函數名稱遺失。此外,你不能在函數宣告之後立即放置函數呼叫。
在本書的後續章節中,我們將看到更多由語法含糊性造成的陷阱範例
每個陳述句以分號結尾
const x = 3;
someFunction('abc');
++; i
除了以區塊結尾的陳述句
function foo() {
// ···
}if (y > 0) {
// ···
}
以下情況稍有棘手
const func = () => {}; // semicolon!
整個 const
宣告(一個陳述句)以分號結尾,但其內部有一個箭頭函數表達式。也就是說,本身並非以大括號結尾的陳述句;而是嵌入的箭頭函數表達式。這就是為什麼結尾會有分號。
控制陳述句的主體本身就是一個陳述句。例如,以下是 while
迴圈的語法
while (condition)
statement
主體可以是一個單一陳述句
while (a > 0) a--;
但區塊也是陳述句,因此是控制陳述句的合法主體
while (a > 0) {
--;
a }
如果你希望迴圈有一個空的主體,你的第一個選項是一個空陳述句(只是一個分號)
while (processNextItem() > 0);
你的第二個選項是一個空區塊
while (processNextItem() > 0) {}
雖然我建議總是寫分號,但大多數分號在 JavaScript 中都是可選的。讓這成為可能的機制稱為自動分號插入 (ASI)。在某種程度上,它會更正語法錯誤。
ASI 的運作方式如下。陳述句的分析會持續到出現以下情況為止
換句話說,ASI 可以視為在換行符號處插入分號。以下小節涵蓋了 ASI 的陷阱。
關於 ASI 的好消息是,如果你不依賴它並總是寫分號,你只需要注意一個陷阱。那就是 JavaScript 禁止在某些令牌之後換行。如果你確實插入換行符號,也會插入分號。
在這個方面最切實相關的令牌是 return
。例如,考慮以下程式碼
return
{first: 'jane'
; }
此程式碼會分析為
return;
{first: 'jane';
};
也就是說
return;
{
first:
的表達式陳述句 'jane';
}
;
為什麼 JavaScript 會這樣做?它可以防止在 return
之後的行中意外回傳值。
在某些情況下,ASI 在你認為應該觸發時不會觸發。這讓不喜歡分號的人的生活變得更複雜,因為他們需要了解這些情況。以下有三個範例。還有更多。
範例 1:意外的函數呼叫。
= b + c
a + e).print() (d
分析為
= b + c(d + e).print(); a
範例 2:意外的除法。
= b
a /hi/g.exec(c).map(d)
分析為
= b / hi / g.exec(c).map(d); a
範例 3:意外的屬性存取。
someFunction()
'ul', 'ol'].map(x => x + x) [
執行為
const propKey = ('ul','ol'); // comma operator
.equal(propKey, 'ol');
assert
someFunction()[propKey].map(x => x + x);
我建議你總是寫分號
不過,也有許多人討厭分號帶來的額外視覺雜訊。如果您是其中之一:沒有分號的程式碼是合法的。我建議您使用工具來幫助您避免錯誤。以下兩個範例
從 ECMAScript 5 開始,JavaScript 有兩種 JavaScript 可以執行的模式
在幾乎總是位於模組中的現代 JavaScript 程式碼中,您很少會遇到隨意模式。在本書中,我假設嚴格模式總是開啟。
在腳本檔案和 CommonJS 模組中,您可以在第一行放置以下程式碼,為整個檔案開啟嚴格模式
'use strict';
這個「指令」很特別的地方在於,5 之前的 ECMAScript 版本會直接忽略它:它是一個什麼都不做的表達式陳述式。
您也可以只為單一函數開啟嚴格模式
function functionInStrictMode() {
'use strict';
}
讓我們看看嚴格模式比隨意模式做得更好的三件事。僅在此一節中,所有程式碼片段都在隨意模式中執行。
在非嚴格模式中,變更未宣告的變數會建立一個全域變數。
function sloppyFunc() {
= 123;
undeclaredVar1
}sloppyFunc();
// Created global variable `undeclaredVar1`:
.equal(undeclaredVar1, 123); assert
嚴格模式做得更好,會擲出 ReferenceError
。這樣可以更容易偵測到拼寫錯誤。
function strictFunc() {
'use strict';
= 123;
undeclaredVar2
}.throws(
assert=> strictFunc(),
()
{name: 'ReferenceError',
message: 'undeclaredVar2 is not defined',
; })
assert.throws()
陳述其第一個引數(一個函數)在呼叫時會擲出 ReferenceError
。
在嚴格模式中,透過函數宣告建立的變數只存在於最內層的封閉區塊中
function strictFunc() {
'use strict';
{function foo() { return 123 }
}return foo(); // ReferenceError
}.throws(
assert=> strictFunc(),
()
{name: 'ReferenceError',
message: 'foo is not defined',
; })
在隨意模式中,函數宣告是函數作用域
function sloppyFunc() {
{function foo() { return 123 }
}return foo(); // works
}.equal(sloppyFunc(), 123); assert
在嚴格模式中,如果您嘗試變更不可變資料,您會收到一個例外
function strictFunc() {
'use strict';
true.prop = 1; // TypeError
}.throws(
assert=> strictFunc(),
()
{name: 'TypeError',
message: "Cannot create property 'prop' on boolean 'true'",
; })
在隨便模式中,指定會靜默失敗
function sloppyFunc() {
true.prop = 1; // fails silently
return true.prop;
}.equal(sloppyFunc(), undefined); assert
進一步閱讀:隨便模式
有關隨便模式與嚴格模式有何不同的更多資訊,請參閱 MDN。
測驗:進階
請參閱 測驗應用程式。