跳转至

A.3 JavaScript 编程语言

Section A.3 The JavaScript Programming Language

JavaScript 是一种为 Web 页面创建的编程语言。最近,一个称为 Node 的版本使得 JavaScript 能够用于服务器端程序,甚至用于通用编程。JavaScript 最初由 Netscape(Firefox 网络浏览器的前身)开发,大约在 Java 引入的同时,选择 JavaScript 这个名字是为了借助 Java 日益增长的人气。尽管名字相似,但这两种语言大不相同。实际上,并没有一个名为 JavaScript 的标准化语言。标准化的语言官方称为 ECMAScript,但这个名字在实践中并不常用,而且实际网络浏览器中的 JavaScript 版本也不一定完全实现标准。

传统上,程序员很难处理不同网络浏览器中 JavaScript 实现的差异。但几乎所有现代浏览器现在都实现了本节讨论的特性,这些特性由 ECMAScript 6 指定,也称为 ES6。一个显著的例外是 Internet Explorer,它是微软 Edge 浏览器的前身,确实不应该再被使用。

这个页面是 JavaScript 的简短概述。如果你真的想详细了解 JavaScript,你可能会考虑阅读 David Flanagan 所著的《JavaScript:权威指南》第七版这本书。

JavaScript is a programming language that was created for use on Web pages. More recently, a version known as node has made it possible to use JavaScript for server-side programs, and even for general programming. JavaScript was first developed by Netscape (the predecessor of the Firefox web browser) at about the same time that Java was introduced, and the name JavaScript was chosen to ride the tide of Java's increasing popularity. In spite of the similar names, the two languages are quite different. Actually, there is no standardized language named JavaScript. The standardized language is officially called ECMAScript, but the name is not widely used in practice, and versions of JavaScript in actual web browsers don't necessarily implement the standard exactly.

Traditionally, it has been difficult for programmers to deal with the differences among JavaScript implementations in different web browsers. But almost all modern browsers now implement the features discussed in this section, which are specified by ECMAScript 6, also known as ES6. A notable exception is Internet Explorer, the predecessor of Microsoft's Edge browser, which really should no longer be used.

This page is a short overview of JavaScript. If you really want to learn JavaScript in detail, you might consider the book JavaScript: The Definitive Guide, seventh edition, by David Flanagan.

A.3.1 核心语言

A.3.1 The Core Language

JavaScript 与网页最为紧密相关,但它是一种通用语言,也在其他地方使用。它有一个与特定网页无关的核心语言,我们首先从这个核心开始了解。

JavaScript 的语法比 Java 或 C 更为宽松。一个例子是分号的使用,在语句结尾不需要分号,除非同一行有另一个语句跟随。像许多宽松的语法规则一样,这可能导致一些意想不到的错误。如果一行是一个合法的语句,它被认为是一个完整的语句,下一行就是新语句的开始——即使你打算下一行是同一语句的延续。我曾因为以下形式的代码而遇到问题:

return
    "very long string";

第一行的 "return" 是一个合法的语句,所以下一行的值不被认为是该语句的一部分。结果是一个没有返回值就返回的函数。这也取决于 JavaScript 接受任何表达式作为语句的事实,比如第二行的字符串,即使评估该表达式没有任何效果。

JavaScript 中的变量没有类型。也就是说,当你声明一个变量时,你不需要声明它的类型,变量可以引用任何类型的数据。通常使用 let 关键字声明变量,它们在声明时可以可选地初始化:

let x, y;
let name = "David";

一个在初始化后不会被改变值的变量可以使用 const 而不是 var 来声明:

const name = "David";

ES6 之前,变量只能使用 var 关键字声明(就像 "var x,y;")。仍然可以使用 var 声明变量,但现在推荐使用 letconst。(使用 var 的一个特殊之处是,在 JavaScript 中,用它来多次声明同一个变量是没问题的;声明只是说明变量存在。)

JavaScript 还允许你在不声明的情况下使用变量。然而,这样做不是一个好主意。你可以通过在程序开头包含以下语句来防止使用未声明的变量,以及某些其他不安全的做法:

"use strict";

尽管变量没有类型,但值有。值可以是数字、字符串、布尔值、对象、函数,或者一些更奇特的东西。一个从未被赋值的变量具有特殊的值 undefined。(函数可以被用作数据值,这对你来说可能是一个惊喜;稍后会详细介绍。)你可以使用 typeof 操作符确定值的类型:表达式 typeof x 返回一个字符串,说明 x 的值的类型。字符串可以是 "undefined"、"number"、"string"、"boolean"、"object"、"function"、"bigint" 或 "symbol"。(本节不讨论 BigInts 和 symbols。)注意,typeof 对任何类型的对象,包括数组,都返回 "object"。还有,typeof null 是 "object"。

JavaScript 中,"string" 被视为原始数据类型,而不是对象类型。当使用 == 或 != 操作符比较两个字符串时,会比较多字符串的内容。没有 char 类型。要表示一个 char,使用长度为 1 的字符串。字符串可以使用 + 操作符进行连接,就像 Java 中一样。

字符串字面量可以用双引号或单引号括起来。从 ES6 开始,还有一种称为 "模板字符串" 的字符串字面量。模板字符串用单个反引号字符括起来。当模板字符串中包含 ${ 和 } 之间的 JavaScript 表达式时,该表达式的值将插入到字符串中。例如,如果 x 是 5,y 是 12,那么语句

result = `The product of ${x} and ${y} is ${x*y}`;

将字符串 "The product of 5 and 12 is 60" 分配给 result。此外,模板字符串可以包含换行符,因此它们提供了一种制作长字符串的简单方法,多行字符串。(反引号键可能在键盘的左上角。)

整数和实数之间没有严格的区别。两者都是 "number" 类型。与 Java 和 C 不同,在 JavaScript 中,整数相除会产生实数,所以 JavaScript 中的 1/2 是 0.5,而不是 Java 中的 0。

尽管有一个布尔类型,字面值 true 和 false,但实际上你可以在布尔上下文中使用任何类型的值。所以,你经常会看到像这样的 JavaScript 测试:

if (x) { ... }

如果 x 作为布尔值被认为是 false,当 x 是数字零或空字符串或 null 或 undefined。实际上,任何类型的值都可以隐式转换为布尔值。

实际上,JavaScript 会进行许多你可能没有预料到的隐式转换。例如,当使用 == 操作符比较数字和字符串时,JavaScript 会尝试将字符串转换为数字。所以,17 == "17" 的值是 true。"" == 0 的值也是 true,因为在这种情况下 JavaScript 将两个操作数都转换为布尔值。由于这种行为并不总是你想要的,JavaScript===!== 操作符,它们类似于 ==!=,但它们永远不会在操作数上进行类型转换。所以,例如,17 === "17" 是 false。通常,===!== 是用于等值测试的首选操作符。

如果你将一个字符串和一个数字相乘、相除或相减,JavaScript 也会尝试将字符串转换为数字——但如果是相加,它不会这样做,因为在那种情况下它将 + 操作符解释为字符串连接,并将数字转换为字符串。

JavaScript 没有 Java 中的类型转换。然而,你可以使用 Number、String 和 Boolean 作为转换函数。例如,

x = Number(y);

将尝试将 y 转换为数字。你可以在 y 是字符串时应用这个,如果转换失败,x 的值将为 NaN,这是一个特殊的数字值,表示 "Not a Number"。Number 函数将空字符串转换为零。

JavaScript 中的数学函数定义在一个 Math 对象中,类似于 Java 中的 Math 类。例如,有函数 Math.sin(x)Math.cos(x)Math.abs(x)Math.sqrt(x)Math.PI 是数学常量 π。Math.random() 是一个函数,返回一个介于 0.0 到 1.0 范围内的随机数,包括 0.0 但不包括 1.0。


JavaScript 的控制结构与 Java 或 C 类似,包括 if、while、for、do..while 和 switch。JavaScript 有一个 try..catch 语句用于处理异常,这与 Java 类似,但由于变量是未类型化的,所以只有一个 catch 块,并且它不为异常声明类型。(也就是说,你写 "catch (e)" 而不是 "catch(Exception e)"。)例如,参见示例程序 canvas2d/GraphicsStarter.html 中的 init() 函数。可以使用 throw 语句生成错误。可以抛出任何类型的值。例如,抛出一个表示错误信息的字符串:

throw "Sorry, that value is illegal.";

然而,最好抛出一个属于 Error 类或其子类的对象:

throw new Error("Sorry, that value is illegal.");

可以使用 function 关键字定义 JavaScript 中的函数。由于变量是未类型化的,所以没有返回类型声明,参数也没有声明类型。这是一个典型的函数定义:

function square(x) {
    return x * x;
}

