这是对 JavaScript 语法的初步了解。如果有些东西现在还不太明白,请不要担心。本书稍后将更详细地解释所有内容。
此外,本概述并不详尽。它侧重于基本要素。
// single-line comment
/*
Comment with
multiple lines
*/
布尔值
true
false
数字
1.141
-123
基本数字类型用于浮点数(双精度)和整数。
BigInt
17n
-49n
基本数字类型只能正确表示 53 位加符号范围内的整数。BigInt 的大小可以任意增大。
字符串
'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;
{
'jane';
,带有标签 first:
}
;
为什么 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。
测验:高级
请参阅测验应用程序。