Hello, world!
首先,让我们看看如何将脚本添加到网页上。
对于服务器端环境(如 Node.js),你只需要使用诸如
"node my.js"
的命令行来执行它。
“script” 标签
比如:
<!DOCTYPE HTML>
<html>
<body>
<p>script 标签之前...</p>
<script>
alert('Hello, world!');
</script>
<p>...script 标签之后</p>
</body>
</html>
<script>
标签中包裹了 JavaScript 代码,当浏览器遇到 <script>
标签,代码会自动运行。
现代的标记(markup)
<script>
标签有一些现在很少用到的特性(attribute),但是我们可以在老代码中找到它们:
-
type
特性:<script type=…>
在老的 HTML4 标准中,要求 script 标签有
type
特性。通常是type="text/javascript"
。这样的特性声明现在已经不再需要。而且,现代 HTML 标准已经完全改变了此特性的含义。现在,它可以用于 JavaScript 模块。后续讨论 -
language
特性:<script language=…>
这个特性是为了显示脚本使用的语言。这个特性现在已经没有任何意义,因为语言默认就是 JavaScript。不再需要使用它了。
-
脚本前后的注释。
在非常古老的书籍和指南中,你可能会在
<script>
标签里面找到注释,就像这样:<script type="text/javascript"><!-- ... //--></script>
现代 JavaScript 中已经不这样使用了。这些注释是用于不支持<script>
标签的古老的浏览器隐藏 JavaScript 代码的。
外部脚本
如果你有大量的 JavaScript 代码,我们可以将它放入一个单独的文件。
脚本文件可以通过 src
特性(attribute)添加到 HTML 文件中。
- 绝对路径
- 相对路径
- URL地址
<script src="/path/to/script.js"></script>
这里,/path/to/script.js
是脚本文件从网站根目录开始的绝对路径。当然也可以提供当前页面的相对路径。例如,src ="script.js"
,就像 src="./script.js"
,表示当前文件夹中的 "script.js"
文件。
我们也可以提供一个完整的 URL 地址,例如:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
要附加多个脚本,请使用多个标签:
<script src="/js/script1.js"></script>
<script src="/js/script2.js"></script>
…
请注意:
一般来说,只有最简单的脚本才嵌入到 HTML 中。更复杂的脚本存放在单独的文件中。
使用独立文件的好处是浏览器会下载它,然后将它保存到浏览器的缓存中。
之后,其他页面想要相同的脚本就会从缓存中获取,而不是下载它。所以文件实际上只会下载一次。
这可以节省流量,并使得页面(加载)更快。
如果设置了
src
特性,script
标签内容将会被忽略。一个单独的
<script>
标签不能同时有src
特性和内部包裹的代码。这将不会工作:
<script src="file.js"> alert(1); // 此内容会被忽略,因为设定了 src </script>
这样子可以工作:
<script src="file.js"></script> <script> alert(1); </script>
小结
- 我们可以使用一个
<script>
标签将 JavaScript 代码添加到页面中。 type
和language
特性(attribute)不是必需的。- 外部的脚本可以通过
<script src="path/to/script.js"></script>
的方式插入。
代码结构
我们将要学习的第一个内容就是构建代码块。
语句
语句是执行行为(action)的语法结构和命令。
我们已经见过了 alert('Hello, world!')
这样可以用来显示消息的语句。
我们可以在代码中编写任意数量的语句。语句之间可以使用分号进行分割。
例如,我们将 “Hello World” 这条信息一分为二:
alert('Hello'); alert('World');
通常,每条语句独占一行,以提高代码的可读性:
alert('Hello');
alert('World');
分号
当存在换行符(line break)时,在大多数情况下可以省略分号。
下面的代码也是可以运行的:
alert('Hello')
alert('World')
在这,JavaScript 将换行符理解成“隐式”的分号。这也被称为自动分号插入。
在大多数情况下,换行意味着一个分号。但是“大多数情况”并不意味着“总是”!
有很多换行并不是分号的例子,例如:
alert(3 +
1
+ 2);
代码输出 6
,因为 JavaScript 并没有在这里插入分号。显而易见的是,如果一行以加号 "+"
结尾,那么这是一个“不完整的表达式”,不需要分号。所以,这个例子得到了预期的结果。
但存在 JavaScript 无法确定是否真的需要自动插入分号的情况。
这种情况下发生的错误是很难被找到和解决的。
一个错误的例子
如果你好奇地想知道一个这种错误的具体例子,那你可以看看下面这段代码:
alert("Hello"); [1, 2].forEach(alert);
运行结果:先显示
Hello
,然后显示1
,然后2
。现在删除
alert
语句后的分号:alert("Hello") [1, 2].forEach(alert);
如果我们运行这段代码,只有第一个
Hello
会被显示出来(并且有一个报错,你可能需要打开控制台才能看到它)。并且不会再有数字被显示出来。对于引擎来说,它是这样的:
alert("Hello")[1, 2].forEach(alert);
看起来很奇怪,对吧?在这种情况下,这样将两行代码合并到一起是不对的。我们需要在
alert
后面加一个分号,代码才能正常运行。这也可能发生在其他情况下。
即使语句被换行符分隔了,我们依然建议在它们之间加分号。这个规则被社区广泛采用。我们再次强调一下 —— 大部分时候可以省略分号,但是最好不要省略分号。
注释
可以在脚本任何地方添加注释,因为引擎会直接忽略它们。
单行注释以两个正斜杠字符 //
开始。
这一行的剩余部分是注释。它可能独占一行或者跟随在一条语句的后面。
就像这样:
// 这行注释独占一行
alert('Hello');
alert('World'); // 这行注释跟随在语句后面
多行注释以一个正斜杠和星号开始 “/\*”
并以一个星号和正斜杠结束 “\*/”
。
就像这样:
/* 两个消息的例子。
这是一个多行注释。
*/
alert('Hello');
alert('World');
注释的内容被忽略了,所以如果我们在 /* … */
中放入代码,并不会执行。
使用快捷键!
在大多数的编辑器中,一行代码可以使用 Ctrl+/ 快捷键进行单行注释,诸如 Ctrl+Shift+/ 的快捷键可以进行多行注释(选择代码,然后按下快捷键)。
不支持注释嵌套!
不要在
/*...*/
内嵌套另一个/*...*/
。下面这段代码报错而无法执行:
/* /* 嵌套注释 ?!? */ */ alert( 'World' );
注释会增加代码总量,但有很多工具在代码部署到服务器之前缩减代码。这些工具会移除注释。
现代模式,“use strict”
2009 年 ECMAScript 5 (ES5) 规范增加了新的语言特性并且修改了一些已经存在的特性。为了保证旧的功能能够使用,大部分的修改是默认不生效的。你需要一个特殊的指令 —— "use strict"
来明确地激活这些特性。
“use strict”
这个指令看上去像一个字符串 "use strict"
或者 'use strict'
。当它处于脚本文件的顶部时,则整个脚本文件都将以“现代”模式进行工作。
比如:
"use strict";
// 代码以现代模式工作
...
注意"use strict"
可以被放在函数体的开头。这样则可以只在该函数中启用严格模式。但通常人们会在整个脚本中启用严格模式。
确保 “use strict” 出现在最顶部
请确保
"use strict"
出现在脚本的最顶部,否则严格模式可能无法启用。这里的严格模式就没有被启用:
alert("some code"); // 下面的 "use strict" 会被忽略,必须在最顶部。 "use strict"; // 严格模式没有被激活
只有注释可以出现在
"use strict"
的上面。
没有办法取消
use strict
没有类似于
"no use strict"
这样的指令可以使程序返回默认模式。一旦进入了严格模式,就没有回头路了。
浏览器控制台
当你使用 开发者控制台 运行代码时,请注意它默认是不启动 use strict
的。
有时,当 use strict
会对代码产生一些影响时,你会得到错误的结果。
那么,怎么在控制台中启用 use strict
呢?
首先,你可以尝试搭配使用 Shift+Enter 按键去输入多行代码,然后将 use strict
放在代码最顶部,就像这样:
'use strict'; <Shift+Enter 换行>
// ...你的代码
<按下 Enter 以运行>
它在大部分浏览器中都有效,像 Firefox 和 Chrome。
我们应该使用 “use strict” 吗?
现代 JavaScript 支持 “class” 和 “module” —— 高级语言结构(后续会讲到),它们会自动启用 use strict
。因此,如果我们使用它们,则无需添加 "use strict"
指令。
因此,目前我们欢迎将 "use strict";
写在脚本的顶部。稍后,当你的代码全都写在了 class 和 module 中时,你则可以将 "use strict";
这行代码省略掉。
目前,我们已经基本了解了 use strict
。
变量
变量是用来储存信息的。
变量
变量 是数据的“命名存储”。
在 JavaScript 中创建一个变量,我们需要用到 let
关键字。
下面的语句创建(也可以称为 声明 或者 定义)了一个名称为 “message” 的变量:
let message;
现在,我们可以通过赋值运算符 =
为变量添加一些数据:
let message;
message = 'Hello'; // 将字符串 'Hello' 保存在名为 message 的变量中
现在这个字符串已经保存到与该变量相关联的内存区域了,我们可以通过使用该变量名称访问它:
let message;
message = 'Hello!';
alert(message); // 显示变量内容
简洁一点,我们可以将变量定义和赋值合并成一行:
let message = 'Hello!'; // 定义变量,并且赋值
alert(message); // Hello!
也可以在一行中声明多个变量:
let user = 'John', age = 25, message = 'Hello';
看上去代码长度更短,但并不推荐这样。为了更好的可读性,请一行只声明一个变量。
多行变量声明有点长,但更容易阅读:
let user = 'John';
let age = 25;
let message = 'Hello';
var
而不是let
在较老的脚本中,你也可能发现另一个关键字
var
,而不是let
:var message = 'Hello';
var
关键字与let
大体 相同,也用来声明变量,但稍微有些不同,也有点“老派”。后面会详细介绍他们的!
一个现实生活的类比
如果将变量想象成一个“数据”的盒子,盒子上有一个唯一的标注盒子名字的贴纸。这样我们能更轻松地掌握“变量”的概念。
例如,变量 message
可以被想象成一个标有 "message"
的盒子,盒子里面的值为 "Hello!"
:
我们可以在盒子内放入任何值。
并且,这个盒子的值,我们想改变多少次,就可以改变多少次:
let message;
message = 'Hello!';
message = 'World!'; // 值改变了
alert(message);
当值改变的时候,之前的数据就被从变量中删除了:
我们还可以声明两个变量,然后将其中一个变量的数据拷贝到另一个变量。
let hello = 'Hello world!';
let message;
// 将字符串 'Hello world' 从变量 hello 拷贝到 message
message = hello;
// 现在两个变量保存着相同的数据
alert(hello); // Hello world!
alert(message); // Hello world!
声明两次会触发 error
一个变量应该只被声明一次。
对同一个变量进行重复声明会触发 error:
let message = "This"; // 重复 'let' 会导致 error let message = "That"; // SyntaxError: 'message' has already been declared
因此,我们对同一个变量应该只声明一次,之后在不使用
let
的情况下对其进行引用。
函数式语言
有趣的是,也存在禁止更改变量值的 函数式编程语言。比如 Scala 或 Erlang。
在这种类型的语言中,一旦值保存在盒子中,就永远存在。如果你试图保存其他值,它会强制创建一个新盒子(声明一个新变量),无法重用之前的变量。
虽然第一次看上去有点奇怪,但是这些语言有很大的发展潜力。不仅如此,在某些领域,比如并行计算,这个限制有一定的好处。研究这样的一门语言(即使不打算很快就用上它)有助于开阔视野。
变量命名
JavaScript 的变量命名有两个限制:
- 变量名称必须仅包含字母、数字、符号
$
和_
。 - 首字符必须非数字。
有效的命名,例如:
let userName;
let test123;
如果命名包括多个单词,通常采用驼峰式命名法(camelCase)。也就是,单词一个接一个,除了第一个单词,其他的每个单词都以大写字母开头:myVeryLongName
。
有趣的是,美元符号 '$'
和下划线 '_'
也可以用于变量命名。它们是正常的符号,就跟字母一样,没有任何特殊的含义。
下面的命名是有效的:
let $ = 1; // 使用 "$" 声明一个变量
let _ = 2; // 现在用 "_" 声明一个变量
alert($ + _); // 3
下面的变量命名不正确:
let 1a; // 不能以数字开始
let my-name; // 连字符 '-' 不允许用于变量命名
区分大小写
命名为
apple
和APPLE
的变量是不同的两个变量。
允许非英文字母,但不推荐
可以使用任何语言,包括西里尔字母(cyrillic letters)甚至是象形文字,就像这样:
let имя = '...'; let 我 = '...';
从技术上讲,这样没问题。这样的命名是完全允许的,但是用英文进行变量命名是国际惯例。哪怕我们正在写一个很小的脚本,它也有可能会被使用很久。某个时候,来自其他国家的人可能会需要阅读它。
保留字
有一张 保留字列表,这张表中的保留字无法用作变量命名,因为它们被用于编程语言本身了。
比如,
let
、class
、return
、function
都被保留了。下面的代码将会抛出一个语法错误:
let let = 5; // 不能用 "let" 来命名一个变量,错误! let return = 5; // 同样,不能使用 "return",错误!
未采用
use strict
下的赋值一般,我们需要在使用一个变量前定义它。但是在早期,我们可以不使用
let
进行变量声明,而可以简单地通过赋值来创建一个变量。现在如果我们不在脚本中使用use strict
声明启用严格模式,这仍然可以正常工作,这是为了保持对旧脚本的兼容。// 注意:这个例子中没有 "use strict" num = 5; // 如果变量 "num" 不存在,就会被创建 alert(num); // 5
上面这是个糟糕的做法,严格模式下会报错。
"use strict"; num = 5; // 错误:num 未定义
常量
声明一个常数(不变)变量,可以使用 const
而非 let
:
const myBirthday = '18.04.1982';
使用 const
声明的变量称为“常量”。它们不能被修改,如果你尝试修改就会发现报错:
const myBirthday = '18.04.1982';
myBirthday = '01.01.2001'; // 错误,不能对常量重新赋值
当程序员能确定这个变量永远不会改变的时候,就可以使用 const
来确保这种行为,并且清楚地向别人传递这一事实。
大写形式的常数
一个普遍的做法是将常量用作别名,以便记住那些在执行之前就已知的难以记住的值。
使用大写字母和下划线来命名这些常量。
例如,让我们以所谓的“web”(十六进制)格式为颜色声明常量:
const COLOR_RED = "#F00";
const COLOR_GREEN = "#0F0";
const COLOR_BLUE = "#00F";
const COLOR_ORANGE = "#FF7F00";
// ……当我们需要选择一个颜色
let color = COLOR_ORANGE;
alert(color); // #FF7F00
好处:
COLOR_ORANGE
比"#FF7F00"
更容易记忆。- 比起
COLOR_ORANGE
而言,"#FF7F00"
更容易输错。 - 阅读代码时,
COLOR_ORANGE
比#FF7F00
更易懂。
什么时候该为常量使用大写命名,什么时候进行常规命名?让我们弄清楚一点。
作为一个“常数”,意味着值永远不变。但是有些常量在执行之前就已知了(比如红色的十六进制值),还有些在执行期间被“计算”出来,但初始赋值之后就不会改变。
例如:
const pageLoadTime = /* 网页加载所需的时间 */;
pageLoadTime
的值在页面加载之前是未知的,所以采用常规命名。但是它仍然是个常量,因为赋值之后不会改变。
换句话说,大写命名的常量仅用作“硬编码(hard-coded)”值的别名。
正确命名变量
一些可以遵循的规则:
- 使用易读的命名,比如
userName
或者shoppingCart
。 - 离诸如
a
、b
、c
这种缩写和短名称远一点 - 变量名在能够准确描述变量的同时要足够简洁。不好的例子就是
data
和value
,这样的名称等于什么都没说。 - 脑海中的术语要和团队保持一致。如果网站的访客称为“用户”,则我们采用相关的变量命名,比如
currentUser
或者newUser
,而不要使用currentVisitor
或者一个newManInTown
。
小结
我们可以使用 var
、let
或 const
声明变量来存储数据。
let
— 现代的变量声明方式。var
— 老旧的变量声明方式。一般情况下,我们不会再使用它。const
— 类似于let
,但是变量的值无法被修改。
变量应当以一种容易理解变量内部是什么的方式进行命名。
数据类型
JavaScript 中的值都具有特定的类型。例如,字符串或数字。
在 JavaScript 中有 8 种基本的数据类型(7 种原始类型和 1 种引用类型)。
我们可以将任何类型的值存入变量。例如,一个变量可以在前一刻是个字符串,下一刻就存储一个数字:
// 没有错误
let message = "hello";
message = 123456;
允许这种操作的编程语言,例如 JavaScript,被称为“动态类型”(dynamically typed)的编程语言,简单来说就是变量不会在定义后,被限制为某一数据类型,可以改变。
Number 类型
let n = 123;
n = 12.345;
number 类型代表整数和浮点数。
数字可以有很多操作,比如,乘法 *
、除法 /
、加法 +
、减法 -
等等。
除了常规的数字,还包括所谓的“特殊数值(“special numeric values”)”也属于这种类型:Infinity
、-Infinity
和 NaN
。
-
Infinity
代表数学概念中的 无穷大 ∞。是一个比任何数字都大的特殊值。我们可以通过除以 0 来得到它:
alert( 1 / 0 ); // Infinity
或者在代码中直接使用它:
alert( Infinity ); // Infinity
-
NaN
代表一个计算错误。它是一个不正确的或者一个未定义的数学操作所得到的结果,比如:alert( "not a number" / 2 ); // NaN,这样的除法是错误的
NaN
是粘性的。任何对NaN
的进一步数学运算都会返回NaN
:alert( NaN + 1 ); // NaN alert( 3 * NaN ); // NaN alert( "not a number" / 2 - 1 ); // NaN
所以,如果在数学表达式中有一个
NaN
,会被传播到最终结果(只有一个例外:NaN ** 0
结果为1
)。
数学运算是安全的
在 JavaScript 中做数学运算是安全的。我们可以做任何事:除以 0,将非数字字符串视为数字,等等。
脚本永远不会因为一个致命的错误(“死亡”)而停止。最坏的情况下,我们会得到
NaN
的结果。
特殊的数值属于 “number” 类型。当然,对“特殊的数值”这个词的一般认识是,它们并不是数字。
BigInt 类型
在 JavaScript 中,“number” 类型无法安全地表示大于 (2^53-1)
(即 9007199254740991
),或小于 -(2^53-1)
的整数。
超出安全整数范围 ±(2^53-1)
会出现精度问题,因为并非所有数字都适合固定的 64 位存储。因此,可能存储的是“近似值”。
例如,这两个数字(正好超出了安全整数范围)是相同的:
console.log(9007199254740991 + 1); // 9007199254740992
console.log(9007199254740991 + 2); // 9007199254740992
也就是说,所有大于 (2^53-1)
的奇数都不能用 “number” 类型存储。
在大多数情况下,±(2^53-1)
范围就足够了,但有时候我们需要整个范围非常大的整数,例如用于密码学或微秒精度的时间戳。
BigInt
类型是最近被添加到 JavaScript 语言中的,用于表示任意长度的整数。
可以通过将 n
附加到整数字段的末尾来创建 BigInt
值。
// 尾部的 "n" 表示这是一个 BigInt 类型
const bigInt = 1234567890123456789012345678901234567890n;
由于很少需要 BigInt
类型的数字,所以这里不做过多介绍。
注意:目前 Firefox/Chrome/Edge/Safari 已经支持 BigInt
了,但 IE 还没有。
String 类型
JavaScript 中的字符串必须被括在引号里。
let str = "Hello";
let str2 = 'Single quotes are ok too';
let phrase = `can embed another ${str}`;
在 JavaScript 中,有三种包含字符串的方式。
- 双引号:
"Hello"
. - 单引号:
'Hello'
. - 反引号:
Hello
.
双引号和单引号都是“简单”引用,在 JavaScript 中两者几乎没有什么差别。
反引号是 功能扩展 引号。它们允许我们通过将变量和表达式包装在 ${…}
中,来将它们嵌入到字符串中。例如:
let name = "John";
// 嵌入一个变量
alert( `Hello, ${name}!` ); // Hello, John!
// 嵌入一个表达式
alert( `the result is ${1 + 2}` ); // the result is 3
${…}
内的表达式会被计算,计算结果会成为字符串的一部分。可以在 ${…}
内放置任何东西:诸如名为 name
的变量,或者诸如 1 + 2
的算数表达式,或者其他一些更复杂的。
需要注意的是,这仅仅在反引号内有效,其他引号不允许这种嵌入。
alert( "the result is ${1 + 2}" ); // the result is ${1 + 2}(使用双引号则不会计算 ${…} 中的内容)
JavaScript 中没有 *character* 类型。
在一些语言中,单个字符有一个特殊的 “character” 类型,在 C 语言和 Java 语言中被称为 “char”。
在 JavaScript 中没有这种类型。只有一种
string
类型,一个字符串可以包含零个(为空)、一个或多个字符。
Boolean 类型(逻辑类型)
boolean 类型仅包含两个值:true
和 false
。
这种类型通常用于存储表示 yes 或 no 的值:true
意味着 “yes,正确”,false
意味着 “no,不正确”。
比如:
let nameFieldChecked = true; // yes, name field is checked
let ageFieldChecked = false; // no, age field is not checked
布尔值也可作为比较的结果:
let isGreater = 4 > 1;
alert( isGreater ); // true(比较的结果是 "yes")
null 值
特殊的 null
值不属于上述任何一种类型。
它构成了一个独立的类型,只包含 null
值:
let age = null;
相比较于其他编程语言,JavaScript 中的 null
不是一个“对不存在的 object
的引用”或者 “null 指针”。
JavaScript 中的 null
仅仅是一个代表“无”、“空”或“值未知”的特殊值。
上面的代码表示 age
是未知的。
undefined 值
特殊值 undefined
和 null
一样自成类型。
undefined
的含义是 未被赋值
。
如果一个变量已被声明,但未被赋值,那么它的值就是 undefined
:
let age;
alert(age); // 弹出 "undefined"
从技术上讲,可以显式地将 undefined
赋值给变量:
let age = 100;
// 将值修改为 undefined
age = undefined;
alert(age); // "undefined"
……但是不建议这样做。通常,使用 null
将一个“空”或者“未知”的值写入变量中,而 undefined
则保留作为未进行初始化的事物的默认初始值。
Object 类型和 Symbol 类型
object
类型是一个特殊的类型。
其他所有的数据类型都被称为“原始类型”,因为它们的值只包含一个单独的内容(字符串、数字或者其他)。相反,object
则用于储存数据集合和更复杂的实体。
因为它非常重要,后续会对其进行单独讲解。
symbol
类型用于创建对象的唯一标识符。我们在这里提到 symbol
类型是为了完整性,但我们要在学完 object
类型后再学习它。
typeof 运算符
typeof
运算符返回参数的类型。当我们想要分别处理不同类型值的时候,或者想快速进行数据类型检验时,非常有用。
对 typeof x
的调用会以字符串的形式返回数据类型:
typeof undefined // "undefined"
typeof 0 // "number"
typeof 10n // "bigint"
typeof true // "boolean"
typeof "foo" // "string"
typeof Symbol("id") // "symbol"
typeof Math // "object" (1)
typeof null // "object" (2)
typeof alert // "function" (3)
最后三行可能需要额外的说明:
Math
是一个提供数学运算的内建object
。typeof null
的结果为"object"
。这是官方承认的typeof
的错误,这个问题来自于 JavaScript 语言的早期阶段,并为了兼容性而保留了下来。null
绝对不是一个object
。null
有自己的类型,它是一个特殊值。typeof
的行为在这里是错误的。typeof alert
的结果是"function"
,因为alert
在 JavaScript 语言中是一个函数。后面我们会学习函数,那时我们会了解到,在 JavaScript 语言中没有一个特别的 “function” 类型。函数隶属于object
类型。但是typeof
会对函数区分对待,并返回"function"
。这也是来自于 JavaScript 语言早期的问题。从技术上讲,这种行为是不正确的,但在实际编程中却非常方便。
typeof(x)
语法你可能还会遇到另一种语法:
typeof(x)
。它与typeof x
相同。简单点说:
typeof
是一个操作符,不是一个函数。这里的括号不是typeof
的一部分。它是数学运算分组的括号。通常,这样的括号里包含的是一个数学表达式,例如
(2 + 2)
,但这里它只包含一个参数(x)
。从语法上讲,它们允许在typeof
运算符和其参数之间不打空格,有些人喜欢这样的风格。有些人更喜欢用
typeof(x)
,尽管typeof x
语法更为常见。
小结
JavaScript 中有八种基本的数据类型(前七种为基本数据类型,也称为原始数据类型,而 object
为复杂数据类型)。
- 七种原始数据类型:
number
用于任何类型的数字:整数或浮点数,在±(2^53-1)
范围内的整数。bigint
用于任意长度的整数。string
用于字符串:一个字符串可以包含 0 个或多个字符,所以没有单独的单字符类型。boolean
用于true
和false
。null
用于未知的值 —— 只有一个null
值的独立类型。undefined
用于未定义的值 —— 只有一个undefined
值的独立类型。symbol
用于唯一的标识符。
- 以及一种非原始数据类型:
object
用于更复杂的数据结构。
我们可以通过 typeof
运算符查看存储在变量中的数据类型。
- 通常用作
typeof x
,但typeof(x)
也可行。 - 以字符串的形式返回类型名称,例如
"string"
。 typeof null
会返回"object"
—— 这是 JavaScript 编程语言的一个错误,实际上它并不是一个object
。
后面会先重点介绍原始类型值,掌握了原始数据类型后,再继续学习 object
。
交互:alert、prompt 和 confirm
前文中出现的用户交互的函数:alert
,prompt
和confirm
,在这里介绍一下。
alert
它会显示一条信息,并等待用户按下 “OK”。
例如:
alert("Hello");
弹出的这个带有信息的小窗口被称为 模态窗。“modal” 意味着用户不能与页面的其他部分(例如点击其他按钮等)进行交互,直到他们处理完窗口。在上面示例这种情况下 —— 直到用户点击“确定”按钮。
prompt
prompt
函数接收两个参数:
result = prompt(title, [default]);
浏览器会显示一个带有文本消息的模态窗口,还有 input 框和确定/取消按钮。
-
title
显示给用户的文本
-
default
可选的第二个参数,指定 input 框的初始值。
语法中的方括号
[...]
上述语法中
default
周围的方括号表示该参数是可选的,不是必需的。
访问者可以在提示输入栏中输入一些内容,然后按“确定”键。然后我们在 result
中获取该文本。或者他们可以按取消键或按 Esc 键取消输入,然后我们得到 null
作为 result
。
prompt
将返回用户在 input
框内输入的文本,如果用户取消了输入,则返回 null
。
举个例子:
let age = prompt('How old are you?', 100);
alert(`You are ${age} years old!`); // You are 100 years old!
confirm
语法:
result = confirm(question);
confirm
函数显示一个带有 question
以及确定和取消两个按钮的模态窗口。
点击确定返回 true
,点击取消返回 false
。
例如:
let isBoss = confirm("Are you the boss?");
alert( isBoss ); // 如果“确定”按钮被按下,则显示 true
小结
我们学习了与用户交互的 3 个浏览器的特定函数:
-
alert
显示信息。
-
prompt
显示信息要求用户输入文本。点击确定返回文本,点击取消或按下 Esc 键返回
null
。 -
confirm
显示信息等待用户点击确定或取消。点击确定返回
true
,点击取消或按下 Esc 键返回false
。
这些方法都是模态的:它们暂停脚本的执行,并且不允许用户与该页面的其余部分进行交互,直到窗口被解除。
上述所有方法共有两个限制:
- 模态窗口的确切位置由浏览器决定。通常在页面中心。
- 窗口的确切外观也取决于浏览器。我们不能修改它。
类型转换
大多数情况下,运算符和函数会自动将赋予它们的值转换为正确的类型。
比如,alert
会自动将任何值都转换为字符串以进行显示。算术运算符会将值转换为数字。
在某些情况下,我们需要将值显式地转换为我们期望的类型。
对象还未纳入讨论中
目前,我们将只学习原始类型。
之后,在我们学习完 object 类型后,学习对象 — 原始值转换。
字符串转换
当我们需要一个字符串形式的值时,就会进行字符串转换。
比如,alert(value)
将 value
转换为字符串类型,然后显示这个值。
我们也可以显式地调用 String(value)
来将 value
转换为字符串类型:
let value = true;
alert(typeof value); // boolean
value = String(value); // 现在,值是一个字符串形式的 "true"
alert(typeof value); // string
字符串转换最明显。false
变成 "false"
,null
变成 "null"
等。
数字型转换
在算术函数和表达式中,会自动进行 number 类型转换。
比如,当把除法 /
用于非 number 类型:
alert( "6" / "2" ); // 3, string 类型的值被自动转换成 number 类型后进行计算
我们也可以使用 Number(value)
显式地将这个 value
转换为 number 类型。
let str = "123";
alert(typeof str); // string
let num = Number(str); // 变成 number 类型 123
alert(typeof num); // number
当我们从 string 类型源(如文本表单)中读取一个值,但期望输入一个数字时,通常需要进行显式转换。
如果该字符串不是一个有效的数字,转换的结果会是 NaN
。例如:
let age = Number("an arbitrary string instead of a number");
alert(age); // NaN,转换失败
number 类型转换规则:
值 | 变成…… |
---|---|
undefined | NaN |
null | 0 |
true 和 false | 1 and 0 |
string | 去掉首尾空白字符(空格、换行符 \n 、制表符 \t 等)后的纯数字字符串中含有的数字。如果剩余字符串为空,则转换结果为 0 。否则,将会从剩余字符串中“读取”数字。当类型转换出现 error 时返回 NaN 。 |
例子:
alert( Number(" 123 ") ); // 123
alert( Number(" ") ); // 0
alert( Number("123z") ); // NaN(从字符串“读取”数字,读到 "z" 时出现错误)
alert( Number(true) ); // 1
alert( Number(false) ); // 0
请注意 null
和 undefined
在这有点不同:null
变成数字 0
,undefined
变成 NaN
。
布尔型转换
布尔(boolean)类型转换是最简单的一个。
它发生在逻辑运算中(稍后我们将进行条件判断和其他类似的东西),但是也可以通过调用 Boolean(value) 显式地进行转换。
转换规则如下:
- 直观上为“空”的值(如
0
、空字符串、null
、undefined
和NaN
)将变为false
。 - 其他值变成
true
。
比如:
alert( Boolean(1) ); // true
alert( Boolean(0) ); // false
alert( Boolean("hello") ); // true
alert( Boolean("") ); // false
请注意:包含 0 的字符串
"0"
是true
一些编程语言(比如 PHP)视
"0"
为false
。但在 JavaScript 中,非空的字符串总是true
。alert( Boolean("0") ); // true alert( Boolean(" ") ); // 空格,也是 true(任何非空字符串都是 true)
小结
有三种常用的类型转换:转换为 string 类型、转换为 number 类型和转换为 boolean 类型。
字符串转换 —— 转换发生在输出内容的时候,也可以通过 String(value)
进行显式转换。原始类型值的 string 类型转换通常是很明显的。
数字型转换 —— 转换发生在进行算术操作时,也可以通过 Number(value)
进行显式转换。
数字型转换遵循以下规则:
值 | 变成…… |
---|---|
undefined | NaN |
null | 0 |
true / false | 1 / 0 |
string | “按原样读取”字符串,两端的空白字符(空格、换行符 \n 、制表符 \t 等)会被忽略。空字符串变成 0 。转换出错则输出 NaN 。 |
布尔型转换 —— 转换发生在进行逻辑操作时,也可以通过 Boolean(value)
进行显式转换。
布尔型转换遵循以下规则:
值 | 变成…… |
---|---|
0 , null , undefined , NaN , "" | false |
其他值 | true |
上述的大多数规则都容易理解和记忆。人们通常会犯错误的值得注意的例子有以下几个:
- 对
undefined
进行数字型转换时,输出结果为NaN
,而非0
。 - 对
"0"
和只有空格的字符串(比如:" "
)进行布尔型转换时,输出结果为true
。
基础运算符,数学运算
术语:“一元运算符”,“二元运算符”,“运算元”
在正式开始前,我们先简单浏览一下常用术语。
-
运算元 —— 运算符应用的对象。比如说乘法运算
5 * 2
,有两个运算元:左运算元5
和右运算元2
。有时候人们也称其为“参数”而不是“运算元”。 -
如果一个运算符对应的只有一个运算元,那么它是 一元运算符。比如说一元负号运算符(unary negation)
-
,它的作用是对数字进行正负转换:let x = 1; x = -x; alert( x ); // -1,一元负号运算符生效
-
如果一个运算符拥有两个运算元,那么它是 二元运算符。减号还存在二元运算符形式:
let x = 1, y = 3; alert( y - x ); // 2,二元运算符减号做减运算
严格地说,在上面的示例中,我们使用一个相同的符号表征了两个不同的运算符:负号运算符,即反转符号的一元运算符,减法运算符,是从另一个数减去一个数的二元运算符。
数学运算
支持以下数学运算:
- 加法
+
, - 减法
-
, - 乘法
*
, - 除法
/
, - 取余
%
, - 求幂
**
.
前四个都很简单,而 %
和 **
则需要说一说。
取余 %
取余运算符是 %
,尽管它看起来很像百分数,但实际并无关联。
a % b
的结果是 a
整除 b
的 余数。
例如:
alert( 5 % 2 ); // 1,5 除以 2 的余数
alert( 8 % 3 ); // 2,8 除以 3 的余数
求幂 **
求幂运算 a ** b
将 a
提升至 a
的 b
次幂。
在数学运算中我们将其表示为 ab。
例如:
alert( 2 ** 2 ); // 2² = 4
alert( 2 ** 3 ); // 2³ = 8
alert( 2 ** 4 ); // 2⁴ = 16
就像在数学运算中一样,幂运算也适用于非整数。
例如,平方根是指数为 ½ 的幂运算:
alert( 4 ** (1/2) ); // 2(1/2 次方与平方根相同)
alert( 8 ** (1/3) ); // 2(1/3 次方与立方根相同)
用二元运算符 + 连接字符串
我们来看一些学校算术未涉及的 JavaScript 运算符的特性。
通常,加号 +
用于求和。
但是如果加号 +
被应用于字符串,它将合并(连接)各个字符串:
let s = "my" + "string";
alert(s); // mystring
注意:只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串。
举个例子:
alert( '1' + 2 ); // "12"
alert( 2 + '1' ); // "21"
你看,第一个运算元和第二个运算元,哪个是字符串并不重要。
下面是一个更复杂的例子:
alert(2 + 2 + '1' ); // "41",不是 "221"
在这里,运算符是按顺序工作。第一个 +
将两个数字相加,所以返回 4
,然后下一个 +
将字符串 1
加入其中,所以就是 4 + '1' = '41'
。
alert('1' + 2 + 2); // "122",不是 "14"
这里,第一个操作数是一个字符串,所以编译器将其他两个操作数也视为了字符串。2
被与 '1'
连接到了一起,也就是像 '1' + 2 = "12"
然后 "12" + 2 = "122"
这样。
**二元 +
是唯一一个以这种方式支持字符串的运算符。**其他算术运算符只对数字起作用,并且总是将其运算元转换为数字。
下面是减法和除法运算的示例:
alert( 6 - '2' ); // 4,将 '2' 转换为数字
alert( '6' / '2' ); // 3,将两个运算元都转换为数字
数字转化,一元运算符 +
加号 +
有两种形式。一种是上面我们刚刚讨论的二元运算符,还有一种是一元运算符。
一元运算符加号,或者说,加号 +
应用于单个值,对数字没有任何作用。但是如果运算元不是数字,加号 +
则会将其转化为数字。
例如:
// 对数字无效
let x = 1;
alert( +x ); // 1
let y = -2;
alert( +y ); // -2
// 转化非数字
alert( +true ); // 1
alert( +"" ); // 0
它的效果和 Number(...)
相同,但是更加简短。
我们经常会有将字符串转化为数字的需求。比如,如果我们正在从 HTML 表单中取值,通常得到的都是字符串。如果我们想对它们求和,该怎么办?
二元运算符加号会把它们合并成字符串:
let apples = "2";
let oranges = "3";
alert( apples + oranges ); // "23",二元运算符加号合并字符串
如果我们想把它们当做数字对待,我们需要转化它们,然后再求和:
let apples = "2";
let oranges = "3";
// 在二元运算符加号起作用之前,所有的值都被转化为了数字
alert( +apples + +oranges ); // 5
// 更长的写法
// alert( Number(apples) + Number(oranges) ); // 5
从一个数学家的视角来看,大量的加号可能很奇怪。但是从一个程序员的视角,没什么好奇怪的:一元运算符加号首先起作用,它们将字符串转为数字,然后二元运算符加号对它们进行求和。
为什么一元运算符先于二元运算符作用于运算元?接下去我们将讨论到,这是由于它们拥有 更高的优先级。
运算符优先级
如果一个表达式拥有超过一个运算符,执行的顺序则由 优先级 决定。换句话说,所有的运算符中都隐含着优先级顺序。
从小学开始,我们就知道在表达式 1 + 2 * 2
中,乘法先于加法计算。这就是一个优先级问题。乘法比加法拥有 更高的优先级。
圆括号拥有最高优先级,所以如果我们对现有的运算顺序不满意,我们可以使用圆括号来修改运算顺序,就像这样:(1 + 2) * 2
。
在 JavaScript 中有众多运算符。每个运算符都有对应的优先级数字。数字越大,越先执行。如果优先级相同,则按照由左至右的顺序执行。
这是一个摘抄自 Mozilla 的 优先级表(你没有必要把这全记住,但要记住一元运算符优先级高于二元运算符):
优先级 | 名称 | 符号 |
---|---|---|
… | … | … |
15 | 一元加号 | + |
15 | 一元负号 | - |
14 | 求幂 | ** |
13 | 乘号 | * |
13 | 除号 | / |
12 | 加号 | + |
12 | 减号 | - |
… | … | … |
2 | 赋值符 | = |
… | … | … |
我们可以看到,“一元加号运算符”的优先级是 15
,高于“二元加号运算符”的优先级 12
。这也是为什么表达式 "+apples + +oranges"
中的一元加号先生效,然后才是二元加法。
赋值运算符
我们知道赋值符号 =
也是一个运算符。从优先级表中可以看到它的优先级非常低,只有 2
。
这也是为什么,当我们赋值时,比如 x = 2 * 2 + 1
,所有的计算先执行,然后 =
才执行,将计算结果存储到 x
。
let x = 2 * 2 + 1;
alert( x ); // 5
赋值 = 返回一个值
=
是一个运算符,而不是一个有着“魔法”作用的语言结构。
在 JavaScript 中,所有运算符都会返回一个值。这对于 +
和 -
来说是显而易见的,但对于 =
来说也是如此。
语句 x = value
将值 value
写入 x
然后返回 value。
下面是一个在复杂语句中使用赋值的例子:
let a = 1;
let b = 2;
let c = 3 - (a = b + 1);
alert( a ); // 3
alert( c ); // 0
上面这个例子,(a = b + 1)
的结果是赋给 a
的值(也就是 3
)。然后该值被用于进一步的运算。
不推荐上面这样写。
链式赋值(Chaining assignments)
另一个有趣的特性是链式赋值:
let a, b, c;
a = b = c = 2 + 2;
alert( a ); // 4
alert( b ); // 4
alert( c ); // 4
链式赋值从右到左进行计算。首先,对最右边的表达式 2 + 2
求值,然后将其赋给左边的变量:c
、b
和 a
。最后,所有的变量共享一个值。
同样,出于可读性,最好将这种代码分成几行:
c = 2 + 2;
b = c;
a = c;
这样可读性更强,尤其是在快速浏览代码的时候。
原地修改
我们经常需要对一个变量做运算,并将新的结果存储在同一个变量中。
例如:
let n = 2;
n = n + 5;
n = n * 2;
可以使用运算符 +=
和 *=
来缩写这种表示。
let n = 2;
n += 5; // 现在 n = 7(等同于 n = n + 5)
n *= 2; // 现在 n = 14(等同于 n = n * 2)
alert( n ); // 14
所有算术和位运算符都有简短的“修改并赋值”运算符:/=
和 -=
等。
这类运算符的优先级与普通赋值运算符的优先级相同,所以它们在大多数其他运算之后执行:
let n = 2;
n *= 3 + 5;
alert( n ); // 16 (右边部分先被计算,等同于 n *= 8)
自增/自减
对一个数进行加一、减一是最常见的数学运算符之一。
所以,对此有一些专门的运算符:
-
自增
++
将变量与 1 相加:let counter = 2; counter++; // 和 counter = counter + 1 效果一样,但是更简洁 alert( counter ); // 3
-
自减
--
将变量与 1 相减:let counter = 2; counter--; // 和 counter = counter - 1 效果一样,但是更简洁 alert( counter ); // 1
重要:
自增/自减只能应用于变量。试一下,将其应用于数值(比如
5++
)则会报错。
运算符 ++
和 --
可以置于变量前,也可以置于变量后。
- 当运算符置于变量后,被称为“后置形式”:
counter++
。 - 当运算符置于变量前,被称为“前置形式”:
++counter
。
两者都做同一件事:将变量 counter
与 1
相加。
那么它们有区别吗?有,但只有当我们使用 ++/--
的返回值时才能看到区别。
详细点说。我们知道,所有的运算符都有返回值。自增/自减也不例外。前置形式返回一个新的值,但后置返回原来的值(做加法/减法之前的值)。
为了直观看到区别,看下面的例子:
let counter = 1;
let a = ++counter; // (*)
alert(a); // 2
(*)
所在的行是前置形式 ++counter
,对 counter
做自增运算,返回的是新的值 2
。因此 alert
显示的是 2
。
下面让我们看看后置形式:
let counter = 1;
let a = counter++; // (*) 将 ++counter 改为 counter++
alert(a); // 1
(*)
所在的行是后置形式 counter++
,它同样对 counter
做加法,但是返回的是 旧值(做加法之前的值)。因此 alert
显示的是 1
。
总结:
-
如果自增/自减的值不会被使用,那么两者形式没有区别:
let counter = 0; counter++; ++counter; alert( counter ); // 2,以上两行作用相同
-
如果我们想要对变量进行自增操作,并且 需要立刻使用自增后的值,那么我们需要使用前置形式:
let counter = 0; alert( ++counter ); // 1
-
如果我们想要将一个数加一,但是我们想使用其自增之前的值,那么我们需要使用后置形式:
let counter = 0; alert( counter++ ); // 0
自增/自减和其它运算符的对比
++/--
运算符同样可以在表达式内部使用。它们的优先级比绝大部分的算数运算符要高。举个例子:
let counter = 1; alert( 2 * ++counter ); // 4
与下方例子对比:
let counter = 1; alert( 2 * counter++ ); // 2,因为 counter++ 返回的是“旧值”
尽管从技术层面上来说可行,但是这样的写法会降低代码的可阅读性。在一行上做多个操作 —— 这样并不好。
当阅读代码时,快速的视觉“纵向”扫描会很容易漏掉
counter++
,这样的自增操作并不明显。我们建议用“一行一个行为”的模式:
let counter = 1; alert( 2 * counter ); counter++;
位运算符
位运算符把运算元当做 32 位整数,并在它们的二进制表现形式上操作。
这些运算符不是 JavaScript 特有的。大部分的编程语言都支持这些运算符。
下面是位运算符:
- 按位与 (
&
) - 按位或 (
|
) - 按位异或 (
^
) - 按位非 (
~
) - 左移 (
<<
) - 右移 (
>>
) - 无符号右移 (
>>>
)
不常用。
逗号运算符
逗号运算符 ,
是最少见最不常使用的运算符之一。有时候它会被用来写更简短的代码,因此为了能够理解代码,我们需要了解它。
逗号运算符能让我们处理多个语句,使用 ,
将它们分开。每个语句都运行了,但是只有最后的语句的结果会被返回。
举个例子:
let a = (1 + 2, 3 + 4);
alert( a ); // 7(3 + 4 的结果)
这里,第一个语句 1 + 2
运行了,但是它的结果被丢弃了。随后计算 3 + 4
,并且该计算结果被返回。
逗号运算符的优先级非常低
请注意逗号运算符的优先级非常低,比
=
还要低,因此上面你的例子中圆括号非常重要。如果没有圆括号:
a = 1 + 2, 3 + 4
会先执行+
,将数值相加得到a = 3, 7
,然后赋值运算符=
执行a = 3
,然后逗号之后的数值7
不会再执行,它被忽略掉了。相当于(a = 1 + 2), 3 + 4
。
为什么我们需要这样一个运算符,它只返回最后一个值呢?
有时候,人们会使用它把几个行为放在一行上来进行复杂的运算。
举个例子:
// 一行上有三个运算符
for (a = 1, b = 3, c = a * b; a < 10; a++) {
...
}
值的比较
我们知道,在数学中有很多用于比较大小的运算符。
在 JavaScript 中,它们的编写方式如下:
- 大于 / 小于:
a > b
,a < b
。 - 大于等于 / 小于等于:
a >= b
,a <= b
。 - 检查两个值的相等:
a == b
,请注意双等号==
表示相等性检查,而单等号a = b
表示赋值。 - 检查两个值不相等:不相等在数学中的符号是
≠
,但在 JavaScript 中写成a != b
。
下面我们将进一步了解不同类型的比较,JavaScript 是如何进行比较的,包括一些重要的特殊性。
比较结果为 Boolean 类型
所有比较运算符均返回布尔值:
true
—— 表示“yes(是)”,“correct(正确)”或“the truth(真)”。false
—— 表示“no(否)”,“wrong(错误)”或“not the truth(非真)”。
示例:
alert( 2 > 1 ); // true(正确)
alert( 2 == 1 ); // false(错误)
alert( 2 != 1 ); // true(正确)
和其他类型的值一样,比较的结果可以被赋值给任意变量:
let result = 5 > 4; // 把比较的结果赋值给 result
alert( result ); // true
字符串比较
在比较字符串的大小时,JavaScript 会使用“字典(dictionary)”或“词典(lexicographical)”顺序进行判定。
换言之,字符串是按字符(母)逐个进行比较的。
例如:
alert( 'Z' > 'A' ); // true
alert( 'Glow' > 'Glee' ); // true
alert( 'Bee' > 'Be' ); // true
字符串的比较算法非常简单:
- 首先比较两个字符串的首位字符大小。
- 如果一方字符较大(或较小),则该字符串大于(或小于)另一个字符串。算法结束。
- 否则,如果两个字符串的首位字符相等,则继续取出两个字符串各自的后一位字符进行比较。
- 重复上述步骤进行比较,直到比较完成某字符串的所有字符为止。
- 如果两个字符串的字符同时用完,那么则判定它们相等,否则未结束(还有未比较的字符)的字符串更大。
在上面的第一个例子中,'Z' > 'A'
比较在算法的第 1 步就得到了结果。
在第二个例子中,字符串 Glow
与 Glee
的比较则需要更多步骤,因为需要逐个字符进行比较:
G
和G
相等。l
和l
相等。o
比e
大,算法停止,第一个字符串大于第二个。
非真正的字典顺序,而是 Unicode 编码顺序
在上面的算法中,比较大小的逻辑与字典或电话簿中的排序很像,但也不完全相同。
比如说,字符串比较对字母大小写是敏感的。大写的
"A"
并不等于小写的"a"
。哪一个更大呢?实际上小写的"a"
更大。这是因为在 JavaScript 使用的内部编码表中(Unicode),小写字母的字符索引值更大。
不同类型间的比较
当对不同类型的值进行比较时,JavaScript 会首先将其转化为数字(number)再判定大小。
例如:
alert( '2' > 1 ); // true,字符串 '2' 会被转化为数字 2
alert( '01' == 1 ); // true,字符串 '01' 会被转化为数字 1
对于布尔类型值,true
会被转化为 1
、false
转化为 0
。
例如:
alert( true == 1 ); // true
alert( false == 0 ); // true
一个有趣的现象
有时候,以下两种情况会同时发生:
- 若直接比较两个值,其结果是相等的。
- 若把两个值转为布尔值,它们可能得出完全相反的结果,即一个是
true
,一个是false
。例如:
let a = 0; alert( Boolean(a) ); // false let b = "0"; alert( Boolean(b) ); // true alert(a == b); // true!
对于 JavaScript 而言,这种现象其实挺正常的。因为 JavaScript 会把待比较的值转化为数字后再做比较(因此
"0"
变成了0
)。若只是将一个变量转化为Boolean
值,则会使用其他的类型转换规则。
严格相等
普通的相等性检查 ==
存在一个问题,它不能区分出 0
和 false
:
alert( 0 == false ); // true
也同样无法区分空字符串和 false
:
alert( '' == false ); // true
这是因为在比较不同类型的值时,处于相等判断符号 ==
两侧的值会先被转化为数字。空字符串和 false
也是如此,转化后它们都为数字 0。
如果我们需要区分 0
和 false
,该怎么办?
严格相等运算符 ===
在进行比较时不会做任何的类型转换。
换句话说,如果 a
和 b
属于不同的数据类型,那么 a === b
不会做任何的类型转换而立刻返回 false
。
让我们试试:
alert( 0 === false ); // false,因为被比较值的数据类型不同
同样的,与“不相等”符号 !=
类似,“严格不相等”表示为 !==
。
严格相等的运算符虽然写起来稍微长一些,但是它能够很清楚地显示代码意图,降低你犯错的可能性。
对 null 和 undefined 进行比较
当使用 null
或 undefined
与其他值进行比较时,其返回结果常常出乎你的意料。
-
当使用严格相等
===
比较二者时它们不相等,因为它们属于不同的类型。
alert( null === undefined ); // false
-
当使用非严格相等
==
比较二者时JavaScript 存在一个特殊的规则,会判定它们相等。它们俩就像“一对恋人”,仅仅等于对方而不等于其他任何的值(只在非严格相等下成立)。
alert( null == undefined ); // true
-
当使用数学式或其他比较方法
< > <= >=
时:null/undefined
会被转化为数字:null
被转化为0
,undefined
被转化为NaN
。
下面让我们看看,这些规则会带来什么有趣的现象。
奇怪的结果:null vs 0
通过比较 null
和 0 可得:
alert( null > 0 ); // (1) false
alert( null == 0 ); // (2) false
alert( null >= 0 ); // (3) true
是的,上面的结果完全打破了你对数学的认识。在最后一行代码显示“null
大于等于 0”的情况下,前两行代码中一定会有一个是正确的,然而事实表明它们的结果都是 false。
为什么会出现这种反常结果,这是因为相等性检查 ==
和普通比较符 > < >= <=
的代码逻辑是相互独立的。进行值的比较时,null
会被转化为数字,因此它被转化为了 0
。这就是为什么(3)中 null >= 0
返回值是 true,(1)中 null > 0
返回值是 false。
另一方面,undefined
和 null
在相等性检查 ==
中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中 null == 0
会返回 false。
本人的简单理解:检查和比较是不一样的。独立的。值的比较会发生转化,相等性检查不发生转化。
特立独行的 undefined
undefined
不应该被与其他值进行比较:
alert( undefined > 0 ); // false (1)
alert( undefined < 0 ); // false (2)
alert( undefined == 0 ); // false (3)
为何它看起来如此厌恶 0?返回值都是 false!
原因如下:
(1)
和(2)
都返回false
是因为undefined
在比较中被转换为了NaN
,而NaN
是一个特殊的数值型值,它与任何值进行比较都会返回false
。(3)
返回false
是因为这是一个相等性检查,而undefined
只与null
相等,不会与其他值相等。
避免问题
- 除了严格相等
===
外,其他但凡是有undefined/null
参与的比较,我们都需要格外小心。 - 除非你非常清楚自己在做什么,否则永远不要使用
>= > < <=
去比较一个可能为null/undefined
的变量。对于取值可能是null/undefined
的变量,请按需要分别检查它的取值情况。
小结
- 比较运算符始终返回布尔值。
- 字符串的比较,会按照“词典”顺序逐字符地比较大小。
- 当对不同类型的值进行比较时,它们会先被转化为数字(不包括严格相等检查)再进行比较。
- 在非严格相等
==
下,null
和undefined
相等且各自不等于任何其他的值。 - 在使用
>
或<
进行比较时,需要注意变量可能为null/undefined
的情况。比较好的方法是单独检查变量是否等于null/undefined
。
条件分支:if 和 ‘?’
有时我们需要根据不同条件执行不同的操作。
我们可以使用 if
语句和条件运算符 ?
(也称为“问号”运算符)来实现。
“if” 语句
if(...)
语句计算括号里的条件表达式,如果计算结果是 true
,就会执行对应的代码块。
例如:
let year = prompt('In which year was ECMAScript-2015 specification published?', '');
if (year == 2015) alert( 'You are right!' );
在上面这个例子中,条件是一个简单的相等性检查(year == 2015
),但它还可以更复杂。
如果有多个语句要执行,我们必须将要执行的代码块封装在大括号内:
if (year == 2015) {
alert( "That's correct!" );
alert( "You're so smart!" );
}
建议每次使用 if 语句都用大括号 {}
来包装代码块,即使只有一条语句。这样可以提高代码可读性。
布尔转换
if (…)
语句会计算圆括号内的表达式,并将计算结果转换为布尔型。
回顾一下转换规则:
- 数字
0
、空字符串""
、null
、undefined
和NaN
都会被转换成false
。因为它们被称为“假值(falsy)”。 - 其他值被转换为
true
,所以它们被称为“真值(truthy)”。
所以,下面这个条件下的代码永远不会执行:
if (0) { // 0 是假值(falsy)
...
}
……但下面的条件 —— 始终有效:
if (1) { // 1 是真值(truthy)
...
}
我们也可以将预先计算的布尔值传入 if
语句,像这样:
let cond = (year == 2015); // 相等运算符的结果是 true 或 false
if (cond) {
...
}
“else” 语句
if
语句有时会包含一个可选的 “else” 块。如果判断条件不成立,就会执行它内部的代码。
例如:
let year = prompt('In which year was ECMAScript-2015 specification published?', '');
if (year == 2015) {
alert( 'You guessed it right!' );
} else {
alert( 'How can you be so wrong?' ); // 2015 以外的任何值
}
多个条件:“else if”
有时我们需要测试一个条件的几个变体。我们可以通过使用 else if
子句实现。
例如:
let year = prompt('In which year was ECMAScript-2015 specification published?', '');
if (year < 2015) {
alert( 'Too early...' );
} else if (year > 2015) {
alert( 'Too late' );
} else {
alert( 'Exactly!' );
}
在上面的代码中,JavaScript 先检查 year < 2015
。如果条件不符合,就会转到下一个条件 year > 2015
。如果这个条件也不符合,则会显示最后一个 alert
。
可以有更多的 else if
块。结尾的 else
是可选的。
条件运算符 ‘?’
有时我们需要根据一个条件去赋值一个变量。
如下所示:
let accessAllowed;
let age = prompt('How old are you?', '');
if (age > 18) {
accessAllowed = true;
} else {
accessAllowed = false;
}
alert(accessAllowed);
所谓的“条件”或“问号”运算符让我们可以更简短地达到目的。
这个运算符通过问号 ?
表示。有时它被称为三元运算符,被称为“三元”是因为该运算符中有三个操作数。实际上它是 JavaScript 中唯一一个有这么多操作数的运算符。
语法:
let result = condition ? value1 : value2;
计算条件结果,如果结果为真,则返回 value1
,否则返回 value2
。
例如:
let accessAllowed = (age > 18) ? true : false;
技术上讲,我们可以省略 age > 18
外面的括号。问号运算符的优先级较低,所以它会在比较运算符 >
后执行。
下面这个示例会执行和前面那个示例相同的操作:
// 比较运算符 "age > 18" 首先执行
//(不需要将其包含在括号中)
let accessAllowed = age > 18 ? true : false;
但括号可以使代码可读性更强,所以我们建议使用它们。
请注意:
在上面的例子中,你可以不使用问号运算符,因为比较本身就返回
true/false
:// 下面代码同样可以实现 let accessAllowed = age > 18;
多个 ‘?’
使用一系列问号 ?
运算符可以返回一个取决于多个条件的值。
例如:
let age = prompt('age?', 18);
let message = (age < 3) ? 'Hi, baby!' :
(age < 18) ? 'Hello!' :
(age < 100) ? 'Greetings!' :
'What an unusual age!';
alert( message );
可能很难一下子看出发生了什么。但经过仔细观察,我们可以看到它只是一个普通的检查序列。
- 第一个问号检查
age < 3
。 - 如果为真 — 返回
'Hi, baby!'
。否则,会继续执行冒号":"
后的表达式,检查age < 18
。 - 如果为真 — 返回
'Hello!'
。否则,会继续执行下一个冒号":"
后的表达式,检查age < 100
。 - 如果为真 — 返回
'Greetings!'
。否则,会继续执行最后一个冒号":"
后面的表达式,返回'What an unusual age!'
。
这是使用 if..else
实现上面的逻辑的写法:
if (age < 3) {
message = 'Hi, baby!';
} else if (age < 18) {
message = 'Hello!';
} else if (age < 100) {
message = 'Greetings!';
} else {
message = 'What an unusual age!';
}
‘?’ 的非常规使用
有时可以使用问号 ?
来代替 if
语句:
let company = prompt('Which company created JavaScript?', '');
(company == 'Netscape') ?
alert('Right!') : alert('Wrong.');
根据条件 company =='Netscape'
,要么执行 ?
后面的第一个表达式并显示对应内容,要么执行第二个表达式并显示对应内容。
在这里我们不是把结果赋值给变量。而是根据条件执行不同的代码。
不建议这样使用问号运算符。
这种写法比 if
语句更短,对一些程序员很有吸引力。但它的可读性差。
下面是使用 if
语句实现相同功能的代码,进行下比较:
let company = prompt('Which company created JavaScript?', '');
if (company == 'Netscape') {
alert('Right!');
} else {
alert('Wrong.');
}
因为我们的眼睛垂直扫描代码。所以,跨越几行的代码块比长而水平的代码更易于理解。
总的来说,问号 ?
用于返回其中一个值。当需要执行不同的代码分支时,请使用 if
。
逻辑运算符
JavaScript 中有四个逻辑运算符:||
(或),&&
(与),!
(非),??
(空值合并运算符)。下面先介绍前三个, ??
运算符之后再详细介绍。
虽然它们被称为“逻辑”运算符,但这些运算符却可以被应用于任意类型的值,而不仅仅是布尔值。它们的结果也同样可以是任意类型。
||(或)
两个竖线符号表示“或”运算符:
result = a || b;
在传统的编程中,逻辑或仅能够操作布尔值。如果参与运算的任意一个参数为 true
,返回的结果就为 true
,否则返回 false
。
在 JavaScript 中,逻辑运算符更加灵活强大。但是,首先让我们看一下操作数是布尔值的时候发生了什么。
下面是四种可能的逻辑组合:
alert( true || true ); // true
alert( false || true ); // true
alert( true || false ); // true
alert( false || false ); // false
正如我们所见,除了两个操作数都是 false
的情况,结果都是 true
。
如果操作数不是布尔值,那么它将会被转化为布尔值来参与运算。
例如,数字 1
被作为 true
处理,数字 0
则被作为 false
:
if (1 || 0) { // 工作原理相当于 if( true || false )
alert( 'truthy!' );
}
大多数情况下,逻辑或 ||
会被用在 if
语句中,用来测试是否有 任何 给定的条件为 true
。
例如:
let hour = 9;
if (hour < 10 || hour > 18) {
alert( 'The office is closed.' );
}
我们可以传入更多的条件:
let hour = 12;
let isWeekend = true;
if (hour < 10 || hour > 18 || isWeekend) {
alert( 'The office is closed.' ); // 是周末
}
或运算寻找第一个真值
上文提到的逻辑处理多少有些传统了。下面让我们看看 JavaScript 的“附加”特性。
拓展的算法如下所示。
给定多个参与或运算的值:
result = value1 || value2 || value3;
或运算符 ||
做了如下的事情:
- 从左到右依次计算操作数。
- 处理每一个操作数时,都将其转化为布尔值。如果结果是
true
,就停止计算,返回这个操作数的初始值。 - 如果所有的操作数都被计算过(也就是,转换结果都是
false
),则返回最后一个操作数。
返回的值是操作数的初始形式,不会做布尔转换。
换句话说,一个或运算
||
的链,将返回第一个真值,如果不存在真值,就返回该链的最后一个值。
例如:
alert( 1 || 0 ); // 1(1 是真值)
alert( null || 1 ); // 1(1 是第一个真值)
alert( null || 0 || 1 ); // 1(第一个真值)
alert( undefined || null || 0 ); // 0(都是假值,返回最后一个值)
与“纯粹的、传统的、仅仅处理布尔值的或运算”相比,这个规则就引起了一些很有趣的用法。
-
获取变量列表或者表达式中的第一个真值。
例如,我们有变量
firstName
、lastName
和nickName
,都是可选的(即可以是 undefined,也可以是假值)。我们用或运算
||
来选择有数据的那一个,并显示出来(如果没有设置,则用"Anonymous"
):let firstName = ""; let lastName = ""; let nickName = "SuperCoder"; alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder
如果所有变量的值都为假,结果就是
"Anonymous"
。 -
短路求值(Short-circuit evaluation)。
或运算符
||
的另一个用途是所谓的“短路求值”。这指的是,
||
对其参数进行处理,直到达到第一个真值,然后立即返回该值,而无需处理其他参数。如果操作数不仅仅是一个值,而是一个有副作用的表达式,例如变量赋值或函数调用,那么这一特性的重要性就变得显而易见了。
在下面这个例子中,只会打印第二条信息:
true || alert("not printed"); false || alert("printed");
在第一行中,或运算符
||
在遇到true
时立即停止运算,所以alert
没有运行。有时,人们利用这个特性,只在左侧的条件为假时才执行命令。
&&(与)
两个 & 符号表示 &&
与运算符:
result = a && b;
在传统的编程中,当两个操作数都是真值时,与运算返回 true
,否则返回 false
:
alert( true && true ); // true
alert( false && true ); // false
alert( true && false ); // false
alert( false && false ); // false
带有 if
语句的示例:
let hour = 12;
let minute = 30;
if (hour == 12 && minute == 30) {
alert( 'Time is 12:30' );
}
就像或运算一样,与运算的操作数可以是任意类型的值:
if (1 && 0) { // 作为 true && false 来执行
alert( "won't work, because the result is falsy" );
}
与运算寻找第一个假值
给出多个参加与运算的值:
result = value1 && value2 && value3;
与运算 &&
做了如下的事:
- 从左到右依次计算操作数。
- 在处理每一个操作数时,都将其转化为布尔值。如果结果是
false
,就停止计算,并返回这个操作数的初始值。 - 如果所有的操作数都被计算过(例如都是真值),则返回最后一个操作数。
换句话说,与运算返回第一个假值,如果没有假值就返回最后一个值。
上面的规则和或运算很像。区别就是与运算返回第一个假值,而或运算返回第一个真值。
例如:
// 如果第一个操作数是真值,
// 与运算返回第二个操作数:
alert( 1 && 0 ); // 0
alert( 1 && 5 ); // 5
// 如果第一个操作数是假值,
// 与运算将直接返回它。第二个操作数会被忽略
alert( null && 5 ); // null
alert( 0 && "no matter what" ); // 0
我们也可以在一行代码上串联多个值。查看第一个假值是如何被返回的:
alert( 1 && 2 && null && 3 ); // null
如果所有的值都是真值,最后一个值将会被返回:
alert( 1 && 2 && 3 ); // 3,最后一个值
与运算
&&
在或运算||
之前进行与运算
&&
的优先级比或运算||
要高。所以代码
a && b || c && d
跟&&
表达式加了括号完全一样:(a && b) || (c && d)
。
不要用
||
或&&
来取代if
有时候,有人会将与运算符
&&
作为“简化if
”的一种方式。例如:
let x = 1; (x > 0) && alert( 'Greater than zero!' );
&&
右边的代码只有运算抵达到那里才能被执行。也就是,当且仅当(x > 0)
为真。所以我们基本可以类似地得到:
let x = 1; if (x > 0) alert( 'Greater than zero!' );
虽然使用
&&
写出的变体看起来更短,但if
更明显,并且往往更具可读性。因此,我们建议根据每个语法结构的用途来使用:如果我们想要if
,就使用if
;如果我们想要逻辑与,就使用&&
。
!(非)
感叹符号 !
表示布尔非运算符。
语法相当简单:
result = !value;
逻辑非运算符接受一个参数,并按如下运作:
- 将操作数转化为布尔类型:
true/false
。 - 返回相反的值。
例如:
alert( !true ); // false
alert( !0 ); // true
两个非运算 !!
有时候用来将某个值转化为布尔类型:
alert( !!"non-empty string" ); // true
alert( !!null ); // false
也就是,第一个非运算将该值转化为布尔类型并取反,第二个非运算再次取反。最后我们就得到了一个任意值到布尔值的转化。
有一个略显冗长的方式也可以实现同样的效果 —— 一个内建的 Boolean
函数:
alert( Boolean("non-empty string") ); // true
alert( Boolean(null) ); // false
非运算符 !
的优先级在所有逻辑运算符里面最高,所以它总是在 &&
和 ||
之前执行。
空值合并运算符 ‘??’
最近新增的特性
这是一个最近添加到 JavaScript 的特性。 旧式浏览器可能需要 polyfills.
空值合并运算符(nullish coalescing operator)的写法为两个问号 ??
。
由于它对待 null
和 undefined
的方式类似,所以在本文中我们将使用一个特殊的术语对其进行表示。为简洁起见,当一个值既不是 null
也不是 undefined
时,我们将其称为“已定义的(defined)”。
a ?? b
的结果是:
- 如果
a
是已定义的,则结果为a
, - 如果
a
不是已定义的,则结果为b
。
换句话说,如果第一个参数不是
null/undefined
,则??
返回第一个参数。否则,返回第二个参数。
空值合并运算符并不是什么全新的东西。它只是一种获得两者中的第一个“已定义的”值的不错的语法。
我们可以使用我们已知的运算符重写 result = a ?? b
,像这样:
result = (a !== null && a !== undefined) ? a : b;
现在你应该清楚了 ??
的作用。让我们来看看它的使用场景吧。
??
的常见使用场景是提供默认值。
例如,在这里,如果 user
的值不为 null/undefined
则显示 user
,否则显示 匿名
:
let user;
alert(user ?? "匿名"); // 匿名(user 未定义)
在下面这个例子中,我们将一个名字赋值给了 user
:
let user = "John";
alert(user ?? "匿名"); // John(user 已定义)
我们还可以使用 ??
序列从一系列的值中选择出第一个非 null/undefined
的值。
假设我们在变量 firstName
、lastName
或 nickName
中存储着一个用户的数据。如果用户决定不填写相应的值,则所有这些变量的值都可能是未定义的。
我们想使用这些变量之一显示用户名,如果这些变量的值都是 null/undefined
,则显示 “匿名”。
让我们使用 ??
运算符来实现这一需求:
let firstName = null;
let lastName = null;
let nickName = "Supercoder";
// 显示第一个已定义的值:
alert(firstName ?? lastName ?? nickName ?? "匿名"); // Supercoder
与 || 比较
或运算符 ||
可以以与 ??
运算符相同的方式使用。
例如,在上面的代码中,我们可以用 ||
替换掉 ??
,也可以获得相同的结果:
let firstName = null;
let lastName = null;
let nickName = "Supercoder";
// 显示第一个真值:
alert(firstName || lastName || nickName || "Anonymous"); // Supercoder
纵观 JavaScript 发展史,或 ||
运算符先于 ??
出现。它自 JavaScript 诞生就存在了,因此开发者长期将其用于这种目的。
另一方面,空值合并运算符 ??
是最近才被添加到 JavaScript 中的,它的出现是因为人们对 ||
不太满意。
它们之间重要的区别是:
||
返回第一个 真 值。??
返回第一个 已定义的 值。
换句话说,||
无法区分 false
、0
、空字符串 ""
和 null/undefined
。它们都一样 —— 假值(falsy values)。如果其中任何一个是 ||
的第一个参数,那么我们将得到第二个参数作为结果。
不过在实际中,我们可能只想在变量的值为 null/undefined
时使用默认值。也就是说,当该值确实未知或未被设置时。
例如,考虑下面这种情况:
let height = 0;
alert(height || 100); // 100
alert(height ?? 100); // 0
- height || 100 首先会检查 height 是否为一个假值,它是 0 ,确实是假值。
- 所以,
||
运算的结果为第二个参数,100
。 - height ?? 100 首先会检查 height 是否为 null/undefined ,发现它不是。
- 所以,结果为
height
的原始值,0
。
实际上,高度 0
通常是一个有效值,它不应该被替换为默认值。所以 ??
运算得到的是正确的结果。
优先级
??
运算符的优先级与 ||
相同,它们的的优先级都为 4
。
这意味着,就像 ||
一样,空值合并运算符在 =
和 ?
运算前计算,但在大多数其他运算(例如 +
和 *
)之后计算。
所以我们可能需要在这样的表达式中添加括号:
let height = null;
let width = null;
// 重要:使用括号
let area = (height ?? 100) * (width ?? 50);
alert(area); // 5000
否则,如果我们省略了括号,则由于 *
的优先级比 ??
高,它会先执行,进而导致错误的结果。
// 没有括号
let area = height ?? 100 * width ?? 50;
// ……将这样计算(不符合我们的期望):
let area = height ?? (100 * width) ?? 50;
?? 与 && 或 || 一起使用
出于安全原因,JavaScript 禁止将 ??
运算符与 &&
和 ||
运算符一起使用,除非使用括号明确指定了优先级。
下面的代码会触发一个语法错误:
let x = 1 && 2 ?? 3; // Syntax error
这个限制无疑是值得商榷的,它被添加到语言规范中是为了避免人们从 ||
切换到 ??
时的编程错误。
可以明确地使用括号来解决这个问题:
let x = (1 && 2) ?? 3; // 正常工作了
alert(x); // 2
小结
-
空值合并运算符
??
提供了一种从列表中选择第一个“已定义的”值的简便方式。它被用于为变量分配默认值:
// 当 height 的值为 null 或 undefined 时,将 height 的值设置为 100 height = height ?? 100;
-
??
运算符的优先级非常低,仅略高于?
和=
,因此在表达式中使用它时请考虑添加括号。 -
如果没有明确添加括号,不能将其与
||
或&&
一起使用。
循环:while 和 for
循环 是一种重复运行同一代码的方法。
for…of 和 for…in 循环
本文仅涵盖了基础的循环:
while
,do..while
和for(..; ..; ..)
。如果你阅读本文是为了寻找其他类型的循环,那么:
- 用于遍历对象属性的
for..in
循环- 用于遍历数组和可迭代对象的循环
- 否则,请继续阅读。
“while” 循环
while
循环的语法如下:
while (condition) {
// 代码
// 所谓的“循环体”
}
当 condition
为真时,执行循环体的 code
。
例如,以下将循环输出当 i < 3
时的 i
值:
let i = 0;
while (i < 3) { // 依次显示 0、1 和 2
alert( i );
i++;
}
循环体的单次执行叫作 一次迭代。上面示例中的循环进行了三次迭代。
如果上述示例中没有 i++
,那么循环(理论上)会永远重复执行下去。实际上,浏览器提供了阻止这种循环的方法,我们可以通过终止进程,来停掉服务器端的 JavaScript。
任何表达式或变量都可以是循环条件,而不仅仅是比较。在 while
中的循环条件会被计算,计算结果会被转化为布尔值。
例如,while (i != 0)
可简写为 while (i)
:
let i = 3;
while (i) { // 当 i 变成 0 时,条件为假,循环终止
alert( i );
i--;
}
单行循环体不需要大括号
如果循环体只有一条语句,则可以省略大括号
{…}
:let i = 3; while (i) alert(i--);
“do…while” 循环
使用 do..while
语法可以将条件检查移至循环体 下面:
do {
// 循环体
} while (condition);
循环首先执行循环体,然后检查条件,当条件为真时,重复执行循环体。
例如:
let i = 0;
do {
alert( i );
i++;
} while (i < 3);
这种形式的语法很少使用,除非你希望不管条件是否为真,循环体 至少执行一次。通常我们更倾向于使用另一个形式:while(…) {…}
。
“for” 循环
for
循环更加复杂,但它是最常使用的循环形式。
for
循环看起来就像这样:
for (begin; condition; step) {
// ……循环体……
}
我们通过示例来了解一下这三个部分的含义。下述循环从 i
等于 0
到 3
(但不包括 3
)运行 alert(i)
:
for (let i = 0; i < 3; i++) { // 结果为 0、1、2
alert(i);
}
我们逐个部分分析 for
循环:
语句段 | ||
---|---|---|
begin | let i = 0 | 进入循环时执行一次。 |
condition | i < 3 | 在每次循环迭代之前检查,如果为 false,停止循环。 |
body(循环体) | alert(i) | 条件为真时,重复运行。 |
step | i++ | 在每次循环体迭代后执行。 |
一般循环算法的工作原理如下:
开始运行
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ (如果 condition 成立 → 运行 body 然后运行 step)
→ ...
所以,begin
执行一次,然后进行迭代:每次检查 condition
后,执行 body
和 step
。
如果你这是第一次接触循环,那么回到这个例子,在一张纸上重现它逐步运行的过程,可能会对你有所帮助。
以下是在这个示例中发生的事:
// for (let i = 0; i < 3; i++) alert(i)
// 开始
let i = 0
// 如果条件为真,运行下一步
if (i < 3) { alert(i); i++ }
// 如果条件为真,运行下一步
if (i < 3) { alert(i); i++ }
// 如果条件为真,运行下一步
if (i < 3) { alert(i); i++ }
// ……结束,因为现在 i == 3
内联变量声明
这里“计数”变量
i
是在循环中声明的。这叫做“内联”变量声明。这样的变量只在循环中可见。for (let i = 0; i < 3; i++) { alert(i); // 0, 1, 2 } alert(i); // 错误,没有这个变量。
除了定义一个变量,我们也可以使用现有的变量:
let i = 0; for (i = 0; i < 3; i++) { // 使用现有的变量 alert(i); // 0, 1, 2 } alert(i); //3,可见,因为是在循环之外声明的
省略语句段
for
循环的任何语句段都可以被省略。
例如,如果我们在循环开始时不需要做任何事,我们就可以省略 begin
语句段。
就像这样:
let i = 0; // 我们已经声明了 i 并对它进行了赋值
for (; i < 3; i++) { // 不再需要 "begin" 语句段
alert( i ); // 0, 1, 2
}
我们也可以移除 step
语句段:
let i = 0;
for (; i < 3;) {
alert( i++ );
}
该循环与 while (i < 3)
等价。
实际上我们可以删除所有内容,从而创建一个无限循环:
for (;;) {
// 无限循环
}
请注意 for
的两个 ;
必须存在,否则会出现语法错误。
跳出循环
通常条件为假时,循环会终止。
但我们随时都可以使用 break
指令强制退出。
例如,下面这个循环要求用户输入一系列数字,在输入的内容不是数字时“终止”循环。
let sum = 0;
while (true) {
let value = +prompt("Enter a number", '');
if (!value) break; // (*)
sum += value;
}
alert( 'Sum: ' + sum );
如果用户输入空行或取消输入,在 (*)
行的 break
指令会被激活。它立刻终止循环,将控制权传递给循环后的第一行,即,alert
。
根据需要,“无限循环 + break
” 的组合非常适用于不必在循环开始/结束时检查条件,但需要在中间甚至是主体的多个位置进行条件检查的情况。
继续下一次迭代
continue
指令是 break
的“轻量版”。它不会停掉整个循环。而是停止当前这一次迭代,并强制启动新一轮循环(如果条件允许的话)。
如果我们完成了当前的迭代,并且希望继续执行下一次迭代,我们就可以使用它。
下面这个循环使用 continue
来只输出奇数:
for (let i = 0; i < 10; i++) {
//如果为真,跳过循环体的剩余部分。
if (i % 2 == 0) continue;
alert(i); // 1,然后 3,5,7,9
}
对于偶数的 i
值,continue
指令会停止本次循环的继续执行,将控制权传递给下一次 for
循环的迭代(使用下一个数字)。因此 alert
仅被奇数值调用。
continue
指令利于减少嵌套显示奇数的循环可以像下面这样:
for (let i = 0; i < 10; i++) { if (i % 2) { alert( i ); } }
从技术角度看,它与上一个示例完全相同。当然,我们可以将代码包装在
if
块而不使用continue
。但在副作用方面,它多创建了一层嵌套(大括号内的
alert
调用)。如果if
中代码有多行,则可能会降低代码整体的可读性。
禁止
break/continue
在 ‘?’ 的右边请注意非表达式的语法结构不能与三元运算符
?
一起使用。特别是break/continue
这样的指令是不允许这样使用的。例如,我们使用如下代码:
if (i > 5) { alert(i); } else { continue; }
……用问号重写:
(i > 5) ? alert(i) : continue; // continue 不允许在这个位置
……代码会停止运行,并显示有语法错误。
这是不(建议)使用问号
?
运算符替代if
语句的另一个原因。
break/continue 标签
有时候我们需要一次从多层嵌套的循环中跳出来。
例如,下述代码中我们的循环使用了 i
和 j
,从 (0,0)
到 (3,3)
提示坐标 (i, j)
:
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
let input = prompt(`Value at coords (${i},${j})`, '');
// 如果我想从这里退出并直接执行 alert('Done!')
}
}
alert('Done!');
我们需要提供一种方法,以在用户取消输入时来停止这个过程。
在 input
之后的普通 break
只会打破内部循环。这还不够 —— 标签可以实现这一功能!
标签 是在循环之前带有冒号的标识符:
labelName: for (...) {
...
}
break <labelName>
语句跳出循环至标签处:
outer: for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
let input = prompt(`Value at coords (${i},${j})`, '');
// 如果是空字符串或被取消,则中断并跳出这两个循环。
if (!input) break outer; // (*)
// 用得到的值做些事……
}
}
alert('Done!');
上述代码中,break outer
向上寻找名为 outer
的标签并跳出当前循环。
因此,控制权直接从 (*)
转至 alert('Done!')
。
我们还可以将标签移至单独一行:
outer:
for (let i = 0; i < 3; i++) { ... }
continue
指令也可以与标签一起使用。在这种情况下,执行跳转到标记循环的下一次迭代。
标签并不允许“跳到”所有位置
标签不允许我们跳到代码的任意位置。
例如,这样做是不可能的:
break label; // 跳转至下面的 label 处(无效) label: for (...)
break
指令必须在代码块内。从技术上讲,任何被标记的代码块都有效,例如:label: { // ... break label; // 有效 // ... }
……尽管 99.9% 的情况下
break
都被用在循环内,就像在上面那些例子中我们看到的那样。
continue
只有在循环内部才可行。
小结
我们学习了三种循环:
while
—— 每次迭代之前都要检查条件。do..while
—— 每次迭代后都要检查条件。for (;;)
—— 每次迭代之前都要检查条件,可以使用其他设置。
通常使用 while(true)
来构造“无限”循环。这样的循环和其他循环一样,都可以通过 break
指令来终止。
如果我们不想在当前迭代中做任何事,并且想要转移至下一次迭代,那么可以使用 continue
指令。
break/continue
支持循环前的标签。标签是 break/continue
跳出嵌套循环以转到外部的唯一方法。
“switch” 语句
switch
语句可以替代多个 if
判断。
switch
语句为多分支选择的情况提供了一个更具描述性的方式。
语法
switch
语句有至少一个 case
代码块和一个可选的 default
代码块。
就像这样:
switch(x) {
case 'value1': // if (x === 'value1')
...
[break]
case 'value2': // if (x === 'value2')
...
[break]
default:
...
[break]
}
- 比较
x
值与第一个case
(也就是value1
)是否严格相等,然后比较第二个case
(value2
)以此类推。 - 如果相等,
switch
语句就执行相应case
下的代码块,直到遇到最靠近的break
语句(或者直到switch
语句末尾)。 - 如果没有符合的 case,则执行
default
代码块(如果default
存在)。
举个例子
switch
的例子(高亮的部分是执行的 case
部分):
let a = 2 + 2;
switch (a) {
case 3:
alert( 'Too small' );
break;
case 4:
alert( 'Exactly!' );
break;
case 5:
alert( 'Too big' );
break;
default:
alert( "I don't know such values" );
}
这里的 switch
从第一个 case
分支开始将 a
的值与 case
后的值进行比较,第一个 case
后的值为 3
匹配失败。
然后比较 4
。匹配,所以从 case 4
开始执行直到遇到最近的 break
。
如果没有 break
,程序将不经过任何检查就会继续执行下一个 case
。
无 break
的例子:
let a = 2 + 2;
switch (a) {
case 3:
alert( 'Too small' );
case 4:
alert( 'Exactly!' );
case 5:
alert( 'Too big' );
default:
alert( "I don't know such values" );
}
在上面的例子中我们会看到连续执行的三个 alert
:
alert( 'Exactly!' );
alert( 'Too big' );
alert( "I don't know such values" );
任何表达式都可以成为
switch/case
的参数
switch
和case
都允许任意表达式。比如:
let a = "1"; let b = 0; switch (+a) { case b + 1: alert("this runs, because +a is 1, exactly equals b+1"); break; default: alert("this doesn't run"); }
这里
+a
返回1
,这个值跟case
中b + 1
相比较,然后执行对应的代码。
“case” 分组
共享同一段代码的几个 case
分支可以被分为一组:
比如,如果我们想让 case 3
和 case 5
执行同样的代码:
let a = 3;
switch (a) {
case 4:
alert('Right!');
break;
case 3: // (*) 下面这两个 case 被分在一组
case 5:
alert('Wrong!');
alert("Why don't you take a math class?");
break;
default:
alert('The result is strange. Really.');
}
现在 3
和 5
都显示相同的信息。
switch/case
有通过 case 进行“分组”的能力,其实是 switch 语句没有 break
时的副作用。因为没有 break
,case 3
会从 (*)
行执行到 case 5
。
类型很关键
强调一下,这里的相等是严格相等。被比较的值必须是相同的类型才能进行匹配。
比如,我们来看下面的代码:
let arg = prompt("Enter a value?")
switch (arg) {
case '0':
case '1':
alert( 'One or zero' );
break;
case '2':
alert( 'Two' );
break;
case 3:
alert( 'Never executes!' );
break;
default:
alert( 'An unknown value' )
}
- 在
prompt
对话框输入0
、1
,第一个alert
弹出。 - 输入
2
,第二个alert
弹出。 - 但是输入
3
,因为prompt
的结果是字符串类型的"3"
,不严格相等===
于数字类型的3
,所以case 3
不会执行!因此case 3
部分是一段无效代码。所以会执行default
分支。
函数
函数是程序的主要“构建模块”。函数使该段代码可以被调用很多次,而不需要写重复的代码。
我们已经看到了内建函数的示例,如 alert(message)
、prompt(message, default)
和 confirm(question)
。但我们也可以创建自己的函数。
函数声明
使用 函数声明 创建函数。
看起来就像这样:
function showMessage() {
alert( 'Hello everyone!' );
}
function
关键字首先出现,然后是 函数名,然后是括号之间的 参数 列表(用逗号分隔,在上述示例中为空,我们将在接下来的示例中看到),最后是花括号之间的代码(即“函数体”)。
function name(parameter1, parameter2, ... parameterN) {
...body...
}
我们的新函数可以通过名称调用:showMessage()
。
例如:
function showMessage() {
alert( 'Hello everyone!' );
}
showMessage();
showMessage();
调用 showMessage()
执行函数的代码。这里我们会看到显示两次消息。
这个例子清楚地演示了函数的主要目的之一:避免代码重复。
如果我们需要更改消息或其显示方式,只需在一个地方修改代码:输出它的函数。
局部变量
在函数中声明的变量只在该函数内部可见。
例如:
function showMessage() {
let message = "Hello, I'm JavaScript!"; // 局部变量
alert( message );
}
showMessage(); // Hello, I'm JavaScript!
alert( message ); // <-- 错误!变量是函数的局部变量
外部变量
函数也可以访问外部变量,例如:
let userName = 'John';
function showMessage() {
let message = 'Hello, ' + userName;
alert(message);
}
showMessage(); // Hello, John
函数对外部变量拥有全部的访问权限。函数也可以修改外部变量。
例如:
let userName = 'John';
function showMessage() {
userName = "Bob"; // (1) 改变外部变量
let message = 'Hello, ' + userName;
alert(message);
}
alert( userName ); // John 在函数调用之前
showMessage();
alert( userName ); // Bob,值被函数修改了
只有在没有局部变量的情况下才会使用外部变量。
如果在函数内部声明了同名变量,那么函数会 遮蔽 外部变量。例如,在下面的代码中,函数使用局部的 userName
,而外部变量被忽略:
let userName = 'John';
function showMessage() {
let userName = "Bob"; // 声明一个局部变量
let message = 'Hello, ' + userName; // Bob
alert(message);
}
// 函数会创建并使用它自己的 userName
showMessage();
alert( userName ); // John,未被更改,函数没有访问外部变量。
全局变量
任何函数之外声明的变量,例如上述代码中的外部变量
userName
,都被称为 全局 变量。全局变量在任意函数中都是可见的(除非被局部变量遮蔽)。
减少全局变量的使用是一种很好的做法。现代的代码有很少甚至没有全局变量。大多数变量存在于它们的函数中。但是有时候,全局变量能够用于存储项目级别的数据。
参数
我们可以通过参数将任意数据传递给函数。
在如下示例中,函数有两个参数:from
和 text
。
function showMessage(from, text) { // 参数:from 和 text
alert(from + ': ' + text);
}
showMessage('Ann', 'Hello!'); // Ann: Hello! (*)
showMessage('Ann', "What's up?"); // Ann: What's up? (**)
当函数在 (*)
和 (**)
行中被调用时,给定值被复制到了局部变量 from
和 text
。然后函数使用它们进行计算。
这里还有一个例子:我们有一个变量 from
,并将它传递给函数。请注意:函数会修改 from
,但在函数外部看不到更改,因为函数修改的是复制的变量值副本:
function showMessage(from, text) {
from = '*' + from + '*'; // 让 "from" 看起来更优雅
alert( from + ': ' + text );
}
let from = "Ann";
showMessage(from, "Hello"); // *Ann*: Hello
// "from" 值相同,函数修改了一个局部的副本。
alert( from ); // Ann
当一个值被作为函数参数(parameter)传递时,它也被称为 参数(argument)。
换一种方式,我们把这些术语搞清楚:
- 参数(parameter)是函数声明中括号内列出的变量(它是函数声明时的术语)。
- 参数(argument)是调用函数时传递给函数的值(它是函数调用时的术语)。
我们声明函数时列出它们的参数(parameters),然后调用它们传递参数(arguments)。
在上面的例子中,我们可以说:“函数 showMessage
被声明,并且带有两个参数(parameters),随后它被调用,两个参数(arguments)分别为 from
和 "Hello"
”。
默认值
如果一个函数被调用,但有参数(argument)未被提供,那么相应的值就会变成 undefined
。
例如,之前提到的函数 showMessage(from, text)
可以只使用一个参数(argument)调用:
showMessage("Ann");
那不是错误,这样调用将输出 "*Ann*: undefined"
。因为参数 text
的值未被传递,所以变成了 undefined
。
我们可以使用 =
为函数声明中的参数指定所谓的“默认”(如果对应参数的值未被传递则使用)值:
function showMessage(from, text = "no text given") {
alert( from + ": " + text );
}
showMessage("Ann"); // Ann: no text given
现在如果 text
参数未被传递,它将会得到值 "no text given"
。
这里 "no text given"
是一个字符串,但它可以是更复杂的表达式,并且只会在缺少参数时才会被计算和分配。所以,这也是可能的:
function showMessage(from, text = anotherFunction()) {
// anotherFunction() 仅在没有给定 text 时执行
// 其运行结果将成为 text 的值
}
默认参数的计算
在 JavaScript 中,每次函数在没带个别参数的情况下被调用,默认参数会被计算出来。
在上面的例子中,如果传递了参数
text
,那么anotherFunction()
就不会被调用。如果没传递参数
text
,那么anotherFunction()
就会被调用。
在 JavaScript 老代码中的默认参数
几年前,JavaScript 不支持默认参数的语法。所以人们使用其他方式来设置默认参数。
如今,我们会在旧代码中看到它们。
例如,显式地检查
undefined
:function showMessage(from, text) { if (text === undefined) { text = 'no text given'; } alert( from + ": " + text ); }
……或者使用
||
运算符:function showMessage(from, text) { // 如果 text 的值为假值,则分配默认值 // 这样赋值 text == "" 与 text 无值相同 text = text || 'no text given'; ... }
后备的默认参数
有些时候,将参数默认值的设置放在函数执行(相较更后期)而不是函数声明时,也行得通。
我们可以通过将参数与 undefined
进行比较,来检查该参数是否在函数执行期间被传递进来:
function showMessage(text) {
// ...
if (text === undefined) { // 如果参数未被传递进来
text = 'empty message';
}
alert(text);
}
showMessage(); // empty message
……或者我们可以使用 ||
运算符:
function showMessage(text) {
// 如果 text 为 undefined 或者为假值,那么将其赋值为 'empty'
text = text || 'empty';
...
}
现代 JavaScript 引擎支持 空值合并运算符 ??
,它在大多数假值(例如 0
)应该被视为“正常值”时更具优势:
function showCount(count) {
// 如果 count 为 undefined 或 null,则提示 "unknown"
alert(count ?? "unknown");
}
showCount(0); // 0
showCount(null); // unknown
showCount(); // unknown
返回值
函数可以将一个值返回到调用代码中作为结果。
最简单的例子是将两个值相加的函数:
function sum(a, b) {
return a + b;
}
let result = sum(1, 2);
alert( result ); // 3
指令 return
可以在函数的任意位置。当执行到达时,函数停止,并将值返回给调用代码(分配给上述代码中的 result
)。
在一个函数中可能会出现很多次 return
。例如:
function checkAge(age) {
if (age >= 18) {
return true;
} else {
return confirm('Got a permission from the parents?');
}
}
let age = prompt('How old are you?', 18);
if ( checkAge(age) ) {
alert( 'Access granted' );
} else {
alert( 'Access denied' );
}
只使用 return
但没有返回值也是可行的。但这会导致函数立即退出。
例如:
function showMovie(age) {
if ( !checkAge(age) ) {
return;
}
alert( "Showing you the movie" ); // (*)
// ...
}
在上述代码中,如果 checkAge(age)
返回 false
,那么 showMovie
将不会运行到 alert
。
空值的
return
或没有return
的函数返回值为undefined
如果函数无返回值,它就会像返回
undefined
一样:function doNothing() { /* 没有代码 */ } alert( doNothing() === undefined ); // true
空值的
return
和return undefined
等效:function doNothing() { return; } alert( doNothing() === undefined ); // true
不要在
return
与返回值之间添加新行对于
return
的长表达式,可能你会很想将其放在单独一行,如下所示:return (some + long + expression + or + whatever * f(a) + f(b))
但这不行,因为 JavaScript 默认会在
return
之后加上分号。上面这段代码和下面这段代码运行流程相同:return; (some + long + expression + or + whatever * f(a) + f(b))
因此,实际上它的返回值变成了空值。
如果我们想要将返回的表达式写成跨多行的形式,那么应该在
return
的同一行开始写此表达式。或者至少按照如下的方式放上左括号:return ( some + long + expression + or + whatever * f(a) + f(b) )
然后它就能像我们预想的那样正常运行了。
函数命名
函数就是行为(action)。所以它们的名字通常是动词。它应该简短且尽可能准确地描述函数的作用。这样读代码的人就能清楚地知道这个函数的功能。
一种普遍的做法是用动词前缀来开始一个函数,这个前缀模糊地描述了这个行为。团队内部必须就前缀的含义达成一致。
例如,以 "show"
开头的函数通常会显示某些内容。
函数以 XX 开始……
"get…"
—— 返回一个值,"calc…"
—— 计算某些内容,"create…"
—— 创建某些内容,"check…"
—— 检查某些内容并返回 boolean 值,等。
这类名字的示例:
showMessage(..) // 显示信息
getAge(..) // 返回 age(gets it somehow)
calcSum(..) // 计算求和并返回结果
createForm(..) // 创建表单(通常会返回它)
checkPermission(..) // 检查权限并返回 true/false
有了前缀,只需瞥一眼函数名,就可以了解它的功能是什么,返回什么样的值。
一个函数 —— 一个行为
一个函数应该只包含函数名所指定的功能,而不是做更多与函数名无关的功能。
两个独立的行为通常需要两个函数,即使它们通常被一起调用(在这种情况下,我们可以创建第三个函数来调用这两个函数)。
有几个违反这一规则的例子:
getAge
—— 如果它通过alert
将 age 显示出来,那就有问题了(只应该是获取)。createForm
—— 如果它包含修改文档的操作,例如向文档添加一个表单,那就有问题了(只应该创建表单并返回)。checkPermission
—— 如果它显示access granted/denied
消息,那就有问题了(只应执行检查并返回结果)。这些例子假设函数名前缀具有通用的含义。你和你的团队可以自定义这些函数名前缀的含义,但是通常都没有太大的不同。无论怎样,你都应该对函数名前缀的含义、带特定前缀的函数可以做什么以及不可以做什么有深刻的了解。所有相同前缀的函数都应该遵守相同的规则。并且,团队成员应该形成共识。
非常短的函数命名
常用的函数有时会有非常短的名字。
例如,jQuery 框架用
$
定义一个函数。LoDash 库的核心函数用_
命名。这些都是例外,一般而言,函数名应简明扼要且具有描述性。
函数 == 注释
函数应该简短且只有一个功能。如果这个函数功能复杂,那么把该函数拆分成几个小的函数是值得的。有时候遵循这个规则并不是那么容易,但这绝对是件好事。
一个单独的函数不仅更容易测试和调试 —— 它的存在本身就是一个很好的注释!
例如,比较如下两个函数 showPrimes(n)
。它们的功能都是输出到 n
的 素数。
第一个变体使用了一个标签:
function showPrimes(n) {
nextPrime: for (let i = 2; i < n; i++) {
for (let j = 2; j < i; j++) {
if (i % j == 0) continue nextPrime;
}
alert( i ); // 一个素数
}
}
第二个变体使用附加函数 isPrime(n)
来检验素数:
function showPrimes(n) {
for (let i = 2; i < n; i++) {
if (!isPrime(i)) continue;
alert(i); // 一个素数
}
}
function isPrime(n) {
for (let i = 2; i < n; i++) {
if ( n % i == 0) return false;
}
return true;
}
第二个变体更容易理解,不是吗?我们通过函数名(isPrime
)就可以看出函数的行为,而不需要通过代码。人们通常把这样的代码称为 自描述。
因此,即使我们不打算重用它们,也可以创建函数。函数可以让代码结构更清晰,可读性更强。
总结
函数声明方式如下所示:
function name(parameters, delimited, by, comma) {
/* code */
}
- 作为参数传递给函数的值,会被复制到函数的局部变量。
- 函数可以访问外部变量。但它只能从内到外起作用。函数外部的代码看不到函数内的局部变量。
- 函数可以返回值。如果没有返回值,则其返回的结果是
undefined
。
为了使代码简洁易懂,建议在函数中主要使用局部变量和参数,而不是外部变量。
与不获取参数但将修改外部变量作为副作用的函数相比,获取参数、使用参数并返回结果的函数更容易理解。
函数命名:
- 函数名应该清楚地描述函数的功能。当我们在代码中看到一个函数调用时,一个好的函数名能够让我们马上知道这个函数的功能是什么,会返回什么。
- 一个函数是一个行为,所以函数名通常是动词。
- 目前有许多优秀的函数名前缀,如
create…
、show…
、get…
、check…
等等。使用它们来提示函数的作用吧。
函数表达式
在 JavaScript 中,函数不是“神奇的语言结构”,而是一种特殊的值。
我们在前面章节使用的语法称为 函数声明:
function sayHi() {
alert( "Hello" );
}
另一种创建函数的语法称为 函数表达式。
它允许我们在任何表达式的中间创建一个新函数。
例如:
let sayHi = function() {
alert( "Hello" );
};
在这里我们可以看到变量 sayHi
得到了一个值,新函数 function() { alert("Hello"); }
。
由于函数创建发生在赋值表达式的上下文中(在 =
的右侧),因此这是一个 函数表达式。
请注意,function
关键字后面没有函数名。函数表达式允许省略函数名。
这里我们立即将它赋值给变量,所以上面的两个代码示例的含义是一样的:“创建一个函数并将其放入变量 sayHi
中”。
在更多更高阶的情况下,稍后我们会遇到,可以创建一个函数并立即调用,或者安排稍后执行,而不是存储在任何地方,因此保持匿名。
函数是一个值
重申一次:无论函数是如何创建的,函数都是一个值。上面的两个示例都在 sayHi
变量中存储了一个函数。
我们还可以用 alert
显示这个变量的值:
function sayHi() {
alert( "Hello" );
}
alert( sayHi ); // 显示函数代码
注意,最后一行代码并不会运行函数,因为 sayHi
后没有括号。在某些编程语言中,只要提到函数的名称都会导致函数的调用执行,但 JavaScript 可不是这样。
在 JavaScript 中,函数是一个值,所以我们可以把它当成值对待。上面代码显示了一段字符串值,即函数的源码。
的确,在某种意义上说一个函数是一个特殊值,我们可以像 sayHi()
这样调用它。
但它依然是一个值,所以我们可以像使用其他类型的值一样使用它。
我们可以复制函数到其他变量:
function sayHi() { // (1) 创建
alert( "Hello" );
}
let func = sayHi; // (2) 复制
func(); // Hello // (3) 运行复制的值(正常运行)!
sayHi(); // Hello // 这里也能运行(为什么不行呢)
解释一下上段代码发生的细节:
(1)
行声明创建了函数,并把它放入到变量sayHi
。(2)
行将sayHi
复制到了变量func
。请注意:sayHi
后面没有括号。如果有括号,func = sayHi()
会把sayHi()
的调用结果写进func
,而不是sayHi
函数 本身。- 现在函数可以通过
sayHi()
和func()
两种方式进行调用。
我们也可以在第一行中使用函数表达式来声明 sayHi
:
let sayHi = function() { // (1) 创建
alert( "Hello" );
};
let func = sayHi;
// ...
这两种声明的函数是一样的。
为什么这里末尾会有个分号?
你可能想知道,为什么函数表达式结尾有一个分号
;
,而函数声明没有:function sayHi() { // ... } let sayHi = function() { // ... };
答案很简单:这里函数表达式是在赋值语句
let sayHi = ...;
中以function(…) {…}
的形式创建的。建议在语句末尾加上分号;
,它不是函数语法的一部分。分号用于更简单的赋值,例如
let sayHi = 5;
,它也用于函数赋值。
回调函数
让我们多举几个例子,看看如何将函数作为值来传递以及如何使用函数表达式。
我们写一个包含三个参数的函数 ask(question, yes, no)
:
-
question
关于问题的文本
-
yes
当回答为 “Yes” 时,要运行的脚本
-
no
当回答为 “No” 时,要运行的脚本
函数需要提出 question
(问题),并根据用户的回答,调用 yes()
或 no()
:
function ask(question, yes, no) {
if (confirm(question)) yes()
else no();
}
function showOk() {
alert( "You agreed." );
}
function showCancel() {
alert( "You canceled the execution." );
}
// 用法:函数 showOk 和 showCancel 被作为参数传入到 ask
ask("Do you agree?", showOk, showCancel);
在实际开发中,这样的函数是非常有用的。实际开发与上述示例最大的区别是,实际开发中的函数会通过更加复杂的方式与用户进行交互,而不是通过简单的 confirm
。在浏览器中,这样的函数通常会绘制一个漂亮的提问窗口。但这是另外一件事了。
ask
的两个参数值 showOk
和 showCancel
可以被称为 回调函数 或简称 回调。
主要思想是我们传递一个函数,并期望在稍后必要时将其“回调”。在我们的例子中,showOk
是回答 “yes” 的回调,showCancel
是回答 “no” 的回调。
我们可以使用函数表达式来编写一个等价的、更简洁的函数:
function ask(question, yes, no) {
if (confirm(question)) yes()
else no();
}
ask(
"Do you agree?",
function() { alert("You agreed."); },
function() { alert("You canceled the execution."); }
);
这里直接在 ask(...)
调用内进行函数声明。这两个函数没有名字,所以叫 匿名函数。这样的函数在 ask
外无法访问(因为没有对它们分配变量),不过这正是我们想要的。
这样的代码在我们的脚本中非常常见,这正符合 JavaScript 语言的思想。
一个函数是表示一个“行为”的值
字符串或数字等常规值代表 数据。
函数可以被视为一个 行为(action)。
我们可以在变量之间传递它们,并在需要时运行。
函数表达式 vs 函数声明
让我们来总结一下函数声明和函数表达式之间的主要区别。
首先是语法:如何通过代码对它们进行区分。
-
函数声明:在主代码流中声明为单独的语句的函数:
// 函数声明 function sum(a, b) { return a + b; }
-
函数表达式:在一个表达式中或另一个语法结构中创建的函数。下面这个函数是在赋值表达式
=
右侧创建的:// 函数表达式 let sum = function(a, b) { return a + b; };
更细微的差别是,JavaScript 引擎会在 什么时候 创建函数。
函数表达式是在代码执行到达时被创建,并且仅从那一刻起可用。
一旦代码执行到赋值表达式 let sum = function…
的右侧,此时就会开始创建该函数,并且可以从现在开始使用(分配,调用等)。
函数声明则不同。
在函数声明被定义之前,它就可以被调用。
例如,一个全局函数声明对整个脚本来说都是可见的,无论它被写在这个脚本的哪个位置。
这是内部算法的原故。当 JavaScript 准备 运行脚本时,首先会在脚本中寻找全局函数声明,并创建这些函数。我们可以将其视为“初始化阶段”。
在处理完所有函数声明后,代码才被执行。所以运行时能够使用这些函数。
例如下面的代码会正常工作:
sayHi("John"); // Hello, John
function sayHi(name) {
alert( `Hello, ${name}` );
}
函数声明 sayHi
是在 JavaScript 准备运行脚本时被创建的,在这个脚本的任何位置都可见。
……如果它是一个函数表达式,它就不会工作:
sayHi("John"); // error!
let sayHi = function(name) { // (*) no magic any more
alert( `Hello, ${name}` );
};
函数表达式在代码执行到它时才会被创建。只会发生在 (*)
行。为时已晚。
函数声明的另外一个特殊的功能是它们的块级作用域。
严格模式下,当一个函数声明在一个代码块内时,它在该代码块内的任何位置都是可见的。但在代码块外不可见。
例如,想象一下我们需要依赖于在代码运行过程中获得的变量 age
声明一个函数 welcome()
。并且我们计划在之后的某个时间使用它。
如果我们使用函数声明,则以下代码无法像预期那样工作:
let age = prompt("What is your age?", 18);
// 有条件地声明一个函数
if (age < 18) {
function welcome() {
alert("Hello!");
}
} else {
function welcome() {
alert("Greetings!");
}
}
// ……稍后使用
welcome(); // Error: welcome is not defined
这是因为函数声明只在它所在的代码块中可见。
下面是另一个例子:
let age = 16; // 拿 16 作为例子
if (age < 18) {
welcome(); // \ (运行)
// |
function welcome() { // |
alert("Hello!"); // | 函数声明在声明它的代码块内任意位置都可用
} // |
// |
welcome(); // / (运行)
} else {
function welcome() {
alert("Greetings!");
}
}
// 在这里,我们在花括号外部调用函数,我们看不到它们内部的函数声明。
welcome(); // Error: welcome is not defined
我们怎么才能让 welcome
在 if
外可见呢?
正确的做法是使用函数表达式,并将 welcome
赋值给在 if
外声明的变量,并具有正确的可见性。
下面的代码可以如愿运行:
let age = prompt("What is your age?", 18);
let welcome;
if (age < 18) {
welcome = function() {
alert("Hello!");
};
} else {
welcome = function() {
alert("Greetings!");
};
}
welcome(); // 现在可以了
或者我们可以使用问号运算符 ?
来进一步对代码进行简化:
let age = prompt("What is your age?", 18);
let welcome = (age < 18) ?
function() { alert("Hello!"); } :
function() { alert("Greetings!"); };
welcome(); // 现在可以了
什么时候选择函数声明与函数表达式?
根据经验,当我们需要声明一个函数时,首先考虑函数声明语法。它能够为组织代码提供更多的灵活性。因为我们可以在声明这些函数之前调用这些函数。
这对代码可读性也更好,因为在代码中查找
function f(…) {…}
比let f = function(…) {…}
更容易。函数声明更“醒目”。……但是,如果由于某种原因而导致函数声明不适合我们(我们刚刚看过上面的例子),那么应该使用函数表达式。
小结
- 函数是值。它们可以在代码的任何地方被分配,复制或声明。
- 如果函数在主代码流中被声明为单独的语句,则称为“函数声明”。
- 如果该函数是作为表达式的一部分创建的,则称其“函数表达式”。
- 在执行代码块之前,内部算法会先处理函数声明。所以函数声明在其被声明的代码块内的任何位置都是可见的。
- 函数表达式在执行流程到达时创建。
在大多数情况下,当我们需要声明一个函数时,最好使用函数声明,因为函数在被声明之前也是可见的。这使我们在代码组织方面更具灵活性,通常也会使得代码可读性更高。
所以,仅当函数声明不适合对应的任务时,才应使用函数表达式。
箭头函数,基础知识
创建函数还有另外一种非常简单的语法,并且这种方法通常比函数表达式更好。
它被称为“箭头函数”,因为它看起来像这样:
let func = (arg1, arg2, ..., argN) => expression;
这里创建了一个函数 func
,它接受参数 arg1..argN
,然后使用参数对右侧的 expression
求值并返回其结果。
换句话说,它是下面这段代码的更短的版本:
let func = function(arg1, arg2, ..., argN) {
return expression;
};
让我们来看一个具体的例子:
let sum = (a, b) => a + b;
/* 这个箭头函数是下面这个函数的更短的版本:
let sum = function(a, b) {
return a + b;
};
*/
alert( sum(1, 2) ); // 3
可以看到 (a, b) => a + b
表示一个函数接受两个名为 a
和 b
的参数。在执行时,它将对表达式 a + b
求值,并返回计算结果。
-
如果我们只有一个参数,还可以省略掉参数外的圆括号,使代码更短。
例如:
let double = n => n * 2; // 差不多等同于:let double = function(n) { return n * 2 } alert( double(3) ); // 6
-
如果没有参数,括号则是空的(但括号必须保留):
let sayHi = () => alert("Hello!"); sayHi();
箭头函数可以像函数表达式一样使用。
例如,动态创建一个函数:
let age = prompt("What is your age?", 18);
let welcome = (age < 18) ?
() => alert('Hello!') :
() => alert("Greetings!");
welcome();
一开始,箭头函数可能看起来并不熟悉,也不容易读懂,但一旦我们看习惯了之后,这种情况很快就会改变。
箭头函数对于简单的单行行为(action)来说非常方便,尤其是当我们懒得打太多字的时候。
多行的箭头函数
到目前为止,我们看到的箭头函数非常简单。它们从 =>
的左侧获取参数,计算并返回右侧表达式的计算结果。
有时我们需要更复杂一点的函数,比如带有多行的表达式或语句。在这种情况下,我们可以使用花括号将它们括起来。主要区别在于,用花括号括起来之后,需要包含 return
才能返回值(就像常规函数一样)。
就像这样:
let sum = (a, b) => { // 花括号表示开始一个多行函数
let result = a + b;
return result; // 如果我们使用了花括号,那么我们需要一个显式的 “return”
};
alert( sum(1, 2) ); // 3
更多内容
在这里,我们赞扬了箭头函数的简洁性。但还不止这些!
箭头函数还有其他有趣的特性。
后续会深入介绍箭头函数
现在,我们已经可以用箭头函数进行单行行为和回调了。
小结
箭头函数对于简单的操作很方便,特别是对于单行的函数。它具体有两种形式:
- 不带花括号:
(...args) => expression
—— 右侧是一个表达式:函数计算表达式并返回其结果。如果只有一个参数,则可以省略括号,例如n => n*2
。 - 带花括号:
(...args) => { body }
—— 花括号允许我们在函数中编写多个语句,但是我们需要显式地return
来返回一些内容。
JavaScript 特性
下面简要回顾我们到现在为止学到的 JavaScript 特性,并特别注意了一些细节。
代码结构
语句用分号分隔:
alert('Hello'); alert('World');
通常,换行符也被视为分隔符,因此下面的例子也能正常运行:
alert('Hello')
alert('World')
这就是所谓的「自动分号插入」。但有时它不起作用,例如:
alert("There will be an error after this message")
[1, 2].forEach(alert)
大多数代码风格指南都认为我们应该在每个语句后面都加上分号。
在代码块 {...}
后以及有代码块的语法结构(例如循环)后不需要加分号:
function f() {
// 函数声明后不需要加分号
}
for(;;) {
// 循环语句后不需要加分号
}
……但即使我们在某处添加了「额外的」分号,这也不是错误。分号会被忽略的。
严格模式
为了完全启用现代 JavaScript 的所有特性,我们应该在脚本顶部写上 "use strict"
指令。
'use strict';
...
该指令必须位于 JavaScript 脚本的顶部或函数体的开头。
如果没有 "use strict"
,所有东西仍可以正常工作,但某些功能将以老式的“兼容”方式运行。我们通常更喜欢现代的方式。
语言的一些现代特征(比如我们将来要学习的类)会隐式地启用严格模式。
变量
可以使用以下方式声明变量:
let
const
(不变的,不能被改变)var
(旧式的,稍后会看到)
一个变量名可以由以下组成:
- 字母和数字,但是第一个字符不能是数字。
- 字符
$
和_
是允许的,用法同字母。 - 非拉丁字母和象形文字也是允许的,但通常不会使用。
变量是动态类型的,它们可以存储任何值:
let x = 5;
x = "John";
有 8 种数据类型:
number
— 可以是浮点数,也可以是整数,bigint
— 用于任意长度的整数,string
— 字符串类型,boolean
— 逻辑值:true/false
,null
— 具有单个值null
的类型,表示“空”或“不存在”,undefined
— 具有单个值undefined
的类型,表示“未分配(未定义)”,object
和symbol
— 对于复杂的数据结构和唯一标识符,我们目前还没学习这个类型。
typeof
运算符返回值的类型,但有两个例外:
typeof null == "object" // JavaScript 编程语言的设计错误
typeof function(){} == "function" // 函数被特殊对待
交互
我们使用浏览器作为工作环境,所以基本的 UI 功能将是:
-
prompt(question[, default\])
提出一个问题
question
,并返回访问者输入的内容,如果他按下「取消」则返回null
。 -
confirm(question)
提出一个问题
question
,并建议用户在“确定”和“取消”之间进行选择。选择结果以true/false
形式返回。 -
alert(message)
输出一个消息
message
。
这些函数都会产生 模态框,它们会暂停代码执行并阻止访问者与页面的其他部分进行交互,直到用户做出回答为止。
举个例子:
let userName = prompt("Your name?", "Alice");
let isTeaWanted = confirm("Do you want some tea?");
alert( "Visitor: " + userName ); // Alice
alert( "Tea wanted: " + isTeaWanted ); // true
运算符
JavaScript 支持以下运算符:
-
算数运算符
常规的:
+ - * /
(加减乘除),取余运算符%
和幂运算符**
。二进制加号+
可以连接字符串。如果任何一个操作数是一个字符串,那么另一个操作数也将被转换为字符串:alert( '1' + 2 ); // '12',字符串 alert( 1 + '2' ); // '12',字符串
-
赋值
简单的赋值:
a = b
和合并了其他操作的赋值:a *= 2
。 -
按位运算符
按位运算符在最低位级上操作 32 位的整数。
-
三元运算符
唯一具有三个参数的操作:
cond ? resultA : resultB
。如果cond
为真,则返回resultA
,否则返回resultB
。 -
逻辑运算符
逻辑与
&&
和或||
执行短路运算,然后返回运算停止处的值(true
/false
不是必须的)。逻辑非!
将操作数转换为布尔值并返回其相反的值。 -
空值合并运算符
??
运算符从一列变量中,选取值为已定义的值(defined value)的变量。a ?? b
的结果是a
,除非a
为null/undefined
,这时结果是b
。 -
比较运算符
对不同类型的值进行相等检查时,运算符
==
会将不同类型的值转换为数字(除了null
和undefined
,它们彼此相等而没有其他情况),所以下面的例子是相等的:alert( 0 == false ); // true alert( 0 == '' ); // true
其他比较也将转换为数字。严格相等运算符===
不会进行转换:不同的类型总是指不同的值。值null
和undefined
是特殊的:它们只在==
下相等,且不相等于其他任何值。大于/小于比较,在比较字符串时,会按照字符顺序逐个字符地进行比较。其他类型则被转换为数字。 -
其他运算符
还有很少一部分其他运算符,如逗号运算符。
循环
-
我们涵盖了 3 种类型的循环:
// 1 while (condition) { ... } // 2 do { ... } while (condition); // 3 for(let i = 0; i < 10; i++) { ... }
-
在
for(let...)
循环内部声明的变量,只在该循环内可见。但我们也可以省略let
并重用已有的变量。 -
指令
break/continue
允许退出整个循环/当前迭代。使用标签来打破嵌套循环。
之后将学习更多类型的循环来处理对象。
“switch” 结构
“switch” 结构可以替代多个 if
检查。它内部使用 ===
(严格相等)进行比较。
例如:
let age = prompt('Your age?', 18);
switch (age) {
case 18:
alert("Won't work"); // prompt 的结果是一个字符串,而不是数字
break;
case "18":
alert("This works!");
break;
default:
alert("Any value not equal to one above");
}
函数
我们介绍了三种在 JavaScript 中创建函数的方式:
-
函数声明:主代码流中的函数
function sum(a, b) { let result = a + b; return result; }
-
函数表达式:表达式上下文中的函数
let sum = function(a, b) { let result = a + b; return result; }
-
箭头函数:
// 表达式在右侧 let sum = (a, b) => a + b; // 或带 {...} 的多行语法,此处需要 return: let sum = (a, b) => { // ... return a + b; } // 没有参数 let sayHi = () => alert("Hello"); // 有一个参数 let double = n => n * 2;
- 函数可能具有局部变量:在函数内部声明的变量,或在其参数列表中。这类变量只在函数内部可见。
- 参数可以有默认值:
function sum(a = 1, b = 2) {...}
。 - 函数总是返回一些东西。如果没有
return
语句,那么返回的结果是undefined
。
更多内容
这些是 JavaScript 特性的简要概述。截至目前,我们仅仅学习了基础知识。之后还将学习更多JavaScript的高级特性。