一个函数可以返回任何类型的值,或者它可能不返回任何东西(就像 Java 中的 void 方法)。实际上,同一个函数有时可能返回一个值,有时可能不返回,尽管这样做风格不佳。JavaScript 不要求函数调用中的参数数量与函数定义中的参数数量匹配。如果在函数调用中提供的参数太少,那么函数定义中多余的参数将获得 undefined 值。你可以通过在函数中测试参数的 typeof 是否为 "undefined" 来检查这一点。这样做有一个好理由:它使得可以有可选参数。例如,考虑

function multiple( str, count ) {
    if ( typeof count === "undefined" ) {
        count = 2;
    }
    let copies = "";
    for (let i = 0; i < count; i++) {
        copies += str;
    }
    return copies;
}

如果没有为 count 提供值,如函数调用 multiple("boo"),那么 count 将被设置为 2。顺便说一下,使用 letconst 在函数中声明变量使其成为函数的局部变量,或者更准确地说是在声明它的块中。(使用 var 声明使其成为函数的局部变量,但不是在声明它的块中。)

也可以为参数提供默认值,这将在函数调用中没有为该参数提供值,或者提供的值是 undefined 时使用。例如,上述函数也可以写成

function multiple( str, count = 2 ) { // count 的默认值是 2
    let copies = "";
    for (let i = 0; i < count; i++) {
        copies += str;
    }
    return copies;
}

你还可以在函数调用中提供额外的值,使用所谓的 "rest 参数":参数列表中的最后一个参数可以由三个点前缀,如 "function f(x, y, ...z)"。任何额外的参数都被收集到一个数组中,该数组成为函数内部 rest 参数的值。例如,这使得可以编写一个求和函数,它可以接受任意数量的输入值:

function sum(...rest) {
    let total = 0;
    for (let i = 0; i < rest.length; i++) {
        total += rest[i];
    }
    return total;
}

有了这个定义,你可以调用 sum(2,2)sum(1,2,3,4,5),甚至 sum()。最后一个函数调用的值是零。

(处理可变数量参数的旧技术是使用特殊变量 arguments。在函数定义中,arguments 是一个类数组对象,包含传递给函数的所有参数的值。)

可以在一个函数内定义另一个函数。嵌套函数是局部的,只对包含它的函数可用。这允许你在函数内部定义一个“辅助函数”,而不是将辅助函数添加到全局命名空间中。


JavaScript 中的函数是“一等公民”。这意味着函数被当作常规数据值对待,你可以对它们执行与数据相同的操作:将它们赋值给变量、存储在数组中、作为参数传递给函数、从函数返回。实际上,执行所有这些操作是非常常见的!

当你像上面示例中那样使用定义来定义一个函数时,这几乎等同于将一个函数赋值给一个变量。例如,给定上面 sum 函数的定义,你可以将 sum 赋值给一个变量或将其作为参数传递,你将是在赋值或传递函数。如果一个变量的值是一个函数,你可以像使用函数名称一样使用该变量来调用函数。也就是说,如果你执行:

let f = sum;

那么你可以调用 f(1,2,3),这将和调用 sum(1,2,3) 相同。(定义函数和赋值变量之间的一个区别是,通过函数定义定义的函数可以在程序的任何地方使用,甚至在函数定义之前。在开始执行程序之前,计算机会读取整个程序以找到它包含的所有函数定义。另一方面,赋值语句在计算机在执行程序时遇到它们时执行。)

JavaScript 甚至有类似“函数字面量”的东西。也就是说,在你需要函数数据值的地方有一种编写方式,而无需给它命名或使用标准函数定义来定义它。这样的函数称为“匿名函数”。匿名函数有两种语法。较旧的语法看起来像没有名称的函数定义。例如,这里创建了一个匿名函数,并将其作为第一个参数传递给名为 setTimeout 的函数:

setTimeout( function () {
    alert("Time's Up!");
}, 5000 );

如果不使用匿名函数来完成相同的操作,将需要定义一个只会使用一次的标准命名函数:

function alertFunc() {
    alert("Time's Up!");
}

setTimeout( alertFunc, 5000 );

匿名函数的第二种语法是 ES6 中新增的“箭头函数”,其形式为 parameter_list => function_definition。例如,

() => { alert("Times Up!"); }

或者

(x,y) => { return x+y; }

如果恰好有一个参数,参数列表中的括号可以省略。如果只有一条语句,可以省略定义周围的大括号。如果单一语句是一个返回语句,那么“return”这个词也可以省略。因此,我们有像 "x => x*x" 这样的箭头函数。箭头函数和任何函数一样,可以赋值给变量、作为参数传递,甚至作为函数的返回值返回。例如,

setTimeout( () => alert("Times up!"), 5000);

在 C 语言中,可以将函数赋值给变量并将其作为参数传递给函数。然而,C 中没有匿名函数。类似于箭头函数的东西以“lambda 表达式”的形式被添加到了 Java 中。

JavaScript is most closely associated with web pages, but it is a general purpose language that is used in other places too. There is a core language that has nothing to do with web pages in particular, and we begin by looking at that core.

JavaScript has a looser syntax than either Java or C. One example is the use of semicolons, which are not required at the end of a statement, unless another statement follows on the same line. Like many cases of loose syntax rules, this can lead to some unexpected bugs. If a line is a legal statement, it is considered to be a complete statement, and the next line is the start of a new statement—even if you meant the next line to be a continuation of the same statement. I have been burned by this fact with code of the form

return
    "very long string";

The "return" on the first line is a legal statement, so the value on the next line is not considered to be part of that statement. The result was a function that returned without returning a value. This also depends on the fact that JavaScript will accept any expression, such as the string on the second line, as a statement, even if evaluating the expression doesn't have any effect.

Variables in JavaScript are not typed. That is, when you declare a variable, you don't declare what type it is, and the variable can refer to data of any type. Variables are usually declared using the keyword let, and they can optionally be initialized when they are declared:

let x, y;
let name = "David";

A variable whose value will not be changed after it is initialized can be declared using const instead of var:

const name = "David";

Before ES6, variables could only be declared with the keyword var (as in "var x,y;"). It is still possible to declare variables using var, but let and const are now preferred. (One peculiarity of using var is that it is OK in JavaScript to use it to declare the same variable more than once; a declaration just says that the variable exists.)

JavaScript also allows you to use variables without declaring them. However, doing so is not a good idea. You can prevent the use of undeclared variables, as well as certain other unsafe practices, by including the following statement at the beginning of your program:

"use strict";

Although variables don't have types, values do. A value can be a number, a string, a boolean, an object, a function, or a couple more exotic things. A variable that has never been assigned a value has the special value undefined. (The fact that a function can be used as a data value might be a surprise to you; more on that later.) You can determine the type of a value, using the typeof operator: The expression typeof x returns a string that tells the type of the value of x. The string can be "undefined", "number", "string", "boolean", "object", "function", "bigint", or "symbol". (Bigints and symbols are not discussed in this section.) Note that typeof returns "object" for objects of any type, including arrays. Also, typeof null is "object".

In JavaScript, "string" is considered to be a primitive data type rather than an object type. When two strings are compared using the == or != operator, the contents of the strings are compared. There is no char type. To represent a char, use a string of length 1. Strings can be concatenated with the + operator, like in Java.

String literals can be enclosed either in double quotes or in single quotes. Starting in ES6, there is also a kind of string literal known as a "template string." A template string is enclosed in single backquote characters. When a template string includes a JavaScript expression between ${ and }, the value of that expression is inserted into the string. For example, if x is 5 and y is 12, then the statement

result = `The product of ${x} and ${y} is ${x*y}`;

assigns the string "The product of 5 and 12 is 60" to result. Furthermore, a template string can include line feeds, so they provide an easy way to make long, multiline strings. (The backquote, or backtick, key might be in the top left corner of your keyboard.)

There is not a strict distinction between integers and real numbers. Both are of type "number". In JavaScript, unlike Java and C, division of integers produces a real number, so that 1/2 in JavaScript is 0.5, not 0 as it would be in Java.

Although there is a boolean type, with literal values true and false, you can actually use any type of value in a boolean context. So, you will often see tests in JavaScript such as

if (x) { ... }

The value of x as a boolean is considered to be false if x is the number zero or is the empty string or is null or is undefined. Effectively, any type of value can be converted implicitly to boolean

In fact, JavaScript does many implicit conversions that you might not expect. For example, when comparing a number to string using the == operator, JavaScript will try to convert the string into a number. So, the value of 17 == "17" is true. The value of "" == 0 is also true, since in this case JavaScript converts both operands to boolean. Since this behavior is not always what you want, JavaScript has operators === and !== that are similar to == and != except that they never do type conversion on their operands. So, for example, 17 === "17" is false. In general, === and !== are the preferred operators for equality tests.

