简介

script标签

  1. 使用 <script> 标签将 javascript 程序插入到 html 文档的任何位置
1
2
3
<script>
console.info(11111);
</script>

现代的标记(markup)

  1. <script> 标签有一些现在很少用到的特性 (attribute),但是我们可以在老代码中找到它们
    • type 特性: <script type=...></script>,在老的 html4 标准中,要求 script 标签有 type 属性,通常是 type="text/javascript"。这样的特性声明现在已经不再需要。而且,现代 HTML 标准已经完全改变了此特性的含义。现在,它可以用于 JavaScript 模块
    • language 特性: <script language=...></script>,这个特性是为了显示脚本使用的语言。这个特性现在已经没有任何意义,因为语言默认就是 JavaScript。不再需要使用它了
    • 脚本前后的注释,在非常古老的书籍和指南中,你可能会在 script 标签里面找到注释,现代 JavaScript 中已经不这样使用了。这些注释是用于不支持 <script> 标签的古老的浏览器隐藏 JavaScript 代码的。由于最近 15 年内发布的浏览器都没有这样的问题,因此这种注释能帮你辨认出一些老掉牙的代码
      1
      2
      3
      <script type="text/javascript"><!--
      ...
      // --></script>

外部脚本

  1. 如果有大量的 JavaScript 代码,可以将它放入一个单独的文件,通过 src 特性添加到 html 文件中
    1
    <script scr="/path/to/script.js"></script>

一般来说,只有最简单的脚本才嵌入到 HTML 中。更复杂的脚本存放在单独的文件中。
使用独立文件的好处是浏览器会下载它,然后将它保存到浏览器的 缓存 中。
之后,其他页面想要相同的脚本就会从缓存中获取,而不是下载它。所以文件实际上只会下载一次。
这可以节省流量,并使得页面(加载)更快。

如果设置了 src 属性,script 标签内容将会被忽略
一个单独的 script 标签不能同时有 src 特性和内部包裹的代码,这将不会工作

1
2
3
<script src="file.js">
alert(1); // 此内容会被忽略,因为设定了 src
</script>

我们必须进行选择,要么使用外部的 <script src="..."></script>,要么使用正常包裹代码的 script 标签

1
2
3
4
<script src="file.js"></script>
<script>
alert(1);
</script>

代码结构

语句

  1. 语句是执行行为的语法结构和命令,通常每条语句独占一行,以提高代码的可读性
1
2
alert(11);
alert(22);

分号

  1. 当存在换行符时,在大多数情况下可以省略分号,下面这个例子中 js 将换行符理解成隐式的分号(自动分号插入)
    1
    2
    alert('hello')
    alert('world')
  2. 在大多数情况下,换行意味着一个分号,但是大多数情况并不意味着总是
    1
    2
    3
    4
    alert(3
    + 1
    + 2);
    // 代码正确输出了,因为 JavaScript 并没有在这里插入分号。显而易见的是,如果一行以加号 "+" 结尾,那么这是一个“不完整的表达式”,不需要分号。所以,这个例子得到了预期的结果
  3. 存在JavaScript无法确定是否真的需要自动插入分号的情况

一个错误的例子

1
2
alert('hello');
[1, 2].forEach(alert);

上面的例子会正确输出

1
2
alert('hello')
[1, 2].forEach(alert);

上面的例子会发生报错,这是因为 JavaScript 引擎并没有假设在方括号 [...] 前有一个分号,因此这段代码被视为了一个语句

1
alert('hello')[1, 2].forEach(alert);

即使语句被换行符分隔了,我们依然建议在它们之间加分号

注释

  1. 随着时间推移,程序变得越来越复杂。为代码添加 注释 来描述它做了什么和为什么要这样做,变得非常有必要了。可以在脚本的任何地方添加注释,它们并不会影响代码的执行,因为引擎会直接忽略它们
  2. 单行注释以两个正斜杠字符 // 开始
    1
    2
    3
    4
    // 独占一行的注释
    alert('hello');

    alert('world'); // 注释跟随语句的后面
  3. 多行注释以一个正斜杠和星号开始 “/*” 并以一个星号和正斜杠结束 “*/”
    1
    2
    3
    4
    5
    6
    /* 多行
    注释 */
    alert('hello');
    alert('world');

    // 注释的内容被忽略了,所以如果我们在 /* … */ 中放入代码,并不会执行

不支持注释嵌套,不要在 /*...*/ 内嵌另一个 /*...*/

1
2
3
4
5
/*
/* 嵌套注释 ??? */
*/

alert('world'); // 无法执行

注释会增加代码总量,但这一点也不是什么问题。有很多工具可以帮你在把代码部署到服务器之前缩减代码。这些工具会移除注释,这样注释就不会出现在发布的脚本中。所以,注释对我们的生产没有任何负面影响。

现代模式

