简介

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 语言中。这种情况一直持续到 2009ECMAScript 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. 现代浏览器支持 classmodule 一旦使用了它们就默认开启了严格模式

变量

  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. 变量区分大小写,appleAPPLE 是两个不同的变量
  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-InfinityNaN
    • 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. JavaScriptnumber 类型无法安全地表示大于 2^53 - 19007199254740991,或小于 -(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 类型仅包含两个值 truefalse
1
let isGreate = 4 > 1; // 比较的结果是一个布尔值

null

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

undefined

  1. 特殊值 undefinednull 一样自成类型,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 绝对不是一个 objectnull 有自己的类型,它是一个特殊值。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
truefalse10
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. 注意 nullundefined 是不同的,null 转换为 0undefined 转为为 NaN

布尔类型转换

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

nullundefined 进行比较

  1. 当使用严格相等 === 比较二者时它们不相等
  2. 当使用非严格相等 == 比较二者时它们相等
1
2
console.log(null === undefined); // false
console.log(null == undefined); // true
  1. 当使用数学式或其他比较方法 < > <= >=
    • null / undefined 会被转化为数字:null 被转化为 0undefined 被转化为 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. 另一方面,undefinednull 在相等性检查 == 中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(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、空字符串 ""nullundefinedNaN 都会被转换成 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

循环 whilefor

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