JavaScript will also try to convert a string to a number if you multiply, divide, or subtract a string and a number—but not if you add them, since in that case it interprets the + operator as string concatenation, and it converts the number into to a string.

JavaScript does not have type-casting as it exists in Java. However, you can use Number, String, and Boolean as conversion functions. For example,

x = Number(y);

will attempt to convert y to a number. You can apply this, for example, when y is a string. If the conversion fails, the value of x will be NaN, a special number value indicating "Not a Number." The Number function converts the empty string to zero.

Mathematical functions in JavaScript are defined in a Math object, which is similar to the Math class in Java. For example, there are functions Math.sin(x), Math.cos(x), Math.abs(x), and Math.sqrt(x). Math.PI is the mathematical constant π. Math.random() is a function that returns a random number in the range 0.0 to 1.0, including 0.0 but excluding 1.0.


JavaScript control structures are similar to those in Java or C, including if, while, for, do..while, and switch. JavaScript has a try..catch statement for handling exceptions that is similar to Java's, but since variables are untyped, there is only one catch block, and it does not declare a type for the exception. (That is, you say, "catch (e)" rather than "catch(Exception e)".) For an example, see the init() function in the sample program canvas2d/GraphicsStarter.html. An error can be generated using the throw statement. Any type of value can be thrown. You might, for example, throw a string that represents an error message:

throw "Sorry, that value is illegal.";

However, it is preferable to throw an object belonging to the class Error or one of its subclasses:

throw new Error("Sorry, that value is illegal.");

Functions in JavaScript can be defined using the function keyword. Since variables are untyped, no return type is declared and parameters do not have declared types. Here is a typical function definition:

function square(x) {
    return x * x;
}

A function can return any type of value, or it can return nothing (like a void method in Java). In fact, the same function might sometimes return a value and sometimes not, although that would not be good style. JavaScript does not require the number of parameters in a function call to match the number of parameters in the definition of the function. If you provide too few parameters in the function call, then the extra parameters in the function definition get the value undefined. You can check for this in the function by testing if typeof the parameter is "undefined". There can be a good reason for doing this: It makes it possible to have optional parameters. For example, consider

function multiple( str, count ) {
    if ( typeof count === "undefined" ) {
        count = 2;
    }
    let copies = "";
    for (let i = 0; i < count; i++) {
        copies += str;
    }
    return copies;
}

If no value is provided for count, as in the function call multiple("boo"), then count will be set to 2. Note by the way that declaring a variable in a function using let or const makes it local to the function, or more generally to the block in which it is declared. (Declaring it using var makes it local to the function but not to the block where it is declared.)

It is also possible to provide a default value for a parameter, which will be used if the function call does not include a value for that parameter or if the value that is provided is undefined. For example, the above function could also be written as

function multiple( str, count = 2 ) { // default value of count is 2
    let copies = "";
    for (let i = 0; i < count; i++) {
        copies += str;
    }
    return copies;
}

You can also provide extra values in a function call, using something called a "rest parameter": The last parameter in the parameter list can be preceded by three dots, as in "function f(x, y, ...z)". Any extra parameters are gathered into an array, which becomes the value of the rest parameter inside the function. For example, this makes it possible to write a sum function that takes any number of input values:

function sum(...rest) {
    let total = 0;
    for (let i = 0; i < rest.length; i++) {
        total += rest[i];
    }
    return total;
}

With this definition, you can call sum(2,2), sum(1,2,3,4,5), and even sum(). The value of the last function call is zero.

(An older technique for dealing with a variable number of parameters is to use the special variable arguments. In a function definition, arguments is an array-like object that contains the values of all of the parameters that were passed to the function.)

It is possible to define a function inside another function. The nested function is then local to the function in which it is nested, and can only be used inside that function. This lets you define a "helper function" inside the function that uses it, instead of adding the helper function to the global namespace.


Functions in JavaScript are "first class objects." This means that functions are treated as regular data values, and you can do the sort of things with them that you do with data: assign them to variables, store them in arrays, pass them as parameters to functions, return them from functions. In fact, it is very common to do all of these things!

When you define a function using a definition like the ones in the examples shown above, it's almost the same as assigning a function to a variable. For example, given the above definition of the function sum, you can assign sum to a variable or pass it as a parameter, and you would be assigning or passing the function. And if the value of a variable is a function, you can use the variable just as you would use the function name, to call the function. That is, if you do

let f = sum;

then you can call f(1,2,3), and it will be the same as calling sum(1,2,3). (One difference between defining a function and assigning a variable is that a function defined by a function definition can be used anywhere in the program, even before the function definition. Before it starts executing the program, the computer reads the entire program to find all the function definitions that it contains. Assignment statements, on the other hand, are executed when the computer gets to them while executing the program.)

JavaScript even has something like "function literals." That is, there is a way of writing a function data value just at the point where you need it, without giving it a name or defining it with a standard function definition. Such functions are called "anonymous functions." There are two syntaxes for anonymous functions. The older syntax looks like a function definition without a name. Here, for example, an anonymous function is created and passed as the first parameter to a function named setTimeout:

setTimeout( function () {
    alert("Time's Up!");
}, 5000 );

To do the same thing without anonymous functions would require defining a standard named function that is only going to be used once:

function alertFunc() {
    alert("Time's Up!");
}

setTimeout( alertFunc, 5000 );

The second syntax for anonymous functions, new in ES6, is the "arrow function," which takes the form parameter_list => function_definition. For example,

() => { alert("Times Up!"); }

or

(x,y) => { return x+y; }

If there is exactly one parameter, the parentheses in the parameter list can be omitted. If there is only one statement, the braces around the definition can be omitted. And if the single statement is a return statement, then the word "return" can also be omitted. Thus, we have arrow functions such as "x => x*x". An arrow function, like any function, can be assigned to a variable, passed as a parameter, or even returned as the return value of a function. For example,

setTimeout( () => alert("Times up!"), 5000);

In C, functions can be assigned to variables and passed as parameters to functions. However, there are no anonymous functions in C. Something similar to arrow functions has been added to Java in the form of "lambda expressions."

A.3.2 数组和对象

A.3.2 Arrays and Objects

JavaScript 中的数组是一个对象,它包括几种用于操作数组的方法。数组中的元素可以是任何类型;实际上,同一个数组中的不同元素可以具有不同的类型。可以通过在方括号 [ ] 内列出值的方式来创建一个数组值。例如:

let A = [1, 2, 3, 4, 5];
let B = ["foo", "bar"];
let C = [];

这个例子中的最后一行创建了一个空数组,它最初长度为零。也可以使用构造函数来创建一个数组,并指定数组的初始大小:

let D = new Array(100);  // 为100个元素预留空间

最初,D 的所有元素的值都是 undefined。

数组的长度不是固定的。(这使得 JavaScript 数组与 Java 的 ArrayLists 更为相似,而与 Java 或 C 的数组不同。)如果 A 是一个数组,它的当前长度是 A.length。可以使用 push 方法将一个新元素添加到数组的末尾,将其长度增加一:A.push(6)。pop 方法移除并返回最后一个项目:A.pop()。实际上,给尚未存在的数组元素赋值是合法的:

let E = [1, 2, 3];  // E 的长度为 3
E[100] = 17;  // E 现在的长度为 101。

在这个例子中,当一个值被赋给 E[100] 时,数组的长度增加,以使其足够大,能够容纳新的元素。

现代 JavaScript 有一个特别适合与数组一起使用的 for 循环的替代版本。它的形式是 for (let v of A) ...,其中 A 是一个数组,v 是循环控制变量。在循环体中,循环控制变量依次取 A 的每个元素的值。因此,要累加一个数字数组中的所有值,你可以说:

let total = 0;
for (let num of A) {
    total = total + num; // num 是数组 A 中的一项。
}

由于其灵活性,标准的 JavaScript 数组在处理数字数组时效率不是很高。现代网络浏览器为数值应用定义了类型化数组。例如,类型为 Int32Array 的数组只能保存 32 位整数的值。类型化数组在 WebGL 中被广泛使用;当需要时,本书将涵盖它们的内容。


JavaScript 拥有对象和类,尽管它的类并不完全等同于 Java 或 C++ 中的类。首先,可以有无需类的对象存在。一个对象本质上只是一组键值对的集合,其中键是一个名称,类似于 Java 中的实例变量或方法名称,它有一个关联的值。在 JavaScript 中通常不使用“实例变量”这个术语;首选的术语是“属性”。

对象属性的值可以是一个普通数据值或一个函数(在 JavaScript 中,函数只是另一种类型的数据值)。可以通过用 {} 包围的键值对列表来创建一个对象。例如:

let pt = { x: 17, y: 42 };