长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题。新的特性被加入,旧的功能也没有改变。这么做有利于兼容旧代码,但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中。这种情况一直持续到 2009 年 ECMAScript 5 (ES5) 的出现。ES5 规范增加了新的语言特性并且修改了一些已经存在的特性。为了保证旧的功能能够使用,大部分的修改是默认不生效的。你需要一个特殊的指令 —— "use strict" 来明确地激活这些特性

use strict

  1. 这个指令看上去像一个字符串 "use strict" 或者 'use strict'。当它处于脚本文件的顶部时,则整个脚本文件都将以“现代”模式进行工作
1
2
3
"use strict"

// 代码以现代模式工作
  1. "use strict" 可以被放在函数体的开头。这样则可以只在该函数中启用严格模式
1
2
3
4
function sky() {
"use strict"
// ...
}
  1. 确保 use strict 出现在最顶部,否则严格模式可能无法启用
  2. 没有办法取消 use strict,一旦进入了严格模式,就没有回头路
  3. 现代浏览器支持 class 和 module 一旦使用了它们就默认开启了严格模式

变量

  1. 变量是数据的命名存储,使用 let 关键字进行定义变量 let message
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 下面的语句创建(也可以称为 声明 或者 定义)了一个名称为 “message” 的变量
let message;
// 可以通过赋值运算符 = 为变量添加一些数据
message = "hello";

// 合并为一行

// ---------------

let message = 'hello';

// ---------------

// 也可以一行声明多个变量
let user = 'John', age = 25, message = 'hello'; // 不易阅读 推荐一行声明一个变量
  1. 变量声明两次会触发 error,一个变量应该只被声明一次,对同一变量进行重复声明会触发 error。因此我们对同一个变量应该只声明一次,之后在不使用 let 的情况下对其引用

变量命名

  1. 变量名称必须仅包含字母、数字、符号$和_
  2. 首字符必须非数字
  3. 推荐驼峰命名法
  4. 变量区分大小写,apple 和 APPLE 是两个不同的变量
  5. 允许非英文字符,但不推荐
  6. 保留字无法作为变量名使用
1
2
let userName;
let test123;

常量

  1. 声明一个不变的变量,可以使用 const 而非 let
  2. 使用 const 声明的变量称为常量,它们不能被修改
1
2
const myBBB = '18.2423';
myBBB = 1234; // ERROR

大写形式的常数

一个普遍的做法是将常量用作别名,以便记住那些在执行之前就已知的难以记住的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 更易懂。
*/
  1. 什么时候该为常量使用大写命名,什么时候进行常规命名?
    • 作为一个“常数”,意味着值永远不变。但是有些常量在执行之前就已知了(比如红色的十六进制值),还有些在执行期间被“计算”出来,但初始赋值之后就不会改变
1
const pageLoadTime = /* 网页加载所需的时间 */;

pageLoadTime 的值在页面加载之前是未知的,所以采用常规命名。但是它仍然是个常量,因为赋值之后不会改变,换句话说,大写命名的常量仅用作“硬编码(hard-coded)”值的别名

数据类型

  1. 在 JavaScript 中有 8 种基本的数据类型(译注:7 种原始类型和 1 种引用类型)
  2. 我们可以将任何类型的值存入变量。例如,一个变量可以在前一刻是个字符串,下一刻就存储一个数字。允许这种操作的编程语言,例如 JavaScript,被称为“动态类型”(dynamically typed)的编程语言,意思是虽然编程语言中有不同的数据类型,但是你定义的变量并不会在定义后,被限制为某一数据类型
1
2
let message = 'hello';
message = 1234;

Number 类型

1
2
let n = 123;
n = 12.345;
  1. number 类型代表整数和浮点数,数值运算 * / + -
  2. 除了常规的数字,还包括特殊数值,也属于 number 类型:Infinity、-Infinity、NaN
    • Infinity 表示正无穷 console.log(1 / 0)
    • NaN 代表计算错误,它是一个不正确的或者一个未定义的数学操作所得到的结果 alert('not' / 2)
    • NaN 是粘性的,任何对 NaN 的进一步数学运算都会返回 NaN
    • 所以,如果在数学表达式中有一个 NaN,会被传播到最终结果(只有一个例外:NaN ** 0 结果为 1)
  3. 数学运算是安全的
    • 在 JavaScript 中做数学运算是安全的。我们可以做任何事:除以 0,将非数字字符串视为数字,等等。脚本永远不会因为一个致命的错误(“死亡”)而停止。最坏的情况下,我们会得到 NaN 的结果
  4. 特殊的数值属于 number 类型
1
2
alert(NaN + 1); // NaN
alert(3 * NaN); // NaN

