这是对 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
assert.equal(7 + 1, 8);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
assert.equal(true && false, false); // And
assert.equal(true || false, true); // Or
// Operators for numbers
assert.equal(3 + 4, 7);
assert.equal(5 - 1, 4);
assert.equal(3 * 4, 12);
assert.equal(10 / 4, 2.5);
// Operators for bigints
assert.equal(3n + 4n, 7n);
assert.equal(5n - 1n, 4n);
assert.equal(3n * 4n, 12n);
assert.equal(10n / 4n, 2n);
// Operators for strings
assert.equal('a' + 'b', 'ab');
assert.equal('I see ' + 3 + ' monkeys', 'I see 3 monkeys');
// Comparison operators
assert.equal(3 < 4, true);
assert.equal(3 <= 4, true);
assert.equal('abc' === 'abc', true);
assert.equal('abc' !== 'def', true);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:
y = 3 * 5;
// 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()
assert.equal(add1(5, 2), 7);箭头函数表达式特别用作函数调用和方法调用的参数
const add2 = (a, b) => { return a + b };
// Calling function add2()
assert.equal(add2(5, 2), 7);
// Equivalent to add2:
const add3 = (a, b) => a + b;前面的代码包含以下两个箭头函数(术语*表达式*和*语句*将在本章后面解释)
// An arrow function whose body is a code block
(a, b) => { return a + b }
// An arrow function whose body is an expression
(a, b) => a + b// 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
assert.equal(obj.first, 'Jane');
// Setting a property value
obj.first = 'Janey';
// Calling the method
assert.equal(obj.getFullName(), 'Janey Doe');// Creating an Array via an Array literal
const arr = ['a', 'b', 'c'];
assert.equal(arr.length, 3);
// Getting an Array element
assert.equal(arr[1], 'b');
// Setting an Array element
arr[1] = 'β';
// Adding an element to an Array:
arr.push('d');
assert.deepEqual(
arr, ['a', 'β', 'c', 'd']);条件语句
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');
assert.equal(
jane.describe(),
'Person named Jane (CTO)');function throwsException() {
throw new Error('Problem!');
}
function catchesException() {
try {
throwsException();
} catch (err) {
assert.ok(err instanceof Error);
assert.equal(err.message, 'Problem!');
}
}注意
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.if
123用于连接单词的常见大小写风格有
threeConcatenatedWords three_concatenated_words three-concatenated-words 通常,JavaScript 使用驼峰式大小写,但常量除外。
小写
myFunctionobj.myMethodspecial-classspecialClass大写
MyClassMY_CONSTANTmyConstant以下命名约定在 JavaScript 中很流行。
如果参数名称以下划线开头(或为下划线),则表示未使用此参数 - 例如
arr.map((_x, i) => i)如果对象属性的名称以下划线开头,则该属性被视为私有属性
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;保留字不能用作变量名,但可以用作属性名。
awaitbreakcasecatchclassconstcontinuedebuggerdefaultdeletedoelseexportextendsfinallyforfunctionifimportininstanceofletnewreturnstaticsuperswitchthisthrowtrytypeofvarvoidwhilewithyield
以下标记也是关键字,但目前在语言中未使用
enumimplementspackageprotectedinterfaceprivatepublic
以下字面量是保留字
truefalsenull
从技术上讲,这些词不是保留字,但您也应该避免使用它们,因为它们实际上是关键字
InfinityNaNundefinedasync
您也不应该将全局变量的名称(String、Math 等)用于您自己的变量和参数。
在本节中,我们将探讨 JavaScript 如何区分两种语法结构:*语句*和*表达式*。之后,我们将看到这可能会导致问题,因为相同的语法在不同的使用位置可能意味着不同的含义。
我们假设只有语句和表达式
为了简单起见,我们假设 JavaScript 中只有语句和表达式。
*语句*是可以执行并执行某种操作的代码段。例如,if 是一个语句
let myStr;
if (myBool) {
myStr = 'Yes';
} else {
myStr = 'No';
}另一个语句的例子:函数声明。
function twice(x) {
return x + x;
}*表达式*是可以*求值*以产生值的代码段。例如,括号之间的代码是一个表达式
let myStr = (myBool ? 'Yes' : 'No');括号之间使用的运算符 _?_:_ 称为*三元运算符*。它是 if 语句的表达式版本。
让我们看看更多表达式的例子。我们输入表达式,REPL 会为我们对其进行求值
> 'ab' + 'cd'
'abcd'
> Number('123')
123
> true || false
trueJavaScript 源代码中的当前位置决定了允许使用哪种语法结构
函数体必须是一系列语句
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:**意外的函数调用。
a = b + c
(d + e).print()解析为
a = b + c(d + e).print();**示例 2:**意外的除法。
a = b
/hi/g.exec(c).map(d)解析为
a = b / hi / g.exec(c).map(d);**示例 3:**意外的属性访问。
someFunction()
['ul', 'ol'].map(x => x + x)执行为
const propKey = ('ul','ol'); // comma operator
assert.equal(propKey, 'ol');
someFunction()[propKey].map(x => x + x);我建议您始终编写分号
然而,也有很多人不喜欢分号带来的额外的视觉混乱。如果您是其中之一:没有它们,代码*是*合法的。我建议您使用工具来帮助您避免错误。以下是两个例子
从 ECMAScript 5 开始,JavaScript 有两种可以执行 JavaScript 的*模式*
在现代 JavaScript 代码中,您很少会遇到宽松模式,这些代码几乎总是位于模块中。在本书中,我假设始终开启严格模式。
在脚本文件和 CommonJS 模块中,您可以通过将以下代码放在第一行来为整个文件开启严格模式
'use strict';这种“指令”的巧妙之处在于,5 之前的 ECMAScript 版本会直接忽略它:它是一个什么也不做的表达式语句。
您也可以只为单个函数开启严格模式
function functionInStrictMode() {
'use strict';
}让我们来看看严格模式比宽松模式做得更好的三件事。仅在本节中,所有代码片段都在宽松模式下执行。
在非严格模式下,更改未声明的变量会创建一个全局变量。
function sloppyFunc() {
undeclaredVar1 = 123;
}
sloppyFunc();
// Created global variable `undeclaredVar1`:
assert.equal(undeclaredVar1, 123);严格模式做得更好,它会抛出一个 ReferenceError。这使得检测拼写错误更容易。
function strictFunc() {
'use strict';
undeclaredVar2 = 123;
}
assert.throws(
() => strictFunc(),
{
name: 'ReferenceError',
message: 'undeclaredVar2 is not defined',
});assert.throws() 表示它的第一个参数(一个函数)在被调用时会抛出一个 ReferenceError。
在严格模式下,通过函数声明创建的变量仅存在于最内层的封闭块中
function strictFunc() {
'use strict';
{
function foo() { return 123 }
}
return foo(); // ReferenceError
}
assert.throws(
() => strictFunc(),
{
name: 'ReferenceError',
message: 'foo is not defined',
});在宽松模式下,函数声明是函数级作用域的
function sloppyFunc() {
{
function foo() { return 123 }
}
return foo(); // works
}
assert.equal(sloppyFunc(), 123);在严格模式下,如果您尝试更改不可变数据,则会收到异常
function strictFunc() {
'use strict';
true.prop = 1; // TypeError
}
assert.throws(
() => strictFunc(),
{
name: 'TypeError',
message: "Cannot create property 'prop' on boolean 'true'",
});在宽松模式下,赋值会静默失败
function sloppyFunc() {
true.prop = 1; // fails silently
return true.prop;
}
assert.equal(sloppyFunc(), undefined); 延伸阅读:宽松模式
有关宽松模式与严格模式区别的更多信息,请参阅 MDN。
测验:高级
请参阅测验应用程序。