let ajaxData = {
    url: "http://some.place.org/ajax.php",
    data: 42,
    onSuccess: function () { alert("It worked!"); },
    onFailure: function (error) { alert("Sorry, it failed: " + error); }
};

根据这些定义,pt 是一个对象。它有 pt.xpt.y 属性,分别有值 17 和 42。而 ajaxData 是另一个对象,包含 ajaxData.urlajaxData.onSuccess 等属性。ajaxData.onSuccess 的值是一个函数,这里作为匿名函数创建。作为对象的一部分的函数通常被称为该对象的“方法”,所以 ajaxData 包含两个方法 onSuccessonFailure

对象是开放的,这意味着你可以在任何时候通过赋值来向现有对象添加新属性。例如,给定上面定义的对象 pt,你可以说:

pt.z = 84;

这为对象添加了一个新的属性 z,初始值为 84。

对象也可以使用构造函数来创建。构造函数是一个使用 new 运算符调用以创建对象的函数。例如:

let now = new Date();

这调用了构造函数 Date(),它是 JavaScript 的标准部分。Date 是一个类,"new Date()" 创建了一个 Date 类型的对象。当不带参数调用时,new Date() 构造一个表示当前日期和时间的对象。

可以使用 class 关键字创建新类。类定义包含一系列函数定义,这些函数是不带 "function" 关键字声明的。类定义应该包括一个名为“constructor”的特殊函数,它作为类的构造函数。使用 new 运算符与类名一起时,实际上会调用这个构造函数。在函数定义中,使用特殊变量 this 引用对象的属性,并通过在构造函数中给它们赋值来向对象添加属性。

class Point2D {
    constructor(x = 0, y = 0) {
        // 构造一个具有属性 x 和 y 的 Point2D 类型的对象。
        // (构造函数的参数 x 和 y 的默认值为 0。)
        if (typeof x !== "number" || typeof y !== "number")
            throw new TypeError("The coordinates of a point must be numbers.");
        this.x = x;
        this.y = y;
    }
    move(dx, dy) {
        // 定义 move() 方法作为任何 Point2D 对象的属性。
        this.x = this.x + dx;
        this.y = this.y + dy;
    }
}

有了这个定义,可以创建 Point2D 类型的对象。任何这样的对象都将具有名为 x 和 y 的属性,以及一个名为 move() 的方法。例如:

let p = new Point2D();  // p.x 和 p.y 都是 0。
let q = new Point2D(17, 42);  // q.x 是 17,q.y 是 42。
q.move(10, 20);  // q.x 现在是 27,q.y 现在是 62。
q.z = 1;  // 我们仍然可以给 q 添加新的属性。

一个新类可以扩展一个现有的类,然后成为那个类的“子类”。然而,这个选项在这里没有覆盖,除了以下简单示例:

class Point3D extends Point2D {
    constructor(x = 0, y = 0, z = 0) {
        if (typeof z !== "number")
            throw new TypeError("The coordinates of a point must be numbers.");
        super(x, y);  // 调用 Point2D 的构造函数;创建 this.x 和 this.y。
        this.z = z;  // 向对象添加属性 z。
    }
    move(dx, dy, dz) { // 覆盖 move() 方法的定义
        super.move(dx, dy);  // 调用超类的 move()。
        if (typeof dz !== "undefined") {
            // 允许 move() 仍然只使用两个参数被调用。
            this.z = this.z + dz;
        }
    }
}

有关类和子类的更广泛示例,请参见 canvas2d/HierarchicalModel2D.html


JavaScript 中的 class 关键字是在 ES6 中新加入的,但 JavaScript 已经有了类的概念。然而,在早期版本的 JavaScript 中,一个类简单地通过一个构造函数来定义,而构造函数可以是任何使用 "new" 操作符调用的函数。由于这种类仍然在使用,了解它的工作原理是值得的。

构造函数的编写方式像一个普通函数;按照惯例,构造函数的名称以大写字母开头。构造函数定义了一个类,其名称是函数的名称。例如,让我们看看如何使用构造函数而不是 class 关键字来定义 Point2D 类:

function Point2D(x, y) {
    if (typeof x === "number") {
        this.x = x;
    } else {
        this.x = 0;
    }
    if (typeof y === "number") {
        this.y = y;
    } else {
        this.y = 0;
    }
    this.move = function(dx, dy) {
        this.x = this.x + dx;
        this.y = this.y + dy;
    };
}

当使用 new 操作符调用时,如 "new Point2D(17,42)",这个函数创建了一个具有 x、y 和 move 属性的对象。这些属性是通过在构造函数中给 this.x、this.y 和 this.move 分配值来创建的。创建的对象本质上与使用上述定义的 Point2D 类创建的对象相同。(注意:这里不能使用箭头函数定义 move 方法,因为在箭头函数的主体中,特殊变量 "this" 没有适当定义。)

这个例子中 move 方法的定义并不是最佳方式。问题是每个 Point2D 类型的对象都得到了自己的 move 副本。也就是说,定义 move 的代码为每个创建的对象都复制了一份。解决方案是使用所谓的函数 Point2D 的 "原型"(prototype)。

这可能让我们深入了解 JavaScript 的细节比我们真正需要的要多,但这里是它的工作原理:每个对象都有一个原型,这是另一个对象。原型的属性被认为是对象的属性,除非对象有同名的自己的属性。当几个对象有相同的原型时,这些对象共享原型的属性。现在,当一个对象通过构造函数创建时,构造函数的原型就成为它创建的新对象的原型。这意味着添加到构造函数原型的属性将被该函数创建的所有对象共享。因此,我们可以不在构造函数中给 this.move 赋值,而是可以在函数 Point2D 的定义之外这样做:

Point2D.prototype.move = function(dx, dy) {
    this.x = this.x + dx;
    this.y = this.y + dy;
};

原型的属性由所有 Point2D 类型的对象共享。在这种情况下,原型中有一个单一的 move 副本,被所有这样的对象使用。结果就是一个 Point2D 类,本质上与使用 class 关键字定义的类相同。

An array in JavaScript is an object, which includes several methods for working with the array. The elements in an array can be of any type; in fact, different elements in the same array can have different types. An array value can be created as a list of values enclosed between square brackets, [ and ]. For example:

let A = [ 1, 2, 3, 4, 5 ];
let B = [ "foo", "bar" ];
let C = [];

The last line in this example creates an empty array, which initially has length zero. An array can also be created using a constructor that specifies the initial size of the array:

let D = new Array(100);  // space for 100 elements

Initially, the elements of D all have the value undefined.

The length of an array is not fixed. (This makes JavaScript arrays more similar to Java ArrayLists than they are to Java or C arrays.) If A is an array, its current length is A.length. The push method can be used to add a new element to the end of an array, increasing its length by one: A.push(6). The pop method removes and returns the last item: A.pop(). In fact, it is legal to assign a value to an array element that does not yet exist:

let E = [ 1, 2, 3 ];  // E has length 3
E[100] = 17;  // E now has length 101.

In this example, when a value is assigned to E[100], the length of the array is increased to make it large enough to hold the new element.

Modern JavaScript has an alternative version of the for loop that is particularly useful with arrays. It takes the form for (let v of A) ..., where A is an array and v is the loop control variable. In the body of the loop, the loop control variable takes on the value of each element of A in turn. Thus, to add up all the values in an array of numbers, you could say:

let total = 0;
for (let num of A) {
    total = total + num; // num is one of the items in the array A.
}

Because of their flexibility, standard JavaScript arrays are not very efficient for working with arrays of numbers. Modern web browsers define typed arrays for numerical applications. For example, an array of type Int32Array can only hold values that are 32-bit integers. Typed arrays are used extensively in WebGL; they are covered in this book when they are needed.

JavaScript has objects and classes, although its classes are not exactly equivalent to those in Java or C++. For one thing, it is possible to have objects without classes. An object is essentially just a collection of key/value pairs, where a key is a name, like an instance variable or method name in Java, which has an associated value. The term "instance variable" is not usually used in JavaScript; the preferred term is "property."

The value of a property of an object can be an ordinary data value or a function (which is just another type of data value in JavaScript). It is possible to create an object as a list of key/value pairs, enclosed by { and }. For example,

let pt = { x: 17, y: 42 };

let ajaxData = {
    url: "http://some.place.org/ajax.php&#34;,  
    data: 42,
    onSuccess: function () { alert("It worked!"); },
    onFailure: function (error) { alert("Sorry, it failed: " + error); }
};

With these definitions, pt is an object. It has properties pt.x, with value 17, and pt.y, with value 42. And ajaxData is another object with properties including ajaxData.url and ajaxData.onSuccess. The value of ajaxData.onSuccess is a function, created here as an anonymous function. A function that is part of an object is often referred to as a "method" of that object, so ajaxData contains two methods, onSuccess and onFailure.