BigInt 类型

  1. 在 JavaScript 中 number 类型无法安全地表示大于 2^53 - 1 即 9007199254740991,或小于 -(2^53 - 1) 的整数
  2. 更准确的说 number 类型可以存储更大的整数(最多 1.7976931348623157 * 10^308),但超过安全整数范围 ±(2^53 - 1) 会出现精度问题,因为并非所有数字都适合固定的 64 位存储。因此,可能存储的是近似值
1
2
3
// 这两个数字(正好超出了安全整数范围)但是它们的输出结果是相同的
console.log(9007199254740991 + 1); // 9007199254740992
console.log(9007199254740991 + 2); // 9007199254740992
  1. 也就是说,所有大于 2^53 - 1 的奇数都不能用 number 类型存储。在大多数情况下,±(2^53 - 1) 范围就足够了,但有时候需要整个范围非常大的整数,例如用于密码学或微秒精度的事件戳
  2. BigInt 类型用来表示任意长度的整数,可以将 n 附加到整数字段的末尾来创建 BigInt 值
1
2
// 尾部的 "n" 表示这是一个 BigInt 类型
const bigInt = 1234567890123456789012345678901234567890n;

String 类型

  1. JavaScript 中的字符串都必须被括在引号中
  2. 三种书写字符串的方式
    • 双引号: "hello"
    • 单引号: 'hello'
    • 反引号: hello
  3. 双引号和单引号都是简单的引用,没有什么区别,反引号是功能拓展引号,它允许通过变量和表达式包装在 ${...} 中,来将它们嵌入到字符串中
  4. JavaScript 中没有 character 类型
1
2
3
let name = "John";

console.info(`hello ${name}`);

Boolean 类型(逻辑类型)

  1. boolean 类型仅包含两个值 true 和 false
1
let isGreate = 4 > 1; // 比较的结果是一个布尔值

null 值

  1. 特殊的 null 值不属于上述任何一个类型,它构成了一个独立的类型,只包含 null 值
  2. JavaScript 中的 null 仅仅是一个代表“无”、“空”或“值未知”的特殊值
1
let age = null;

undefined 值

  1. 特殊值 undefined 和 null 一样自成类型,undefined 的含义是 未被赋值,如果一个变量已被声明,但未被赋值,那么它的值就是 undefined
1
2
3
4
let age;
console.log(age); // undefined

age = undefined; // 从技术上讲,可以显式地将 undefined 赋值给变量 但是不建议这样做。通常,使用 null 将一个“空”或者“未知”的值写入变量中,而 undefined 则保留作为未进行初始化的事物的默认初始值

Object 类型和 Symbol 类型

  1. object 类型是一个特殊的类型
  2. 其他所有的数据类型都被称为“原始类型”,因为它们的值只包含一个单独的内容(字符串、数字或者其他)。相反,object 则用于储存数据集合和更复杂的实体
  3. symbol 类型用于创建对象的唯一标识符

typeof 运算符

  1. typeof 运算符返回参数的类型
1
2
3
4
5
6
7
8
9
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)
  1. 最后三行额外说明
    • Math 是一个提供数学运算的内建 object
    • typeof null 的结果为 "object"。这是官方承认的 typeof 的错误,这个问题来自于 JavaScript 语言的早期阶段,并为了兼容性而保留了下来。null 绝对不是一个 object。null 有自己的类型,它是一个特殊值。typeof 的行为在这里是错误的
    • typeof alert 的结果是 "function" 在 JavaScript 语言中没有一个特别的 “function” 类型。函数隶属于 object 类型。但是 typeof 会对函数区分对待,并返回 "function"。这也是来自于 JavaScript 语言早期的问题。从技术上讲,这种行为是不正确的,但在实际编程中却非常方便
  2. typeof(x) 语法
    • typeof(x) 和 typeof x 相同
    • 简单点说:typeof 是一个操作符,不是一个函数。这里的括号不是 typeof 的一部分。它是数学运算分组的括号
    • 通常,这样的括号里包含的是一个数学表达式,例如 (2 + 2),但这里它只包含一个参数 (x)。从语法上讲,它们允许在 typeof 运算符和其参数之间不打空格

交互:alert、prompt和confirm

alert

  1. 弹出带有信息的小窗口,“modal” 意味着用户不能与页面的其他部分(例如点击其他按钮等)进行交互,直到他们处理完窗口

prompt

  1. prompt 函数接收两个参数 prompt(title [, default]);
    • title 显示给用户的文本
    • default 可选的第二个参数,指定 input 框的初始值
  2. 浏览器显示一个带有文本消息的模态窗口,还有 input 框和确定/取消按钮
  3. 返回用户输入的文本,如果用户取消了输入,则返回 null

confirm

  1. confirm 函数显示带有 question 以及确定和取消两个按钮的模态窗口,语法 confirm(question)
  2. 点击确认返回 true 点击取消返回 false

类型转换

  1. 大多数情况下,运算符和函数会自动将赋予它们的值转换为正确的类型
  2. 在某些情况下,我们需要将值显式地转换为我们期望的类型