Objects are open in the sense that you can add a new property to an existing object at any time just by assigning a value. For example, given the object pt defined above, you can say

pt.z = 84;

This adds z as a new property of the object, with initial value 84.

Objects can also be created using constructors. A constructor is a function that is called using the new operator to create an object. For example,

let now = new Date();

This calls the constructor Date(), which is a standard part of JavaScript. Date is a class, and "new Date()" creates an object of type Date. When called with no parameters, new Date() constructs an object that represents the current date and time.

New classes can be created using the class keyword. A class definition contains a list of function definitions, which are declared without the "function" keyword. A class definition should include a special function named "constructor" that serves as the constructor for the class. This constructor function is actually called when the new operator is used with the name of the class. In the function definition, properties of the object are referred to using the special variable this, and properties are added to the object by assigning values to them in the constructor.

class Point2D {
    constructor(x = 0,y = 0) {
        // Construct an object of type Point2D with properties x and y.
        // (The parameters x and y to the constructor have default value 0.)
        if (typeof x !== "number" || typeof y !== "number")
            throw new TypeError("The coordinates of a point must be numbers.");
        this.x = x;
        this.y = y;
    }
    move(dx,dy) {
        // Defines a move() method as a property of any Point2D object.
        this.x = this.x + dx;
        this.y = this.y + dy;
    }
}

With this definition, it is possible to create objects of type Point2D. Any such object will have properties named x and y, and a method named move(). For example:

let p = new Point2D();  // p.x and p.y are 0.
let q = new Point2D(17,42);  // q.x is 17, q.y is 42.
q.move(10,20);  // q.x is now 27, and q.y is now 62.
q.z = 1;  // We can still add new properties to q.

A new class can extend an existing class, and then becomes a "subclass" of that class. However, this option is not covered here, except for the following simple example:

class Point3D extends Point2D {
    constructor(x = 0, y = 0, z = 0) {
        if (typeof z !== "number")
            throw new TypeError("The coordinates of a point must be numbers.");
        super(x,y);  // Call the Point2D constructor; creates this.x and this.y.
        this.z = z;  // Add the property z to the object.
    }
    move(dx,dy,dz) { // Override the definition of the move() method
        super.move(dx,dy);  // Call move() from the superclass.
        if (typeof dz !== "undefined") {
            // Allows move() to still be called with just two parameters.
            this.z = this.z + dz;
        }
    }
}

For a more extensive example of classes and subclasses, see canvas2d/HierarchicalModel2D.html.

The class keyword was new in ES6, but JavaScript already had classes. However, in earlier versions of JavaScript, a class was simply defined by a constructor function, and a constructor function could be any function called with the "new" operator. Since this kind of class is still used, it is worthwhile to look at how it works.

A constructor function is written like an ordinary function; by convention, the name of a constructor function begins with an upper case letter. A constructor function defines a class whose name is the name of the function. For example, let's see how to use a constructor function instead of the class keyword to define the class Point2D:

function Point2D(x,y) {
    if ( typeof x === "number") {
        this.x = x;
    }
    else {
        this.x = 0;
    }
    if ( typeof y === "number" ) {
        this.y = y;
    }
    else {
        this.y = 0;
    }
    this.move = function(dx,dy) {
        this.x = this.x + dx;
        this.y = this.y + dy;
    }
}

When called with the new operator, as in "new Point2D(17,42)", this function creates an object that has properties x, y, and move. These properties are created by assigning values to this.x, this.y, and this.move in the constructor function. The object that is created is essentially the same as an object created using the Point2D class defined above. (One note: the move method could not be defined here using an arrow function, since the special variable "this" is not appropriately defined in the body of an arrow function.)

The definition of the move method in this example is not done in the best way possible. The problem is that every object of type Point2D gets its own copy of move. That is, the code that defines move is duplicated for each object that is created. The solution is to use something called the "prototype" of the function Point2D.

This might take us farther into the details of JavaScript than we really need to go, but here is how it works: Every object has a prototype, which is another object. Properties of the prototype are considered to be properties of the object, unless the object is given its own property of the same name. When several objects have the same prototype, those objects share the properties of the prototype. Now, when an object is created by a constructor function, the prototype of the constructor becomes the prototype of the new object that it creates. This means that properties that are added to the prototype of a constructor function are shared by all the objects that are created by that function. Thus, instead of assigning a value to this.move in the constructor function, we can do the following outside the definition of function Point2D:

Point2D.prototype.move = function(dx,dy) {
    this.x = this.x + dx;
    this.y = this.y + dy;
}

The properties of the prototype are shared by all objects of type Point2D. In this case, there is a single copy of move in the prototype, which is used by all such objects. The result is then a Point2D class that is essentially the same as the class defined using the class keyword.

A.3.3 网页上的 JavaScript

A.3.3 JavaScript on Web Pages

在网页上(即 HTML 文件中)包含 JavaScript 代码有三种方式。首先,你可以将代码包含在 <script> 元素内部,其形式如下:

<script>
    // ... JavaScript 代码放这里 ...
</script>

你有时会在第一行看到 type 属性,如下所示:

<script type="text/javascript">

该属性指定了脚本使用的编程语言。然而,"text/javascript" 是默认值,对于 JavaScript 脚本并不是必需的。(你可能还会看到 <script> 带有 type="module",表示一个模块化的 JavaScript 程序。模块是 ES6 中的一个新特性。它们使得将大型程序拆分成组件,并控制组件之间变量的共享成为可能。模块在 three.js 3D 图形库中使用。它们在关于 three.js 的章节中有简要介绍。本书其他部分没有使用。)

第二种使用 JavaScript 代码的方式是将其放在一个单独的文件中,文件名通常以 ".js" 结尾,并将该文件导入到网页中。可以使用以下形式的 <script> 标签变体来导入 JavaScript 文件:

<script src="filename.js"></script>

这里的 "filename.js" 应该替换为 JavaScript 文件的 URL,可以是相对路径或绝对路径。这里需要闭合标签 </script> 来标记脚本的结束,即使脚本元素内部不允许有任何代码。(如果你这样做了,它将被忽略。)以这种方式从文件中导入 JavaScript 代码与直接将文件中的代码键入网页具有相同的效果。

两种类型的脚本元素通常包含在 HTML 文件的 <head> 部分,但它们实际上可以在文件的任何位置出现。你可以在同一页面上使用任意数量的脚本元素。脚本可以包括诸如函数调用和赋值语句等语句,以及变量和函数声明。

在网页上使用 JavaScript 的第三种方式是在 HTML 元素内部的事件处理器中。例如,考虑以下代码:

<h1 onclick="doClick()">我的网页</h1>

这里,onclick 属性定义了一个事件处理器,当用户点击 <h1> 元素的文本时将执行该处理器。事件处理器属性(如 onclick)的值可以是任何 JavaScript 代码。它可以包括多个用分号分隔的语句,甚至可以跨越多行。这里,代码是 "doClick()",所以点击 <h1> 元素将导致调用 JavaScript 函数 doClick()。我需要指出的是,这是将事件处理器附加到元素的一种过时方式,不应该被认为是最佳风格。我将在后面提到替代方案。然而,我有时也会以老式的方式做事。

理解所有在 <script> 元素中的 JavaScript 代码,包括导入文件中的代码,在页面加载时被读取和执行,这一点很重要。通常,这些脚本中的大部分代码由变量初始化和定义函数组成,这些函数旨在在页面加载后被调用,以响应事件。此外,页面上的所有脚本都是同一个程序的一部分。例如,你可以在一个脚本中定义一个变量或函数,甚至在一个导入的文件中定义,然后在另一个脚本中使用它。


JavaScript 为网页提供了几种标准函数,允许您使用对话框与用户交互。其中最简单的是 alert(message),它将在弹出对话框中向用户显示 message,并提供一个“确定”按钮,用户可以点击该按钮关闭消息。

函数 prompt(question) 将在对话框中显示问题,旁边有一个输入字段,用户可以在其中输入响应。prompt 函数将其返回值作为用户的响应。这种类型的对话框带有“确定”按钮和“取消”按钮。如果用户点击“取消”,则 prompt 的返回值为 null。如果用户点击“确定”,则返回值是输入字段中的内容(可能是空字符串)。

函数 confirm(question) 会在对话框中显示问题,旁边有“确定”和“取消”按钮。返回值是 true 或 false,取决于用户点击“确定”还是“取消”。

以下是一个使用这些函数进行用户交互的简单猜数游戏示例:

alert("我将选择一个 1 到 100 之间的数字。\n"
    + "试着猜猜看!");