字符串转换

  1. 当我们需要一个字符串形式的值时,就会进行字符串转换
  2. 显式调用 String(value) 将 value 转换为字符串类型

数字型转换

  1. 在算术函数和表达式中,会自动进行 number 类型转换 "6" / "2"
  2. 显式调用 Number(value) 将 value 转换为 number 类型
  3. 如果不是一个有效的数字,转换的结果会是 NaN
值结果
undefinedNaN
null0
true 和 false1 和 0
string去掉首尾空白字符(空格、换行符 \n、制表符 \t 等)后的纯数字字符串中含有的数字。如果剩余字符串为空,则转换结果为 0。否则,将会从剩余字符串中“读取”数字。当类型转换出现 error 时返回 NaN
1
2
3
4
alert( Number("   123   ") ); // 123
alert( Number("123z") ); // NaN(从字符串“读取”数字,读到 "z" 时出现错误)
alert( Number(true) ); // 1
alert( Number(false) ); // 0
  1. 注意 null 和 undefined 是不同的,null 转换为 0 而 undefined 转为为 NaN

布尔类型转换

  1. 直观上为空的值(如 0、空字符串、null、undefined 和 NaN)将变为 false,其他值变成 true
  2. 包含 0 的字符串 "0" 是 true
1
2
3
4
alert( Boolean(1) ); // true
alert( Boolean(0) ); // false
alert( Boolean("hello") ); // true
alert( Boolean("") ); // false

基础运算符

术语

  1. 运算元 — 运算符应用的对象。比如说乘法运算 5 * 2,有两个运算元:左运算元 5 和右运算元 2。有时候人们也称其为“参数”而不是“运算元”
  2. 一元运算符 — 如果一个运算符对应的只有一个运算元,那么它是 一元运算符。比如说一元负号运算符(unary negation)-,它的作用是对数字进行正负转换
  3. 二元运算符 — 如果一个运算符拥有两个运算元,那么它是 二元运算符。减号还存在二元运算符形式
1
2
3
4
5
6
let x = 1;
x = -x;

let y = 3;

console.log(x - y);

数据运算

  1. 加法 +
  2. 减法 -
  3. 乘法 *
  4. 除法 /
  5. 取余 % a % b => a 整除 b 的余数
  6. 求幂 ** a ** b => a^b => Math.pow(a, b)

用二元运算符 + 连接字符串

  1. 通常 + 号用于求和,但是如果 + 被用于字符串,它将合并(连接)各个字符串
  2. 二元 + 是唯一一个以这种方式支持字符串的运算符。其他算术运算符只对数字起作用,并且总是将其运算元转换为数字
1
2
3
4
5
6
7
8
9
10
let s = "my" + "string";

// 注意: 只要任意一个运算元是字符串,那么另一个运算元也将被转化为字符串

console.log("1" + 2); // 12
console.log(2 + '1'); // 21
console.log(2 + 2 + '1'); // 41

console.log(6 - '2'); // 4
console.log('6' / '2'); // 3

数字转化,一元运算符 +

  1. 加号 + 有两种形式。一种是二元运算符,一种是一元运算符
  2. 加号 + 可以将其他类型转换为数字类型,它的效果和 Number(...) 相同,但是更加简短
1
2
3
4
5
6
7
8
9
10
11
12
13
let x = 1;
console.log(+x); // 1

let y = -2;
console.log(+y); // -2

console.log(+true); // 1
console.log(+""); // 0


let app = "1";
let xpp = "2";
console.log(+app + +xpp); // 3

运算符优先级

|优先级|名称|符号|
|…|…|…|
|...|...|...|
|15|一元加号|+|
|15|一元负号|-|
|14|求幂|**|
|13|乘号|*|
|13|除号|/|
|12|加号|+|
|12|减号|-|
|...|...|...|
|2|赋值符|=|
|...|...|...|

赋值运算符

  1. 赋值运算符的优先级只有 2

赋值 = 返回一个值

  1. = 是一个运算符,而不是一个有着魔法作用的语言结构
  2. 在 JavaScript 中,所有运算符都会返回一个值,这对于 + - 来说是显而易见的,但对于 =来说也是如此
  3. 语句 x = value 将值 value 写入 x 然后返回 value
1
2
3
4
5
6
7
8
// 一个复杂的例子
let a = 1;
let b = 2;

let c = 3 - (a = b + 1); // b + 1 写入 a 并返回 b + 1

console.log(a); // 3
console.log(c); // 0

链式赋值

1
2
3
4
5
6
7
let a, b, c;

a = b = c = 2 + 2;

console.log(a, b, c); // 4 4 4

// 链式赋值从右到左进行计算。首先,对最右边的表达式 2 + 2 求值,然后将其赋给左边的变量:c、b 和 a。最后,所有的变量共享一个值

原地修改

1
2
3
let n = 2;
n = n + 5;
n = n * 2;
  1. 上面的写法可以使用运算符 += 和 *= 缩写来表示
  2. 所有算数和位运算都有简单的 修改并赋值 的运算符 /= 和 -=,这类运算符的优先级和普通的赋值运算符的优先级相同,所以它们在大多数其他运算之后执行
1
2
3
let n = 2;

n *= 3 + 5; // 16

自增/自减

  1. 对一个数进行加一、减一是最常见的数学运算符之一
  2. 自增 ++ 将变量和 1 相加,自减 -- 将变量与一相减
  3. 自增/自减只能用于变量,`5++` 常量是不可以这样使用的
  4. ++ -- 是可以置于变量前也可以放在变量后,它们都做同一件事,将变量 +1
    • 放在变量前,被称为前置形式 ++count
    • 放在变量后,被称为后置形式 count++
  5. 前置和后置的区别在于返回值,上面我们说所有的运算符都有返回值,自增/自减也不例外。前置形式返回一个新的值,但后置返回原来的值(做加法/减法之前的值)
    • 如果自增/自减的值不会被使用,那么两者形式没有区别
    • 如果我们想要对变量进行自增操作,并且 需要立刻使用自增后的值,那么我们需要使用前置形式
    • 如果我们想要将一个数加一,但是我们想使用其自增之前的值,那么我们需要使用后置形式
  6. 运算符同样可以在表达式内部使用,它的优先级比绝大部份的算数运算符要高
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let count = 1;
let a = ++count;

console.log(a); // 2

let count1 = 1;
let a1 = count1++;

console.log(a1); // 1


let count2 = 1;

console.log(2 * ++count2); // 4

let count3 = 1;
console.log(2 * count3++); // 2

位运算符

  1. 位运算符把运算元当作 32 位整数,并在它们的二进制表现形式上操作
  2. 位运算符
    • 按位与 &
    • 按位或 |
    • 按位异或 ^
    • 按位非 ~
    • 左移 <<
    • 右移 >>
    • 无符号右移 >>>

逗号运算符

  1. 逗号运算符 , 是最少见最不常用的运算符之一
  2. 逗号运算符能让我们处理多个表达式,使用 , 将它们分开,每个表达式都运行了,但是只有最后一个结果会被返回
  3. 逗号运算符的优先级非常低,请注意逗号的优先级非常低,比 `=` 还要低,因此下面的例子中圆括号非常重要
1
2
3
4
5
6
7
let a = (1 + 2, 3 + 4); // 7
// 这里,第一个表达式 1 + 2 运行了,但是它的结果被丢弃了。随后计算 3 + 4,并且该计算结果被返回


for (a = 1, b = 3, c = a * b; a < 10; a++) {
// ...
}

值的比较

  1. 大于 / 小于: a > b a < b
  2. 大于等于 / 小于等于 a >= b a <= b
  3. 检查两个值的相等 a == b
  4. 检查两个值的不相等 a != b

比较结果为布尔类型

  1. 所有的运算符均返回布尔值
    • true 表示真
    • false 表示假
1
2
3
alert(2 > 1); // true
alert(2 == 1); // false
alert(2 != 1); // true
  1. 比较的值可以赋值给任意值

字符串比较

  1. 比较字符串的大小时,JavaScript 会使用字典(unicode 编码)顺序进行判定,换言之,字符串是按字符逐个进行比较的
    • 首先比较两个字符的首位字符大小
    • 如果一方字符较大(或较小),则该字符串大于(或小于)另一个字符串,算法结束
    • 否则,如果两个字符串的首位字符相等,则继续取出两个字符串各自的后一位字符进行比较
    • 重复上述步骤进行比较,直到比较完成某个字符串的所有字符为止
    • 如果两个字符串的字符同时用完,那么判定他们相等,否则未结束(还有未比较的字符)的字符串更大
1
2
3
alert('Z' > 'A'); // true
console.log('Glow' > 'Glee'); // true
console.log('Bee' > 'Be'); // true

不同类型间的比较

  1. 当不同类型的值进行比较时,JavaScript 会首先将其转化为数字(number)再判定大小
  2. 对于布尔类型值,true 会转化为 1 false 转化为 0
1
2
3
4
5
console.log('2' > 1); // true '2' => 2
console.log('01' == 1); // true 01 => 1

console.log(true == 1); // true
console.log(false == 0); // true
  1. 一个有趣的现象
    • 若直接比较两个值,其结果是相等的
    • 若把两个值转为布尔值,它们可能得出完全相反的结果,即一个是 true 一个是 false
    • 因为 JavaScript 会把待比较的值转化为数字后再做比较(因此 "0" 变成了 0)。若只是将一个变量转化为 Boolean 值,则会使用其他的类型转换规则
1
2
3
4
5
6
7
let a = 0;
console.log(Boolean(a)); // false