do {
    let number = Math.floor(1 + 100 * Math.random());
    let guesses = 1;
    let guess = Number(prompt("猜猜看是什么?"));
    while (guess !== number) {
        if (isNaN(guess) || guess < 1 || guess > 100) { 
            guess = Number(prompt("请输入一个整数\n"
                            + "范围在 1 到 100 之间"));
        }
        else if (guess < number) {
            guess = Number(prompt("太低了。再试一次!"));
            guesses++;
        }
        else {
            guess = Number(prompt("太高了。再试一次!"));
            guesses++;
        }
    }
    alert("你在 " + guesses + " 次猜测中找到了。");

} while (confirm("再玩一次?"));

(该程序使用 Number() 将用户的响应转换为数字。如果响应不能解析为数字,则该值将为非数字值 NaN。isNaN(guess) 函数用于检查 guess 的值是否为这个特殊的非数字值。不能通过说 "if (guess === NaN)" 来做到这一点,因为表达式 NaN === NaN 求值为 false!顺便说一下,在 Java 中也是如此。)


您可以在许多网络浏览器中提供的 JavaScript 控制台中尝试 JavaScript 代码。例如,在 Chrome 浏览器中,您可以通过菜单下的“更多工具”/“开发者工具”访问控制台,然后点击开发者工具中的“控制台”标签。这将在 Chrome 窗口底部显示网络控制台,有一个 JavaScript 输入提示符。控制台也可以分离成一个单独的窗口。当您输入一行 JavaScript 并按回车时,它将被执行,其值将在控制台中输出。代码在当前网页的上下文中进行评估,所以您甚至可以输入影响该页面的命令。Web 控制台还显示在执行当前网页上的代码时发生的 JavaScript 错误,并且 JavaScript 代码可以通过调用 console.log(message) 向控制台写入消息。所有这些使控制台非常适合调试。(浏览器工具还包括一个复杂的 JavaScript 程序调试器。)

其他浏览器也有类似的开发者工具。在 Firefox 中的 JavaScript 控制台,可以在菜单中找到“Web 开发者工具”下的“Web 开发者”。在 Safari 浏览器中,使用“开发”菜单中的“显示 JavaScript 控制台”(但请注意,在 Safari 首选项中的“高级”标签下必须启用“开发”菜单)。在 Edge 浏览器中,通过按 F12 键访问“开发者工具”。

当网页上发生错误时,除了控制台中的一些输出外,您不会收到任何通知。所以,如果您的脚本似乎不起作用,您应该做的第一件事是打开控制台并查找错误消息。当您进行 JavaScript 开发时,您可能希望始终保持控制台打开。

There are three ways to include JavaScript code on web pages (that is, in HTML files). First, you can include it inside <script> elements, which have the form

<script>

    // ... JavaScript code goes here ...

</script>

You will sometimes see a type attribute in the first line, as in

<script type="text/javascript">

The attribute specifies the programming language used for the script. However, the value "text/javascript" is the default and the type attribute is not required for JavaScript scripts. ()You might also see a <script> with type="module", indicating a modular JavaScript program. Modules were a new feature in ES6. They make it possible to break up a large program into components and control the sharing of variables between components. Modules are used in the three.js 3D graphics library. They are covered briefly in the chapter on three.js. They are not used elsewhere in this textbook.)

The second way to use JavaScript code is to put it in a separate file, usually with a name ending with ".js", and import that file into the web page. A JavaScript file can be imported using a variation of the <script> tag that has the form

<script src="filename.js"></script>

where "filename.js" should be replaced by the URL, relative or absolute, of the JavaScript file. The closing tag, </script>, is required here to mark the end of the script, even though it is not permitted to have any code inside the script element. (If you do, it will be ignored.) Importing JavaScript code from a file in this way has the same effect as typing the code from the file directly into the web page.

Script elements of either type are often included in the <head> section of an HTML file, but they actually occur at any point in the file. You can use any number of script elements on the same page. A script can include statements such as function calls and assignment statements, as well as variable and function declarations.

The third way to use JavaScript on a web page is in event handlers that can occur inside HTML elements. For example, consider

<h1 onclick="doClick()">My Web Page</h1>

Here, the onclick attribute defines an event handler that will be executed when the user clicks on the text of the <h1> element. The value of an event handler attribute such as onclick can be any JavaScript code. It can include multiple statements, separated by semicolons, and can even extend over several lines. Here, the code is "doClick()", so that clicking the <h1> element will cause the JavaScript function doClick() to be called. I should note that this is an old-fashioned way to attach an event handler to an element, and it should not be considered best style. There are alternatives that I will mention later. Nevertheless, I sometimes do things the old-fashioned way.

It is important to understand that all the JavaScript code in <script> elements, including code in imported files, is read and executed as the page is being loaded. Usually, most of the code in such scripts consists of variable initializations and the definitions of functions that are meant to be called after the page has loaded, in response to events. Furthermore, all the scripts on a page are part of the same program. For example, you can define a variable or function in one script, even in an imported file, and then use it in another script.


JavaScript for web pages has several standard functions that allow you to interact with the user using dialog boxes. The simplest of these is alert(message), which will display message to the user in a popup dialog box, with an "OK" button that the user can click to dismiss the message.

The function prompt(question) will display question in a dialog box, along with an input field where the user can enter a response. The prompt function returns the user's response as its return value. This type of dialog box comes with an "OK" button and with a "Cancel" button. If the user hits "Cancel", the return value from prompt is null. If the user hits "OK", the return value is the content of the input field (which might be the empty string).

The function confirm(question) displays question in a dialog box along with "OK" and "Cancel" buttons. The return value is true or false, depending on whether the user hits "OK" or "Cancel".

Here, for example, is a simple guessing game that uses these functions for user interaction:

alert("I will pick a number between 1 and 100.\n"
        + "Try to guess it!");

do {

    let number = Math.floor( 1 + 100*Math.random() );
    let guesses = 1;
    let guess = Number( prompt("What's your guess?") );
    while (guess !== number ) {
        if ( isNaN(guess) || guess < 1 || guess > 100 ) { 
            guess = Number( prompt("Please enter an integer\n" +
                            "in the range 1 to 100") );
        }
        else if (guess < number) {
            guess = Number( prompt("Too low.  Try again!") );
            guesses++;
        }
        else {
            guess = Number( prompt("Too high.  Try again!") );
            guesses++;
        }
    }
    alert("You got it in " + guesses + " guesses.");

} while ( confirm("Play again?") );

(This program uses Number() to convert the user's response to a number. If the response cannot be parsed as a number, then the value will be the not-a-number value NaN. The function isNaN(guess) is used to check whether the value of guess is this special not-a-number value. It's not possible to do that by saying "if (guess === NaN)" since the expression NaN === NaN evaluates to false! The same, by the way, is true of the not-a-number value in Java.)


You can try out JavaScript code in the JavaScript consoles that are available in many web browsers. In the Chrome browser, for example, you can access a console in the menu under "More Tools" / "Developer Tools", then click the "Console" tab in the developer tools. This will show the web console at the bottom of the Chrome window, with a JavaScript input prompt. The console can also be detached into a separate window. When you type a line of JavaScript and press return, it is executed, and its value is output in the console. The code is evaluated in the context of the current web page, so you can even enter commands that affect that page. The Web console also shows JavaScript errors that occur when code on the current web page is executed, and JavaScript code can write a message to the console by calling console.log(message). All this makes the console very useful for debugging. (Browser tools also include a sophisticated JavaScript program debugger.)

Other browsers have similar developer tools. For the JavaScript console in Firefox, look for "Web Developer Tools" under "Web Developer" in the menu. In the Safari browser, use "Show JavaScript Console" in the "Develop" menu (but note that the Develop menu has to be enabled in the Safari Preferences, under the "Advanced" tab). In the Edge browser, access "Developer Tools" by hitting the F12 key.

When an error occurs on a web page, you don't get any notification, except for some output in the console. So, if your script doesn't seem to be working, the first thing you should do is open the console and look for an error message. When you are doing JavaScript development, you might want to keep the console always open.

A.3.4 与页面交互

A.3.4 Interacting with the Page

网页上的 JavaScript 代码可以操作该页面的内容和样式。这是可能的,因为 DOM(文档对象模型)。当一个网页被加载时,页面上的所有内容都被编码成一个数据结构,由 DOM 定义,可以从 JavaScript 访问为对象集合。有几种方法可以获得这些对象的引用,但我将只讨论一种:document.getElementById。网页上的任何元素都可以有一个 id 属性。例如:

<img src="somepicture.jpg" id="pic">

或者

<h1 id="mainhead">我的页面</h1>

id 应该是页面上唯一的,以便元素可以通过其 id 被唯一标识。任何元素都由一个 DOM 对象表示。如果一个元素有一个 id,你可以通过将 id 传递给函数 document.getElementById 来获得对相应 DOM 对象的引用。例如:

let image = document.getElementById("pic");
let heading = document.getElementById("mainhead");

一旦你有了 DOM 对象,你就可以使用它来操作它所代表的元素。例如,元素的内容由对象的 innerHTML 属性给出。这个值是一个包含文本或 HTML 代码的字符串。在我们的示例中,heading.innerHTML 的值是字符串 "我的页面"。此外,你可以给这个属性赋值,这样做会改变元素的内容。例如:

heading.innerHTML = "最好的页面!";

这不仅改变了对象中的属性值;它实际上改变了网页上显示的文本!对于刚接触 JavaScript 的程序员来说,这可能看起来很奇怪(甚至有点令人毛骨悚然):这是一个带有副作用的赋值语句。但这就是 DOM 的工作原理。对代表网页的 DOM 数据结构的更改实际上会修改页面并改变它在 Web 浏览器中的显示。

元素的一些属性变成了代表它们的属性。这对于图像元素的 src 属性来说是正确的,所以在我们的示例中,我们可以这样说:

image.src = "anotherpicture.jpg";

这将改变图像元素的来源。同样,这是一个“实时”赋值:当执行赋值语句时,网页上的图像会改变。

对于熟悉 CSS 的读者,请注意,元素的 DOM 对象有一个名为 style 的属性,它本身是一个对象,代表对象的 CSS 样式。style 对象具有 color、backgroundColor 和 fontSize 等属性,代表 CSS 属性。通过给这些属性赋值,你可以改变页面上元素的外观。例如,

heading.style.color = "red";
heading.style.fontSize = "150%";

这些命令将使 <h1> 元素中的文本变为红色,并且比通常大 50%。style 属性的值必须是对于相应的 CSS 样式来说是一个合法值的字符串。

在网页的 HTML 源代码中,我们可能有以下输入元素:

<input type="text" id="textin">

<select id="sel">
    <option value="1">选项 1</option>
    <option value="2">选项 2</option>
    <option value="3">选项 3</option>
</select>

<input type="checkbox" id="cbox">

JavaScript 中,我们可以通过以下方式获取这些元素的引用:

let textin = document.getElementById("textin");
let sel = document.getElementById("sel");
let checkbox = document.getElementById("cbox");

然后,属性 checkbox.checked 的值是一个布尔值,可以用来测试复选框是否被选中,也可以给 checkbox.checked 赋值为 true 或 false 来以编程方式选中或取消选中该框。属性 checkbox.disabled 的值也是一个布尔值,表示复选框是否被禁用(用户不能更改被禁用的复选框的值)。同样,你可以测试并设置这个值。属性 sel.disabled 和 textin.disabled 对 <select> 菜单和文本输入框也有相同的功能。属性 textin.value 和 sel.value 分别代表这些元素的当前值。文本输入的值是当前在框中的文本。<select> 元素的值是当前选中的选项的值。例如,下面是一个使用文本输入框和按钮实现猜数游戏的完整网页源代码:

<!DOCTYPE html>
<html>
<head>
    <title>猜数游戏</title>
    <script>
        "use strict";
        let number = Math.floor(1 + 100 * Math.random());
        let guessCount = 0;
        let guessMessage = "你目前的猜测:";
        function guess() {
            let userNumber = Number(document.getElementById("guess").value);
            document.getElementById("guess").value = "";
            if (isNaN(userNumber) || userNumber < 1 || userNumber > 100) {
                document.getElementById("question").innerHTML =
                "输入无效!<br>请再次使用 1 到 100 范围内的整数尝试。";
            }
            else if (userNumber === number) {
                guessCount++;
                document.getElementById("question").innerHTML =
                    "你在 " + guessCount + " 次猜测中找到了。 " +
                    number + " 是正确的。<br>" +
                    "我选择了另一个数字。 猜猜看!";
                number = Math.floor(1 + 100 * Math.random());
                guessCount = 0;
                guessMessage = "你目前的猜测:";
                document.getElementById("message").innerHTML = "";
            }
            else if (userNumber < number) {
                guessCount++;
                document.getElementById("question").innerHTML =
                    userNumber + " 太低了。<br>再试一次。";
                guessMessage += " " + userNumber;
                document.getElementById("message").innerHTML = guessMessage;
            }
            else {
                guessCount++;
                document.getElementById("question").innerHTML =
                    userNumber + " 太高了。<br>再试一次。";
                guessMessage += " " + userNumber;
                document.getElementById("message").innerHTML = guessMessage;
            }
        }
    </script>
</head>
<body>
    <p id="question">我将选择一个 1 到 100 之间的数字。<br>
    尝试猜猜看。你的第一个猜测是什么?</p>
    <p><input type="text" id="guess">
    <button onclick="guess()">进行猜测</button></p>
    <p id="message"></p>
</body>
</html>

我的一些讨论存在一个问题。假设一个脚本使用函数 document.getElementById 来获取某个 HTML 元素的 DOM 对象。如果该脚本在页面加载完成之前执行,它尝试访问的元素可能还不存在。请记住,脚本是在页面加载时执行的。当然,一个解决方案是仅在页面加载完成后才响应事件执行的函数中调用 document.getElementById;这正是我在前面示例中所做的。但有时,你可能想将 DOM 对象赋值给一个全局变量。你应该在哪里这样做呢?一种可能性是将脚本放在页面的末尾。这可能会起作用。另一种更常见的技术是将赋值放入一个函数中,并安排在页面加载完成后运行该函数。当浏览器加载完页面并构建其 DOM 表示时,会触发一个加载事件。你可以安排一些 JavaScript 代码作为对该事件的响应。一种常见的方法是向 <body> 标签添加一个 onload 事件处理器:

<body onload="init()">

这将在页面加载时调用名为 init() 的函数。该函数应该包含你的程序所需的任何初始化代码。

你可以在其他元素中定义类似的事件处理器。例如,对于 <input><select> 元素,你可以提供一个 onchange 事件处理器,当用户更改与元素关联的值时,将执行此处理器。这允许你在用户选中或取消选中复选框或从选择菜单中选择新选项时做出响应。

可以将事件处理器包含在创建元素的 HTML 标签中,正如我在 body onload 事件中所做的那样。但这并不是设置事件处理的首选方式。首先,混合 JavaScript 代码和 HTML 代码通常被认为是不良风格。或者,你可以使用 DOM 安装事件处理器的另外两种方式。假设 checkbox 是表示复选框元素的 DOM 对象,可能是通过调用 document.getElementById 获得的。该对象有一个名为 onchange 的属性,表示复选框的 onchange 事件的事件处理器。你可以通过将函数分配给该属性来设置事件处理。如果 checkBoxChanged 是你希望在用户选中或取消选中框时调用的函数,你可以使用 JavaScript 命令:

checkbox.onchange = checkBoxChanged;

你也可以使用匿名函数:

checkbox.onchange = function() { alert("复选框已更改"); };

请注意,checkbox.onchange 的值是一个函数,而不是 JavaScript 代码的字符串。

JavaScript 中设置事件处理的另一种方式是使用 addEventListener 函数。这种技术更加灵活,因为它允许你为同一事件设置多个事件处理器。该函数是任何 DOM 元素对象的方法。使用它,我们的复选框示例变为:

checkbox.addEventListener("change", checkBoxChanged, false);

addEventListener 的第一个参数是一个字符串,提供事件的名称。名称与 HTML 中的事件属性名称相同,只是去掉了前面的 "on":onchange 变为 "change"。第二个参数是事件发生时将被调用的函数。它可以作为函数的名称给出,也可以作为匿名函数给出。第三个参数对我们的目的来说更难解释,将始终是 false。你可以使用与调用 element.addEventListener 时相同的参数从元素中移除事件侦听器,方法是调用 element.removeEventListener。加载事件与名为 window 的预定义对象关联,所以你可以不说在 <body> 标签中附加该事件的处理程序,而是说:

window.onload = init;

或者

window.addEventListener("load", init, false);

同样,有一个 onmousedown 事件是为任何元素定义的。可以分别通过将函数分配给 elem.onmousedown 或调用 elem.addEventListener("mousedown",handler,false) 将此事件的处理程序附加到 DOM 元素,elem。其他常见事件包括 onmouseuponmousemoveonclickonkeydownonkeydown 事件处理器响应用户按下键盘上的键。该处理器通常附加到文档对象:

document.onkeydown = doKeyPressed;

事件处理程序函数可以带一个参数,其中包含有关事件的信息。例如,在鼠标事件的处理程序中,使用 evt 作为参数的名称,evt.clientXevt.clientY 提供鼠标在浏览器窗口中的位置。在处理 onkeydown 事件的处理程序中,evt.keyCode 是所按键的数字代码。