let b = '0';
console.log(Boolean(b)); // true

console.log(a == b); // true

严格相等

  1. 普通的相等性检查 == 存在一个问题,不能区分出 0 和 false,也无法区分出空字符串和 false
1
2
console.log(0 == false); // true
console.log('' == false); // true
  1. 在比较不同类型的值时,处于相等判断符号 == 两侧的值会先被转化为数字。空字符串和 false 也是如此,转化后它们都为数字 0
  2. 严格相等运算符 === 在进行比较时不会做任何的类型转换
  3. 同样的与不相等类似,严格不相等可以表示为 !==
1
console.log(0 === false); // false

对 null 和 undefined 进行比较

  1. 当使用严格相等 === 比较二者时它们不相等
  2. 当使用非严格相等 == 比较二者时它们相等
1
2
console.log(null === undefined); // false
console.log(null == undefined); // true
  1. 当使用数学式或其他比较方法 < > <= >= 时
    • null / undefined 会被转化为数字:null 被转化为 0,undefined 被转化为 NaN

奇怪的结果 null vs 0

1
2
3
console.log(null > 0); // 1 false
console.log(null == 0); // 2 false
console.log(null >= 0); // 3 true
  1. 为什么会出现这种反常结果,这是因为相等性检查 == 和普通比较符 > < >= <= 的代码逻辑是相互独立的。进行值的比较时,null 会被转化为数字,因此它被转化为了 0。这就是为什么(3)中 null >= 0 返回值是 true,(1)中 null > 0 返回值是 false
  2. 另一方面,undefined 和 null 在相等性检查 == 中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中 null == 0 会返回 false

特立独行的 undefined

  1. undefined 不应该被与其他值进行比较
1
2
3
console.log(undefined > 0); // false 1
console.log(undefined < 0); // false 2
console.log(undefined == 0); // false 3
  1. (1) 和 (2) 都返回 false 是因为 undefined 在比较中被转换为了 NaN,而 NaN 是一个特殊的数值型值,它与任何值进行比较都会返回 false
  2. (3) 返回 false 是因为这是一个相等性检查,而 undefined 只与 null 相等,不会与其他值相等

避免问题

  1. 除了严格相等 === 外,其他但凡是有 undefined/null 参与的比较,我们都需要格外小心
  2. 除非你非常清楚自己在做什么,否则永远不要使用 >= > < <= 去比较一个可能为 null/undefined 的变量。对于取值可能是 null/undefined 的变量,请按需要分别检查它的取值情况

条件分支: if 和 ?

if 语句

  1. if(...) 语句计算括号里的条件表达式,如果计算结果是 true 就会执行对应的代码块

布尔转换

  1. if(...) 语句会计算圆括号内的表达式,并将计算结果转化为布尔值
  2. 类型转换的规则
    • 数字 0、空字符串 ""、null、undefined 和 NaN 都会被转换成 false
    • 其他值被转换为 true

else 语句

  1. if 语句有时会包含一个可选的 “else” 块。如果判断条件不成立,就会执行它内部的代码

多个条件 else if

  1. 可以通过使用 else if 子句实现一个条件的几个变体

条件运算符 ?

  1. 这个运算符通过问号 ? 表示。有时它被称为三元运算符,被称为“三元”是因为该运算符中有三个操作数。实际上它是 JavaScript 中唯一一个有这么多操作数的运算符
1
let result = condition ? value1 : value2;

多个 ?

  1. 使用一系列问号 ? 运算符可以返回一个取决于多个条件的值
1
2
3
4
5
6
7
8
let age = prompt('age?', 18);

let message = (age < 3) ? 'Hi, baby!' :
(age < 18) ? 'Hello!' :
(age < 100) ? 'Greetings!' :
'What an unusual age!';

console.log(message);

? 的非常规使用

1
(company == 'Netscape') ? alert('Right!') : alert('Wrong.');

逻辑运算符

  1. JavaScript 中的四个逻辑运算 || 、&&、!、??(空值合并运算符)

|| 或运算符

  1. 在传统的编程中,逻辑或仅能够操作布尔值。如果参与运算的任意一个参数为 true,返回的结果就为 true,否则返回 false
  2. 在 JavaScript 中,逻辑运算符更加灵活强大
1
2
3
4
console.log(true || true); // true
console.log(false || true); // true
console.log(true || false); // true
console.log(false || false); // false
  1. 大多数情况下,逻辑或 || 会被用在 if 语句中,用来测试是否有任何给定的条件为 true

或运算寻找第一个真值

1
let result = value1 || value2 || value3;
  1. 上面的或运算符做了如下过程:
    • 从左到右依次计算操作数
    • 处理每一个操作数时,都将其转化为布尔值,如果结果是 true 就停止计算,返回这个操作数的初始值
    • 如果所有的操作数都被计算过(也就是,转化结果都是 false),则返回最后一个操作数
  2. 返回的值是操作数的初始形式,不会做布尔转换
  3. 换句话说,一个或运算 || 的链,将返回第一个真值,如果不存在真值,就返回该链的最后一个值
1
2
3
4
console.log(1 || 0); // 1
console.log(null || 1); // 1
console.log(null || 0 || 1); // 1
console.log(undefined || null || 0); // 0
  1. 有趣的用法
    • 获取变量列表或者表达式中的第一个真值
    • 短路求值
1
2
3
4
5
6
7
8
9
10
// 获取变量列表或者表达式中的第一个真值
let firstName = "";
let lastName = "";
let nickName = "SuperCoder";

alert( firstName || lastName || nickName || "Anonymous"); // SuperCoder

// 短路求值
true || alert("not printed");
false || alert("printed");

&& 与运算符

  1. 在传统的编程中,当两个操作数都是真值时,与运算返回 true,否则返回 false
  2. 就像或运算一样,与运算的操作数可以是任意类型的值
1
2
3
4
console.log(true && true); // true
console.log(false && true); // false
console.log(true && false); // false
console.log(false && false); // false

与运算寻找第一个假值

1
result = value1 && value2 && value3
  1. && 做了如下事
    • 从左到右依次计算操作数
    • 在处理每一个操作数时,都将其转化为布尔值,如果结果是 false 就停止,并返回这个操作数的初始值
    • 如果所有操作数都被计算过,则返回最后一个操作数
  2. 与运算 && 在或运算 || 之前进行
    • 与运算 && 优先级比或运算 || 要高

! 非

  1. 逻辑非运算符接收一个参数,并按如下运作:
    • 将操作数转化为布尔类型: true / false
    • 返回相反的值
  2. 两个非运算 !! 有时候用来将某个值转化为布尔类型 !!0
  3. 非运算符 ! 的优先级在所有逻辑运算符里面最高,所以它总是在 && 和 || 之前执行

空值合并运算符 ??

  1. 空值运算符 ??, a ?? b 的结果是,如果 a 是已定义的,则结果是 a 否则结果是 b 换言之,如果第一个参数不是 null/undefined,则 ?? 返回第一个参数,否则返回第二个参数,也可以使用 result = a || b => result = a ?? b
  2. 还可以使用 ?? 从一系列值中选择第一个非 null/undefined 的值 result = a ?? b ?? c ?? 'dd'
  3. 或运算符 || 可以与 ?? 运算符相同的方式使用,但是它们之间最重要的区别在于
    • || 返回第一个 真值
    • ?? 返回第一个 已定义的值
  4. || 无法区别 false 0 空字符串 "" 和 null / undefined 它们都 一样。如果其中任何一个是 || 的第一个参数,那么都将得到第二个参数作为结果
  5. ?? 运算符的优先级与 || 相同,他们的优先级都是 4
  6. 出于安全原因,JavaScript 禁止将 ?? 运算符 && 和 || 一起使用,除非使用括号明确指定了优先级
1
2
3
let x = 1 && 2 ?? 3; // Syntax error

let x = (1 && 2) ?? 3; // right

循环 while 和 for

while 循环

1
2
3
4
5
6
7
while (condition) {
// 循环体 condition 为真时执行
}


let i = 3;
while (i) console.info(i--);
  1. 循环体的单次执行叫作 一次迭代
  2. 单行循环体不需要大括号,可以省略

do...while 循环

1
2
3
do {
// 循环体
} while (condition);
  1. do...while 循环不管条件是否为真,循环体 至少执行一次

for 循环

1
2
3
4
5
6
7
8
for (begin; condition; step) {
// 循环体 body
}

// begin 进入循环时执行一次
// condition 在每次循环迭代之前检查,如果为 `false` 停止循环
// body 条件为真时,重复运行
// step 在每次循环体迭代后执行
1
2
3
4
5
6
// 内联变量声明
for (let i = 0; i < 3; i++) {
alert(i); // 0, 1, 2
}

alert(i); // 错误,没有这个变量
  1. for 循环的任何语句段都可以被省略,例如,如果我们在循环开始时不需要做任何事,我们就可以省略 begin 语句段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let i = 0; // 我们已经声明了 i 并对它进行了赋值

for (; i < 3; i++) { // 不再需要 "begin" 语句段
alert( i ); // 0, 1, 2
}

// 也可以移除 step 语句段

let i = 0;

for (; i < 3;) {
alert( i++ );
}

// 我们可以删除所有内容,从而创建一个无限循环
for (;;) {
// 无限循环
}

// 请注意 for 的两个 ; 必须存在,否则会出现语法错误

跳出循环

  1. 通常条件为假时,循环会终止
  2. 可以使用 break 指令强制退出
1
2
3
4
5
6
7
8
let sum = 0;