事件处理是一个复杂的话题,我这里只做了一个简短的介绍。作为学习 JavaScript 事件的第一步,你可以查看示例网页 canvas2d/EventsStarter.htmlHTML 源代码。

JavaScript code on a web page can manipulate the content and the style of that page. It can do this because of the DOM (Document Object Model). When a web page is loaded, everything on the page is encoded into a data structure, defined by the DOM, which can be accessed from JavaScript as a collection of objects. There are several ways to get references to these objects, but I will discuss only one: document.getElementById. Any element on a web page can have an id attribute. For example:

<img src="somepicture.jpg" id="pic">

or

<h1 id="mainhead">My Page</h1>

An id should be unique on the page, so that an element is uniquely identified by its id. Any element is represented by a DOM object. If an element has an id, you can obtain a reference to the corresponding DOM object by passing the id to the function document.getElementById. For example:

let image = document.getElementById("pic");
let heading = document.getElementById("mainhead");

Once you have a DOM object, you can use it to manipulate the element that it represents. For example, the content of the element is given by the innerHTML property of the object. The value is a string containing text or HTML code. In our example, the value of heading.innerHTML is the string "My Page". Furthermore, you can assign a value to this property, and doing so will change the content of the element. For example:

heading.innerHTML = "Best Page Ever!";

This does not just change the value of the property in the object; it actually changes the text that is displayed on the web page! This will seem odd (and maybe even a little creepy) to programmers who are new to JavaScript: It's an assignment statement that has a side effect. But that's the way the DOM works. A change to the DOM data structure that represents a web page will actually modify the page and change its display in the web browser.

Some attributes of elements become properties of the objects that represent them. This is true for the src attribute of an image element, so that in our example, we could say

image.src = "anotherpicture.jpg";

This will change the source of the image element. Again, this is a "live" assignment: When the assignment statement is executed, the image on the web page changes.

For readers who know CSS, note that the DOM object for an element has a property named style that is itself an object, representing the CSS style of the object. The style object has properties such as color, backgroundColor, and fontSize representing CSS properties. By assigning values to these properties, you can change the appearance of the element on the page. For example,

heading.style.color = "red";
heading.style.fontSize = "150%";

These commands will make the text in the

element red and 50% larger than usual. The value of a style property must be a string that would be a legal value for the corresponding CSS style.

Most interesting along these lines, perhaps, are properties of input elements, since they make it possible to program interaction with the user. Suppose that in the HTML source of a web page, we have

<input type="text" id="textin">

<select id="sel">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>

<input type="checkbox" id="cbox">

and in JavaScript, we have

let textin = document.getElementById("textin");
let sel = document.getElementById("sel");
let checkbox = document.getElementById("cbox");

Then the value of the property checkbox.checked is a boolean that can be tested to determine whether the checkbox is checked or not, and the value true or false can be assigned to checkbox.checked to check or uncheck the box programmatically. The value of checkbox.disabled is a boolean that tells whether the checkbox is disabled. (The user can't change the value of a disabled checkbox.) Again, you can both test and set this value. The properties sel.disabled and textin.disabled do the same thing for the <select> menu and the text input box. The properties textin.value and sel.value represent the current values of those elements. The value of a text input is the text that is currently in the box. The value of a <select> element is the value of the currently selected option. As an example, here is complete source code for a web page that implements a guessing game using a text input box and buttons:

<!DOCTYPE html>
<html>
<head>
<title>Guessing Game</title>
<script>
    "use strict";
    let number = Math.floor( 1 + 100*Math.random() );
    let guessCount = 0;
    let guessMessage = "Your guesses so far: ";
    function guess() {
        let userNumber = Number( document.getElementById("guess").value );
        document.getElementById("guess").value = "";
        if ( isNaN(userNumber) || userNumber < 1 || userNumber > 100 ) {
            document.getElementById("question").innerHTML =
            "Bad input!<br>Try again with an integer in the range 1 to 100.";
        }
        else if (userNumber === number) {
            guessCount++;
            document.getElementById("question").innerHTML =
                "You got it in " + guessCount + " guesses. " +
                userNumber + " is correct.<br>" + 
                "I have picked another number.  Make a guess!";
            number = Math.floor( 1 + 100*Math.random() );
            guessCount = 0;
            guessMessage = "Your guesses so far: ";
            document.getElementById("message").innerHTML = "";
        }
        else if (userNumber < number) {
            guessCount++;
            document.getElementById("question").innerHTML =
                userNumber + " is too low.<br>Try again.";
            guessMessage += " " + userNumber;
            document.getElementById("message").innerHTML = guessMessage;
        }
        else {
            guessCount++;
            document.getElementById("question").innerHTML =
                userNumber + " is too high.<br>Try again.";
            guessMessage += " " + userNumber;
            document.getElementById("message").innerHTML = guessMessage;
        }
    }
</script>
</head>
<body>
    <p id="question">I will pick a number between 1 and 100.<br>
    Try to guess it.  What is your first guess?</p>
    <p><input type="text" id="guess">
    <button onclick="guess()">Make Guess</button></p>
    <p id="message"></p>
</body>
</html>

Here's one problem with some of my discussion. Suppose that a script uses the function document.getElementById to get the DOM object for some HTML element. If that script is executed before the page has finished loading, the element that it is trying to access might not yet exist. And remember that scripts are executed as the page is loading. Of course, one solution is to call document.getElementById only in functions that are executed in response to events that can only occur after the page has loaded; that's what I did in the previous example. But sometimes, you want to assign a DOM object to a global variable. Where should you do that? One possibility is to put the script at the end of the page. That will probably work. Another, more common technique is to put the assignment into a function and arrange for that function to run after the page has finished loading. When the browser has finished loading the page and building its DOM representation, it fires a load event. You can arrange for some JavaScript code to be called in response to that event. A common way of doing this is to add an onload event-handler to the <body> tag:

<body onload="init()">

This will call a function named init() when the page has loaded. That function should include any initialization code that your program needs.

You can define similar event-handlers in other elements. For example, for <input> and <select> elements, you can supply an onchange event-handler that will be executed when the user changes the value associated with the element. This allows you to respond when the user checks or unchecks a checkbox or selects a new option from a select menu.

It's possible to include an event handler for an element in the HTML tag that creates the element, as I did with the body onload event. But that's not the preferred way to set up event handling. For one thing, the mixing of JavaScript code and HTML code is often considered to be bad style. Alternatively, there are two other ways to install event handlers using the DOM. Suppose that checkbox is a DOM object representing a check box element, probably obtained by calling document.getElementById. That object has a property named onchange that represents an event-handler for the checkbox's onchange event. You can set up event handling by assigning a function to that property. If checkBoxChanged is the function that you want to call when the user checks or unchecks the box, you can use the JavaScript command:

checkbox.onchange = checkBoxChanged;

You could also use an anonymous function:

checkbox.onchange = function() { alert("Checkbox changed"); };

Note that the value of checkbox.onchange is a function, not a string of JavaScript code.

The other way to set up event handling in JavaScript is with the addEventListener function. This technique is more flexible because it allows you to set up more than one event handler for the same event. This function is a method in any DOM element object. Using it, our checkbox example becomes

checkbox.addEventListener( "change", checkBoxChanged, false );

The first parameter to addEventListener is a string that gives the name of the event. The name is the same as the name of the event attribute in HTML, with "on" stripped off the front: onchange becomes "change". The second parameter is the function that will be called when the event occurs. It can be given as the name of a function or as an anonymous function. The third parameter is harder to explain and will, for our purposes, always be false. You can remove an event listener from an element by calling element.removeEventListener with the same parameters that were used in the call to element.addEventListener. The load event is associated with a predefined object named window, so instead of attaching an event-handler for that event in the <body> tag, you could say

window.onload = init;

or

window.addEventListener("load", init, false);

Similarly, there is an onmousedown event that is defined for any element. A handler for this event can be attached to a DOM element, elem, either by assigning a function to elem.onmousedown or by calling elem.addEventListener("mousedown",handler,false). Other common events include onmouseup, onmousemove, onclick, and onkeydown. An onkeydown event handler responds when the user presses a key on the keyboard. The handler is often attached to the document object:

document.onkeydown = doKeyPressed;

An event-handler function can take a parameter that contains information about the event. For example, in an event-handler for mouse events, using evt as the name of the parameter, evt.clientX and evt.clientY give the location of the mouse in the browser window. In a handler for the onkeydown event, evt.keyCode is a numeric code for the key that was pressed.

Event handling is a complicated subject, and I have given only a short introduction here. As a first step in learning more about events in JavaScript, you might look at the HTML source code for the sample web page canvas2d/EventsStarter.html.