while (true) {
let value = +prompt("Enter a number", '');
if (!value) break; // (*)
sum += value;
}
alert( 'Sum: ' + sum );

继续下一次迭代

  1. continue 指令是 break 的“轻量版”。它不会停掉整个循环。而是停止当前这一次迭代,并强制启动新一轮循环
1
2
3
4
5
for (let i = 0; i < 10; i++) {
//如果为真,跳过循环体的剩余部分。
if (i % 2 == 0) continue;
alert(i); // 1,然后 3,5,7,9
}
  1. continue 指令利于减少嵌套
1
2
3
4
5
6
// 多了 if 的嵌套
for (let i = 0; i < 10; i++) {
if (i % 2) {
alert( i );
}
}
  1. 禁止 break / continue 在 ? 的右边
    • 请注意非表达式的语法结构不能与三元运算符 `?` 一起使用。特别是 `break/continue` 这样的指令是不允许这样使用的
1
2
3
4
5
6
7
8
9
if (i > 5) {
alert(i);
} else {
continue;
}

// 用 ? 重写

(i > 5) ? alert(i) : continue; // continue 不允许在这个位置 显示有语法错误

break / continue 标签

  1. 有时候需要一次从多层嵌套的循环中跳出来
1
2
3
4
5
6
7
8
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!');
  1. 在 input 之后的普通 break 只会打破内部循环,标签可以实现这个功能
  2. 标签是在循环之前带有冒号的标识符
1
2
3
labelName: for (...) {
...
}
  1. break <labelName> 语句跳出循环至标签处
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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;
}
// ...
}
}

// 标签可以单独一行
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;
}
// ...
}
}
  1. continue 指令也可以与标签一起使用,这种情况下,执行跳转到标记循环的下一次迭代
  2. 标签并不允许跳到所有位置
1
2
3
4
5
6
7
8
9
10
break label; // 跳转至下面的 label 处(无效)
label: for (...);

// break 指令必须在代码块内。从技术上讲,任何被标记的代码块都有效,但 continue 只有在循环内部才可行

label: {
// ...
break label; // 有效
// ...
}

switch 语句

  1. switch 语句可以替代多个 if 判断
  2. switch 语句为多分枝选择的情况提供了一个更具描述性的方式

switch 语法

1
2
3
4
5
6
7
8
9
10
11
switch(x) {
case "value1":
// ...
[break]
case "value2":
// ...
[break]
default:
// ...
[break]
}
  1. 比较 x 值和第一个 case 是否相等,然后比较第二个 case 以此类推(强调一下,这里的相等是严格相等。被比较的值必须是相同的类型才能进行匹配)
  2. 如果相等,switch 语句就执行相应 case 下的代码块,直到遇到最靠近的 break 语句 (或者直到 switch 语句末尾)
  3. 如果没有符合的 case 则执行 default 代码块 (如果 default 存在)
  4. 如果没有 break 程序将不经过任何检查就会继续执行下一个 case
1
2
3
4
5
6
7
8
9
10
11
12
let a = 2 + 2;

switch (a) {
case 3:
console.log(1);
case 4:
console.log(2);
case 5:
console.log(3);
default:
console.log(4);
}
  1. 任何表达式都可以成为 switch/case 的参数

case 分组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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.');
}

函数

  1. 函数声明
1
2
3
function 函数名(参数列表) {
// 函数体
}
  1. 局部变量,在函数中声明的变量只在函数内部可见
  2. 外部变量,函数也可以访问外部变量,函数对外部变量拥有全部的访问权限。函数也可以修改外部变量
  3. 只有在没有局部变量的情况下才会使用外部变量,如果在函数内部声明了同名变量,那么函数会 遮蔽 外部变量

任何函数之外声明的变量都被称为全局变量。

全局变量在任意函数中都是可见的(除非被局部变量遮蔽)

减少全局变量的使用是一种很好的做法。现代的代码有很少甚至没有全局变量。大多数变量存在于它们的函数中。但是有时候,全局变量能够用于存储项目级别的数据

参数

  1. 可以通过参数将任意数据传递给函数
  2. 参数是函数声明中括号内列出的变量(它是函数声明时的术语)
  3. 参数是调用函数时传递给函数的值(它是函数调用时的术语)
1
2
3
4
5
6
7
8
9
function showMessage(from, text) {
from = '*' + from + '*'; // 让 "from" 看起来更优雅
alert( from + ': ' + text );
}

let from = "Ann";
showMessage(from, "Hello"); // *Ann*: Hello
// "from" 值相同,函数修改了一个局部的副本。
alert( from ); // Ann

默认值

  1. 如果一个函数被调用,但有参数未被提供,那么相应的值就会变成 undefined
1
2
3
4
5
function showMessage(from, text = "no text given") {
alert( from + ": " + text );
}

showMessage("Ann"); // Ann: no text given