基于 ES6 的 JavaScript 简明语法书

JavaScript 是一款基于原型的的多范式动态脚本语言,支持命令式、函数式以及面向对象式的编程风格。其标准化工作主要由欧洲计算机制造商协会ECMA,European Computer Manufacturers Association)负责,其语言标准被称为ECMAScript,其它组织可以遵循该标准开发各自的 JavaScript 实现,例如 Firefox 内置的 SpiderMonkey 以及 Chrome 内置的 V8 解析引擎(ECMAScript 规范文档主要针对解析引擎的开发人员,而非 JavaScript 脚本的编写人员)。

2012 年之后推出的 Web 浏览器都完整实现了 ECMAScript 5.1 标准,而 2015 年发布的 ECMAScript 6 标准(官方称为 ECMAScript 2015,俗称为 ES6),目前已经被 FirefoxChromium 以及 NodeJS 进行了较为完整的实现,所以本文将会基于此介绍 JavaScript 的重点语法特性,以及一些较为用常用的核心工具函数,本文在写作过程当中参考了 Mozilla 开发者社区的 《JavaScript Guide》《JavaScript Reference》 两篇官方文档。

基础语法

语句

JavaScript 使用 Unicode 字符集,并且对于大小写敏感。源文件当中的每一条 JavaScript 语句都要使用分号 ; 进行分隔,虽然这并非必需,但是可以大大降低代码发生错误的可能。

1
var name = "www.uinio.com";

注意:Javascript 源代码会被解析引擎从左至右进行扫描执行。

注释

JavaScript 的注释与许多 C/C++ 类语言相似,同样可以划分为单行多行两种类型:

1
2
3
4
5
// 单行注释

/*
多行注释
*/

变量

变量的名称叫做标识符,虽然可以使用大部分的 ISO 8859-1 或者 Unicode 编码字符作为标识符,其必须以字母、下划线 _、美元符号 $ 作为前缀,后续可以是数字 0 ~ 9 或者大写字母 A ~ Z 以及小写字母 a ~ z,例如:

1
Hank  _uinika  $Chengdu  from2021  to_2022

声明

ECMAScript 6 当中一共拥有 varletconst 三种变量声明关键字:

  • var:声明一个变量
  • let:声明具有块作用域的局部变量
  • const:声明具有块作用域的只读常量

JavaScript 源代码当中可以采用如下三种方式来声明一个变量:

  • 采用 var 关键字:用于声明局部变量全局变量,例如 var x = 2022
  • 采用 let 关键字:用于声明具有块作用域的局部变量,例如 let y = 13
  • 直接赋值:函数外使用会产生一个全局变量,严格模式下这种方式会发生错误,所以应该避免使用,例如 x = 2021

求值

采用关键字 var 或者 let 声明的变量,如果没有赋予初始值,则其值将会默认为 undefined。而访问未经过声明的变量,将会导致解析引擎抛出一个 ReferenceError 引用错误异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a;
console.log("变量 a 的值为 " + a); // 变量 a 的值为 undefined

console.log("变量 b 的值为 " + b); // 变量 b 的值为 undefined
var b;

console.log("变量 c 的值为 " + c); // Uncaught ReferenceError: c is not defined

let x;
console.log("变量 x 的值为 " + x); // 变量 x 的值为 undefined

console.log("变量 y 的值为 " + y); // Uncaught ReferenceError: y is not defined
let y;

因此,可以使用 undefined 来判断一个变量是否已经赋值,下面代码当中,由于变量 input 未被赋值,所以 if 语句的求值结果为 true

1
2
3
4
5
6
var input;
if (input === undefined) {
doThis();
} else {
doThat();
}

类似的,由于下面代码当中数组 myArray 里的元素未被赋值,所以 myFunction() 函数将会得到执行:

1
2
3
var myArray = [];

if (!myArray[0]) myFunction();

在数值类型的计算当中,undefined 值会被自动转换为非数值 NaN

1
2
3
var number;

number + 2; // NaN

同样在数值计算当中,当对一个空值变量 null 进行求值时,其值会被自动转换为 0;而如果是在布尔类型运算当中,null 值则会被作为 false 进行处理:

1
2
3
var test = null;

console.log(test * 32); // 0

作用域

全局变量是在函数之外声明的变量,可以在源文件当中的其它位置进行访问;而局部变量是在函数内部声明的变量,其只能在当前函数的内部进行访问。

ECMAScript 6 之前的 JavaScript 不存在语句块作用域的概念,语句块当中声明的变量将会成为语句块所在函数或者全局作用域的局部变量,例如下面代码会在控制台输出 2022,因为变量 code 的作用域处于全局范围,而并非仅限于 if 语句块。

1
2
3
4
5
if (true) {
var code = 610000;
}

console.log(code); // 610000

如果使用 ECMAScript 6 当中的 let 关键字进行声明,则上述行为将会发生变化:

1
2
3
4
if (true) {
let code = 610000;
}
console.log(code); // Uncaught ReferenceError: code is not defined

变量提升

JavaScript 变量支持先使用再声明,而且不会引发任何异常,称为变量提升。但是提升之后的变量,如果没有及时的进行初始化,则依然会返回 undefined 值:

1
2
3
4
5
6
7
8
9
10
/* 示例 1 */
console.log(num === undefined); // true
var num = 2022;

/* 示例 2 */
var name = "Hank";
(function () {
console.log(name); // undefined
var name = "uinika";
})();

注意:由于 JavaScript 变量提升特性的存在,所有 var 变量的声明语句应当尽量放置该函数顶部,从而极大的提升代码清晰度。

ECMAScript 6 提供的 letconst 关键字声明的变量,同样会存在变量提升的情况,但是并不会被赋予任何初始值,如果在变量声明之前引用(此时该变量处于暂时性死区,该状态会持续到该变量被声明为止),则会抛出 ReferenceError 引用错误异常。

1
2
console.log(week); // Uncaught ReferenceError: week is not defined
let week = 7;

函数提升

JavaScript 当中的函数,只有函数声明会被提升至源文件顶部,而函数表达式并不会被提升。

1
2
3
4
5
6
7
8
9
10
11
/* 函数声明 */
test1(); // test1
function test1() {
console.log("test1");
}

/* 函数表达式 */
test2(); // Uncaught TypeError: test2 is not a function
var test2 = function () {
console.log("test2");
};

全局变量

全局变量本质上是全局对象的属性,Web 浏览器当中的全局对象是 window,可以通过 window.variable 设置和访问全局变量:

1
2
var URL = "www.uinio.com";
console.log(window.URL); // www.uinio.com

NodeJS 当中的全局对象是 global,则需要使用 global.variable 来设置和访问全局变量。

1
2
URL = "www.uinika.com";
console.log(global.URL); // www.uinika.com

常量

使用关键字 const 可以创建一个只读的常量,其命名规则与变量相同,必须以字母、下划线 _、美元符号 $ 开头,并且可以包含有字母、数字、下划线。

1
const PI = 3.141592654;

常量即不可以重新赋值,也不可以在代码运行时重新声明,所以声明常量的时候,必须显式的将其初始化为某个具体值。常量的作用域规则与 let 块级作用域变量相同,在同一个作用域当中,不能使用与变量名或者函数名相同的名字来命名常量:

1
2
function WEEK() {}
const WEEK = 7; // Uncaught SyntaxError: Identifier 'WEEK' has already been declared

然而,对于常量对象属性进行赋值并不会受到保护,所以下面语句在执行时不会产生错误:

1
2
const TEST = { key: "value1" };
TEST.key = "value2"; // 'value2'

同样的,对于常量数组元素进行赋值也不会受到保护,所以下面语句在执行时同样不会产生错误:

1
2
3
const WEB = ["HTML", "CSS"];
WEB.push("JAVASCRIPT");
console.log(WEB); // ['HTML', 'CSS', 'JAVASCRIPT']

数据类型

最新的 ECMAScript 标准当中一共定义了 8 种数据类型,其中 7 种基本数据类型,以及 1 种对象数据类型:

数据类型 名称 描述
Boolean 布尔值 拥有 truefalse 两个值;
null 空值 表示空值,由于 JavaScript 大小写敏感,所以 nullNullNULL 三者完全不同。
undefined 未定义 表示变量未赋值时的属性;
Number 数值 用于存放整数或者浮点数;
BigInt 大整数 用于安全的存储和操作大整数;
String 字符串 表示一串文本值的字符序列;
Symbol 独一无二的值 ECMAScript 6 新增加的数据类型,是一种实例唯一并且不可改变的数据类型;
Object 对象 用于充当各种数据类型的命名容器;

数据类型转换

JavaScript 是一种动态类型语言,这意味着在声明变量时无需指定数据类型,变量的数据类型会在代码执行期间,根据需要自动进行转换:

1
2
var temporary = 2022;
temporary = "wwww.uinio.com";

如果表达式当中同时包含数字字符串,通过加法运算符 + 可以将数值转换成字符串:

1
2
3
let x = "The Number is " + 2021; // 'The Number is 2021'

let y = 2022 + " is the Number"; // '2022 is the Number'

但是 JavaScript 当中并不能使用减法运算符 - 完成相同的任务:

1
2
"36" - 16; // 20
"19" + 85; // '1985'

字符串转换为数值

通过 parseInt(string, radix) 函数可以将字符串转换为整数(会丢弃小数部分),其中第 2 个参数 radix 表示进制或者基数,其取值范围介于 2 ~ 36 之间:

1
2
3
parseInt("32", 16); // 50

parseInt("0x32"); // 50

而通过 parseFloat(string) 函数则可以将字符串转换为浮点数

1
parseFloat(3.141592654); // 3.141592654

注意:如果第 1 个参数 string 并非表达一个有效数值,那么上述函数就会返回一个非数 NaN

将字符串转换为数字的另外一种方法,就是采用简单粗暴的加法运算符 +

1
2
3
"1.1" + "5.5" = "1.15.5"

(+"1.1") + (+"5.5") = 6.6

各种字面量

字面量(Literals)是由特定语法表达式所定义的一组常量。

数组

数组是一个在方括号 [] 当中包含有零个或者多个表达式的列表,其中每个表达式代表数组的一个元素,元素的个数就是该数组的长度。下面示例声明并且初始化了一个拥有 3 个元素的数组 motors,同时采用 length 关键字获取数组长度,并通过索引 Array[index] 访问数组里的元素。

1
2
3
4
var motors = ["Geely", "Haval", "Chery"];

console.log(motors.length); // 3
console.log(motors[0]); // Geely

注意:数组字面量同时也是一个 Array 对象。

初始化数组时如果连续输入两个逗号 ,,则数组当中就会产生一个初始值为 undefined 的元素。

1
2
3
4
var animal = ["Lion", , "dog", "cat"];

console.log(animal.length); // 4
console.log(animal[1]); // undefined

如果在元素列表的尾部添加了一个逗号,则这个都好将会被忽略,数组依然保持原有的长度。

1
2
3
4
var animal = ["Hank", "uinika"];

console.log(animal.length); // 2
console.log(animal[2]); // undefined

注意:尾部逗号在早期的 Web 浏览器当中会产生错误,所以最佳实践是移除它们。而在必要的时候,可以显式的将元素声明为 undefined

布尔值

布尔类型只有 truefalse 两种字面量,下面示例通过 typeof 运算符获取变量的数据类型:

1
2
3
4
5
let yes = true;
let no = false;

console.log(typeof yes); // boolean
console.log(typeof no); // boolean

注意Boolean 只是布尔类型的包装对象,两者不能混淆使用;

整数

整数可以采用十进制(基数为 10)、十六进制(基数为 16)、八进制(基数为 8)、二进制(基数为 2)进行表示:

  • 十进制整数:由一串数值序列组成,并且没有前缀 0,例如 0、117-345`;
  • 八进制的整数:前缀为 00O0o(严格模式下,必须以 0o0O 开头),只能包含数字 0 ~ 7,例如 0150001-0o77
  • 十六进制整数:以 0x0X 开头,可以包含数字 0 ~ 9 和字母 a ~ f 或者 A ~ F,例如 0x11230x001110xF1A7
  • 二进制整数:以 0b0B 开头,只能包含数字 01,例如 0b110b0011-0b11
1
2
3
4
0, 117  -345  十进制, 基数为10)
015, 0001 and -0o77 (八进制, 基数为8)
0x1123, 0x00111 and -0xF1A7 (十六进制, 基数为16"hex")
0b11, 0b0011 and -0b11 (二进制, 基数为2)

浮点数

浮点数的字面量可以由带正负号 +/- 前缀的十进制整数部分小数点 .十进制小数部分指数部分组成。其中指数部分以 e 或者 E 作为前缀,后面紧接着一个带有正负号 +/- 的整数。

1
2
3
-0.12345789; // -0.12345789
3.14e10; // 31400000000
0.1e-20; // 1e-21

对象

对象是一种封闭在花括号 {} 当中,具有多个键值对元素的列表。下面示例创建了一个 auto 对象,将第 1 个元素 motor1 定义为变量 motor1 的值;第二个元素,属性 getCar,引用了一个函数(即 CarTypes("Honda"));第三个元素,属性 special,使用了一个已有的变量(即 Sales)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var motor1 = "BYD";

function drive(name) {
return name === "BYD" ? "对不起," + name + " 的 EV 受限!" : "把 " + name + " 开回家!";
}

var auto = {
motor1: motor1,
motor2: "AION",
getMotor: drive,
};

console.log(auto.motor1); // BYD
console.log(auto.motor2); // AION
console.log(auto.getMotor("BYD")); // 对不起,BYD 的 EV 受限!

可以采用数值作为对象的属性名称,引用该属性的时候,则需要采用类似于数组索引 object.[property] 的访问方式:

1
2
3
4
let motor = { favorites: { 1: "BYD", 2: "AION" }, 3: "Tesla" };

console.log(motor.favorites[1]); // BYD
console.log(motor[3]); // Tesla

同理,如果需要采用其它不合法的属性名称,则同样可以通过数组索引的方式进行访问:

1
2
3
4
5
6
7
let motor = {
埃安: "AION",
特斯拉: "Tesla",
};

console.log(motor["埃安"]); // AION
console.log(motor["特斯拉"]); // Tesla

ECMAScript 6 支持创建对象时通过 __proto__ 设置原型、简化 key: value 属性赋值、直接在对象中定义函数、支持调用父对象方法、动态计算表达式属性名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let property = "this is a property";

const Test = {
/* 对象创建时设置原型 */
__proto__: {
name: "Hank",
},

property, // 缩写 property: property

/* 直接定义函数 */
toString() {
return super.toString(); // 调用父对象上的函数
},

["property_" + (() => 1985)()]: 2022, // 动态计算属性名称
};

console.dir(Test);
/*
property: "this is a property"
property_1985: 2022
toString: ƒ toString()
[[Prototype]]: Object
name: "Hank"
[[Prototype]]: Object
*/

RegExp

正则表达式字面量是一个被两条正斜线 / ... / 包围起来的表达式。

1
const RegExp = /ab+c/;

字符串

字符串是由双引号 " 或者单引号 ' 括起来的零个或者多个字符:

1
2
3
"2022";
"uinika";
"Hank's \n motor";

JavaScript 会自动将字符串字面量转换为一个 String 对象进行处理,所以可以直接调用 String.length 属性:

1
console.log("Hank's motor".length); // 12

ECMAScript 6 提供一种模板字面量(Template Literals)功能,用于通过一些语法糖来构造字符串:

1
2
3
4
5
6
7
8
9
10
11
let name = "Hank";
const URL = "www.uinio.com";
var greet = `hello
world`; //

console.log(`${name}'s blog is ${URL}`); // Hank's blog is www.uinio.com
console.log(greet);
/*
hello
world
*/

转义字符

下面的表格列举了可以在 JavaScript 字符串当中使用的特殊转义字符

特殊字符 功能解释
\0 Null 字节;
\b 退格符;
\f 换页符;
\n 换行符;
\r 回车符;
\t Tab 制表符;
\v 垂直制表符;
\' 单引号;
\" 双引号;
\\ 反斜杠字符;
\XXX 由从 0 ~ 377 三位八进制数 XXX 表示的 Latin-1 字符,例如:\251 是版权符号 © 的八进制表达;
\xXX 由从 00 和 FF 的两位十六进制数字 XX 表示的 Latin-1 字符,例如:\xA9 是版权符号 © 的十六进制表达;
\uXXXX 由 4 位十六进制数字 XXXX 表示的 Unicode 字符,例如:\u00A9 是版权符号 © 的 Unicode 表达;
\u{XXXXX} Unicode 代码点(Code Point)转义字符,例如:\u{2F804} 相当于 Unicode 转义字符 \uD87E\uDC04 的简写形式;

注意:严格模式下,不能使用八进制转义字符。

通过转义字符,可以在字符串当中方便的插入引号反斜线,甚至于进行字符串换行处理:

1
2
3
4
5
6
"Hank's blog is 'www.uinio.com'"; // "Hank's blog is 'www.uinio.com'"

"C:\\Workspace\\blog"; // 'C:\\Workspace\\blog'

"Hank's Github is \
https://github.com/uinika"; // "Hank's Github is https://github.com/uinika"

运算符

JavaScript 根据操作数的不同,可以划分为一元二元三元总共 3 类运算符。

赋值运算符

赋值运算符   示例 描述
赋值 = x = y x = y
加法赋值 += x += y x = x + y
减法赋值 -= x -= y x = x - y
乘法赋值 *= x *= y x = x * y
除法赋值 /= x /= y x = x / y
求余赋值 %= x %= y x = x % y
求幂赋值 **= x **= y x = x ** y
左移位赋值 <<= x <<= y x = x << y
右移位赋值 >>= x >>= y x = x >> y
无符号右移位赋值 >>>= x >>>= y x = x >>> y
按位与赋值 &= x &= y x = x & y
按位异或赋值 ^= x ^= y x = x ^ y
按位或赋值 │= x │= y x = x │ y

比较运算符

比较运算符   示例 描述
等于 == 2006 == 2006 或者 2006 == "2006",返回 true 左右两侧操作数相等时返回 true
不等于 != 2006 != 2019 或者 2006 != "2019",返回 true 左右两侧操作数不相等时返回 true
严格等于 === 2022 === 2022 返回 true2022 === "2022" 返回 false 左右两侧操作数相等并且数据类型相同时返回 true
不严格等于 !== 2022 !== 2022 返回 false2022 !== "2022" 返回 true 左右两侧操作数不相等并且数据类型不相同时返回 true
大于 > 2021 > 2022 或者 2021 > "2022",返回 false 左侧操作数大于右侧时返回 true
大于等于 >= 2021 >= 2021 或者 2021 >= "2021",返回 true 左侧操作数大于或者等于右侧时返回 true
小于 < 2019 < 2022 或者 2019 < "2022",返回 true 左侧操作数小于右侧时返回 true
小于等于 <= 2019 <= 2019 或者 2019 <= "2019",返回 true 左侧操作数小于或者等于右侧时返回 true

算数运算符

算数运算符   示例 描述
求余 % 15 % 6,返回 3 返回左右两侧操作数相除之后的余数
自增 ++ let index = 0 时,首先执行 ++index 返回 1,然后再执行 index++ 依然返回 1 如果放置在操作数前面,则返回加上 1 之后的值;如果放置在操作数后面,则返回操作数之后再加上 1
自减 -- let index = 6 时,首先执行 --index 返回 5,然后再执行 index-- 依然返回 5 如果放置在操作数前面,则返回减去 1 之后的值;如果放置在操作数后面,则返回操作数之后再减去 1
负值 - console.log(-5),返回 -5 返回操作数的负值;
正值 + console.log( +"5" ),返回 5 返回操作数的正值,同时将操作数转换为数值类型;
指数运算 ** 2 ** 3,返回 8 返回指数运算之后的结果,左侧为底数部分,右侧为指数部分;

位运算符

位运算符   示例 描述
按位与 & a & b 左右两侧操作数每一个位都为 1 时返回 1,否则返回 0
按位或 a │ b 左右两侧操作数每一个位只要其中一个为 1 则返回 1,否则返回 0
按位异或 ^ a ^ b 左右两侧操作数的每一个对应的位不相同就返回 1,否则返回 0
按位非 ~ ~ a 对右侧操作数的二进制形式按位取反;
左移 << a << b 将左侧操作数的二进制形式向左移动右侧操作数个位,并在右侧填充 0
右移 >> a >> b 将左侧操作数的二进制形式向右移动右侧操作数个位,左侧填充位由左侧操作数的最高位是 0 还是 1 来决定;
无符号右移 >>> a >>> b 将左侧操作数的二进制形式向右移动右侧操作数个位,并将左侧全部填充为 0

逻辑运算符

逻辑运算符   示例 描述
逻辑与 && true && true 返回 truefalse && false 返回 falsetrue && false 返回 false; 当操作数都为 true 时返回 true,否则返回 false
逻辑或 ││ true ││ true 返回 truefalse ││ false 返回 falsetrue ││ false 返回 true; 任意一个操作数为 true 时返回 true,如果操作数都为 false 则返回 false
逻辑非 ! !true 返回 false!false 返回 true 如果操作数为 true 时返回 false,反之则会返回 true

条件运算符

条件运算符是 JavaScript 当中唯一的三目运算符(需要 3 个操作数),其返回结果是根据指定条件在两个值当中选取一个。

1
condition ? value1 : value2;

如果条件 condition 的判断结果为 true,则返回 value1,否则返回 value2

1
2
3
4
5
const getStage = (age) => {
return age >= 18 ? "成年人" : "小孩";
};

console.log(getStage(36)); // 成年人

分组操作符

分组操作符 () 可以用于控制了表达式当中计算的优先级。

1
2
3
4
5
6
let x = 1;
let y = 2;
let z = 3;

console.log(x + y * z); // 7
console.log((x + y) * z); // 9

typeof 操作符

typeof 操作符用于返回当前操作数的数据类型,可以通过如下方式进行调用:

1
typeof operand;

上面的 typeof 操作符将会返回一个表示操作数 operand 数据类型的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var year = new Function("2022 - 2010");
var name = "Hank";
var age = 36;
var today = new Date();

/* 判断已定义变量的数据类型 */
console.log(typeof year); // function
console.log(typeof name); // string
console.log(typeof age); // number
console.log(typeof today); // object

/* 判断未定义变量的数据类型 */
console.log(typeof nothing); // undefined

/* JavaScript 预定义关键字的数据类型 */
console.log(typeof true); // boolean
console.log(typeof NaN); // number
console.log(typeof null); // object
console.log(typeof Infinity); // number

void 操作符

void 操作符用于表示右侧表达式没有返回值,通常用于标识 HTML 页面当中不需要操作返回值的 JavaScript 表达式。

1
void expression;

下面的代码创建了一个超文本链接,当用户使用鼠标点击之后,不会产生任何效果。

1
<a href="javascript:void(0)">无效果点击</a>

下面的超文本链接会在用户鼠标点击之后,提交一个 HTML 表单。

1
<a href="javascript:void(document.form.submit())">点击提交</a>

字符串连接

字符串连接符 + 用于连接两个字符串,也允许通过 += 完成字符串拼接。

1
2
3
4
5
6
7
8
9
/* 使用 + 拼接字符串 */
let string1 = "Hello";
let string2 = " Hank";
console.log(string1 + string2); // Hello Hank

/* 使用 += 拼接字符串 */
let string3 = "Hello";
string3 += " PCB";
console.log(string3); // Hello PCB

剩余参数

ES6 提供的剩余参数运算符 ... 可以用于快速原地展开一个数组或者一组函数参数

1
2
3
4
5
6
7
8
9
10
11
/* 展开函数参数 */
function total(x, y, z) {
return x + y + z;
}
let arguments = [1, 2, 3];
total(...arguments); // 6

/* 展开数组 */
const color = ["B", "l", "u", "e"];
const pcb = [...color, "P", "C", "B"];
console.log(pcb); // ['B', 'l', 'u', 'e', 'P', 'C', 'B']

解构赋值

ES6 提供的解构赋值语法可以用于从数组当中提取元素,或者从对象当中提取属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 提取数组当中的元素 */
const militaryArray = ["陆军", "海军", "空军"];
let [army, navy, airForce] = militaryArray;
console.log(army + "➙" + navy + "➙" + airForce); // 陆军➙海军➙空军

/* 提取对象上的属性 */
const up = {
army: "陆军",
navy: "海军",
};
const down = {
airForce: "空军",
missileForce: "火箭军",
};
const militaryObject = { ...up, ...down };
console.log(militaryObject); // {army: '陆军', navy: '海军', airForce: '空军', missileForce: '火箭军'}

流程控制

JavaScript 提供了一套灵活的流程控制语句,可以在应用程序当中实现大量的交互性功能。

语句块

语句块用于组合多条 JavaScript 语句,由一对大括号 {} 进行界定:

1
2
3
4
5
6
7
8
9
{
statement_1;
statement_2;
statement_3;
.
.
.
statement_n;
}

语句块通常与 ifforwhile 等流程控制语句结合起来使用:

1
2
3
4
5
let week = 1;

while (week <= 7) {
console.log(week++); // 1 2 3 4 5 6 7
}

ECMAScript 6 标准之前,Javascript 并不存在块作用域的概念,变量的作用域从声明位置开始,一直会覆盖到源文件的末尾:

1
2
3
4
5
6
7
8
var day = 27;

/* 块作用域 */
{
var day = 1;
}

console.log(day); // 1

而使用 ECMAScript 6 的 letconst 关键字声明的变量,其作用域仅限于该声明所在的语句块

1
2
3
4
5
6
7
8
9
10
let year = 2022;

/* 块作用域 */
{
let year = 2016;
const month = 12;
}

console.log(year); // 2022
console.log(month); // Uncaught ReferenceError: month is not defined

条件判断

JavaScript 支持 if...elseswitch 两种条件判断语句。

if...else 语句

条件表达式 condition 返回 true 或者 false,如果返回的是 true,就会执行 statement_1 语句;如果返回的是 false,则会执行 statement_2 语句:

1
2
3
4
5
if (condition) {
statement_1;
} else {
statement_2;
}

通过组合使用 else if 语句,可以连续测试多种 condition_x 判断条件:

1
2
3
4
5
6
7
8
9
if (condition_1) {
statement_1;
} else if (condition_2) {
statement_2;
} else if (condition_n) {
statement_n;
} else {
statement_last;
}

通常不建议在条件表达式 condition 当中使用赋值语句,因为阅读代码时容易将其视为等值比较运算符 ==。如果一定要在条件表达式当中使用赋值语句,通常需要在赋值语句前后额外添加一对括号 (condition)

1
2
3
4
5
6
let x;
let y = 2022;

if ((x = y)) {
console.log(x); // 2022
}

空字符串 ""undefinednull0NaN 在条件表达式 condition 当中,都将会被视为 false 条件:

1
2
3
4
5
if ("" && undefined && null && 0 && NaN) {
console.log("全部条件为 true");
} else {
console.log("全部条件为 false"); // 全部条件为 false
}

切记不要混淆原始布尔值 truefalseBoolean 对象的比较关系:

1
2
3
4
5
6
7
8
9
const test = new Boolean(false);

if (test) {
console.log("Boolean 对象被视为 true"); // Boolean 对象被视为 true
}

if (test == true) {
console.log("Boolean 对象被视为 false");
}

switch 语句

switch 语句首先会查找与 expression 匹配的 case 子句,然后执行该子句当中对应的代码;如果没有匹配值,则会执行 default 子句里的代码,通常会将 default 子句放置在 switch 语句的最后。

1
2
3
4
5
6
7
8
9
10
11
12
switch (expression) {
case label_1:
statements_1
[break;]
case label_2:
statements_2
[break;]
...
default:
statements_def
[break;]
}

可选的 break 语句放置在 case 子句当中,以确保匹配语句执行之后,执行流程可以跳出 switch 语句执行后续内容。如果忽略掉 break 语句,那么就会继续执行下一条 case 子句当中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const day = new Date().getDay();

/* 使用 break 语句 */
switch (day) {
case 0:
console.log("今天是星期天");
break;
case 1:
console.log("今天是星期一");
break;
case 2:
console.log("今天是星期二");
break;
case 3:
console.log("今天是星期三");
break;
case 4:
console.log("今天是星期四");
break;
case 5:
console.log("今天是星期五");
break;
case 6:
console.log("今天是星期六");
break;
}
/*
今天是星期一
*/

/* 不使用 break 语句 */
switch (day) {
case 0:
console.log("今天是星期天");
case 1:
console.log("今天是星期一");
case 2:
console.log("今天是星期二");
case 3:
console.log("今天是星期三");
case 4:
console.log("今天是星期四");
case 5:
console.log("今天是星期五");
case 6:
console.log("今天是星期六");
}
/*
今天是星期一
今天是星期二
今天是星期三
今天是星期四
今天是星期五
今天是星期六
*/

循环

JavaScript 提供了 fordo...whilewhilelabelbreakcontinuefor...infor...of 一共 8 种循环相关的语法。

for 循环

for 循环会一直重复执行,直至循环条件变为 false

1
2
3
for ([initialExpression]; [condition]; [incrementExpression]) {
statement;
}
  1. 首先,执行初始化表达式 initialExpression,通常该表达式会初始化一个或多个循环计数器;
  2. 然后,计算 condition 表达式的值,如果 conditiontrue,那么执行循环当中的语句;如果 conditionfalse,那么循环将会终止;如果省略 condition 表达式,则 condition 的值默认为 true
  3. 接着,执行循环里的 statement 语句,多条语句可以使用代码块 {} 进行包裹;
  4. 如果 incrementExpression 表达式存在更新,则会执行更新表达式,并且重新执行第 2 个步骤;
1
2
3
4
5
6
7
8
9
10
11
12
for (var index = 0; index < 6; index++) {
console.log("当前索引为", index);
}

/*
当前索引为 0
当前索引为 1
当前索引为 2
当前索引为 3
当前索引为 4
当前索引为 5
*/

do...while 循环

do...while 循环同样会一直重复直执行,直至指定的条件为 false

1
2
3
do {
statement;
} while (condition);

statement 语句会在检查 condition 条件之前会执行一次,多条 statement 语句需要放置到花括号 {} 当中。如果 conditiontrue,那么 statement 语句将会再一次执行;而如果 conditionfalse,则循环终止运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let index = 0;

do {
index++;
console.log("当前索引为", index);
} while (index < 5);

/*
当前索引为 0
当前索引为 1
当前索引为 2
当前索引为 3
当前索引为 4
当前索引为 5
*/

while 循环

while 循环只要指定的条件 conditiontrue,就会一直执行 statement 语句块。如果条件 conditionfalse,则会终止循环。

1
2
3
while (condition) {
statement;
}

do...while 循环不同的是,while 循环的 condition 条件检测会在每次 statement 执行之前发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let index = 0;

while (index < 5) {
index++;
console.log("当前索引为", index);
}

/*
当前索引为 0
当前索引为 1
当前索引为 2
当前索引为 3
当前索引为 4
当前索引为 5
*/

label 语句

label 用于在程序当中提供一个代码标识符,然后结合 breakcontinue 语句精确的返回到代码指定的位置。

1
2
label:
statement;

这里的 label 可以是任意合法的 JavaScript 标识符,而 statement 则可以是任意需要标识的 JavaScript 语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* label 与 break 结合使用 */
let result1 = 0;
myLabel: for (var index1 = 0; index1 < 10; index1++) {
for (var index2 = 0; index2 < 10; index2++) {
if (index1 == 5 && index2 == 5) {
break myLabel; // 当 i = 5 且 j = 5 时跳出全部循环,继续执行 myLabel 下方语句
}
result1++;
}
}
console.log(result1); // 55

/* label 与 continue 结合使用 */
let result2 = 0;
myLabel: for (var index1 = 0; index1 < 10; index1++) {
for (var index2 = 0; index2 < 10; index2++) {
if (index1 == 5 && index2 == 5) {
continue myLabel; // 当 i = 5 且 j = 5 时跳过本次循环,继续执行 myLabel 下方语句
}
result2++;
}
}
console.log(result2); // 95

break 语句

break 语句用于终止全部循环,当 break 没有指定 label 时,就会立刻终止当前所在的 whiledo...whilefor 循环语句,以及 switch 流程控制语句,并且执行这些语句之后的内容;而当使用带有 labelbreak 时,则会在终止全部循环之后,跳转至 label 所在的位置继续执行。

1
break [label];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
for (let index = 0; index < 5; index++) {
console.log("当前索引为", index);
if (index === 2) {
console.log("当索引等于 2 时结束全部循环");
break;
}
}

/*
当前索引为 0
当前索引为 1
当前索引为 2
当索引等于 2 时结束全部循环
*/

continue 语句

continue 语句用于终止单次循环,当 continue 没有指定 label 时,就会立刻终止本次的 whiledo...whilefor 循环,以及 switch 流程控制语句,并且继续执行下一次循环;而当使用带有 labelcontinue 时,则只会在中断本次循环之后,跳转至 label 所处位置继续执行。

1
continue [label];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (let index = 0; index < 5; index++) {
console.log("当前索引为", index);
if (index === 2) {
console.log("当索引等于 2 时跳过该次循环");
continue;
}
}

/*
当前索引为 0
当前索引为 1
当前索引为 2
当索引等于 2 时跳过该次循环
当前索引为 3
当前索引为 4
*/

for...in 循环

for...in 循环通常用于遍历某个对象的可枚举属性

1
2
3
for (property in object) {
statements;
}

当在 for...in 循环当中访问对象属性时,可以采用 object[property] 的语法形式:

1
2
3
4
5
6
7
8
9
10
11
const URL = { blog: "www.uinika.com", github: "uinika.github.io", gitee: "uinika.gitee.io" };

for (url in URL) {
console.log(url + " ➡ " + URL[url]);
}

/*
blog ➡ www.uinika.com
github ➡ uinika.github.io
gitee ➡ uinika.gitee.io
*/

除此之外,for...in 还可以用于遍历数组元素,但是此时返回的并非对象名称,而是数组索引

1
2
3
4
5
6
7
8
9
10
11
const URLs = ["www.uinika.com", "uinika.github.io", "uinika.gitee.io"];

for (index in URLs) {
console.log(index + " ➡ " + URLs[index]);
}

/*
0 ➡ www.uinika.com
1 ➡ uinika.github.io
2 ➡ uinika.gitee.io
*/

注意for...in 是为了遍历对象属性而设计,由于会遍历出所有可枚举的属性,所以不建议使用它来遍历数组。

for...of 循环

for...of 用于遍历数组时,返回的结果是每一个数组元素。

1
2
3
4
5
6
7
8
9
10
11
const URLs = ["www.uinika.com", "uinika.github.io", "uinika.gitee.io"];

for (url of URLs) {
console.log(url);
}

/*
www.uinika.com
uinika.github.io
uinika.gitee.io
*/

Array.forEach() 方法

Array.prototype.forEach() 方法同样也可以用于迭代指定数组的每一个元素,并且执行一次给定的函数。

1
array.forEach(callback(currentValue, index, array), thisArg);

forEach 的参数 callback 是迭代数组当中的每个元素时,所要执行的回调函数,该函数的 currentValue 参数表示当前正在迭代的元素,可选的 index 参数表示当前元素的索引,可选的 array 指代当前正在操作的数组;而另一个参数 thisArg 可选,表示执行 callback 回调函数时的 this 指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const URLs = ["www.uinika.com", "uinika.github.io", "uinika.gitee.io"];

URLs.forEach((currentValue, index, array) => {
console.log(index);
console.log(array);
console.log(currentValue + "\n");
});

/*
0
['www.uinika.com', 'uinika.github.io', 'uinika.gitee.io']
www.uinika.com

1
['www.uinika.com', 'uinika.github.io', 'uinika.gitee.io']
uinika.github.io

2
['www.uinika.com', 'uinika.github.io', 'uinika.gitee.io']
uinika.gitee.io
*/

数组

数组用于表示一个有序的数据集合,可以通过数组的名称和索引访问其中的元素。

创建数组

JavaScript 没有内置明确的数组数据类型,但是可以通过数组字面量语法,以及内置的 Array 对象,显式的创建拥有指定元素的数组:

1
2
3
const array = [element0, element1, ..., elementN]; // 数组字面量方式
const array = Array(element0, element1, ..., elemntN);
const array = new Array(element0, element1, ..., elementN);

除此之外,还可以通过如下方式创建一个指定长度的空数组

1
2
3
const array = [];
const array = Array(arrayLength);
const array = new Array(arrayLength);

初始化数组

JavaScript 支持通过直接为元素赋值的方式来初始化数组:

1
2
3
4
5
6
const URLs = [];
URLs[0] = "www.uinio.com";
URLs[1] = "www.uinimi.com";
URLs[2] = "www.uinika.com";

console.log(URLs); // ['www.uinio.com', 'www.uinimi.com', 'www.uinika.com']

也可以像本节内容开头所描述的那样,在创建数组的时候直接进行初始化:

1
2
3
4
const fruits = ["苹果", "西柚", "橙子"];

const trees = new Array("松树", "柳树", "樟树");
console.log(trees); // ['松树', '柳树', '樟树']

引用数组元素

通过数组元素的索引,可以从 0 开始引用数组的元素:

1
2
3
4
5
const trees = new Array("松树", "柳树", "樟树");

console.log(trees[0]); // 松树
console.log(trees[1]); // 柳树
console.log(trees[2]); // 樟树

数组长度

通过数组对象上提供的 length 属性,可以获取当前数组的实际长度信息,也就是数组元素的实际个数:

1
2
const fruits = ["苹果", "西柚", "橙子"];
console.log(fruits.length); // 3

通过手动设置 length 属性,还可以对数组的长度进行调整:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 当 length 属性大于实际数组长度时,采用 undefined 填充多余元素 */
const fruits = ["苹果", "西柚", "橙子"];
fruits.length = 4;
console.log(fruits); // ['苹果', '西柚', '橙子', empty]
console.log(fruits[3]); // undefined

/* 当 length 属性小于实际数组长度时,就会截取尾部多余的元素 */
fruits.length = 2;
console.log(fruits); // ['苹果', '西柚']

/* 将 length 属性设置为 0 可以直接清空数组 */
fruits.length = 0;
console.log(fruits); // []

遍历数组

使用 for 循环遍历数组元素是一种比较常规的操作:

1
2
3
4
5
6
7
8
9
10
const colors = ["红色", "绿色", "蓝色"];

for (let index = 0; index < colors.length; index++) {
console.log(colors[index]);
}
/*
红色
绿色
蓝色
*/

使用 forEach() 则是另外一种遍历数组元素的方法,传递给 forEach() 的函数会在遍历数组的每个元素时执行一次,数组元素将会作为参数传递给该函数。

1
2
3
4
5
6
7
8
9
10
const colors = ["红色", "绿色", "蓝色"];

colors.forEach(function (color) {
console.log(color);
});
/*
红色
绿色
蓝色
*/

注意forEach() 不会遍历出定义数组时未经初始化的元素,但是会遍历出被手动赋值为 undefined 的元素:

原则上 for...in 语句是为遍历对象设计的,使用其迭代数组时,会枚举出所有的可枚举属性,生产环境下不建议使用。例如下面示例代码,就将定义在 Array 对象原型上的 blog 遍历打印了出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Array.prototype.blog = function () {
console.log("www.uinio.com");
};

const colors = ["红色", "绿色", "蓝色"];

for (index in colors) {
console.log(index + colors[index]);
}
/*
0红色
1绿色
2蓝色
blogfunction () {
console.log("www.uinio.com");
}
*/

函数

函数是 JavaScript 的基本语法组件,由一系列用于执行计算任务的语句构成,而且与变量一样,只能在其有效的作用域当中进行调用。

函数定义

JavaScript 函数采用 function 关键字进行定义,主要由函数名称参数列表(包围在圆括号 () 当中并且由逗号 , 进行分隔)、函数体(采用大括号 {} 包围,并且使用 return 关键字返回结果)三个部分组成。

1
2
3
function square(size) {
return size * size;
}
  • 值传递基本数据类型的参数值会直接传递给函数,如果在函数当中修改了该参数的值,这种影响并不会传递到函数外部的作用域;
  • 引用传递对象类型的参数值会将引用传递给函数,如果在函数当中修改了对象的属性,这种影响就会传递到函数外部的作用域;
1
2
3
4
5
6
7
8
9
10
11
12
let number = 2022;
const object = { name: "Hank" };

function update(number, object) {
number = 1985; // 修改基本数据类型
object.name = "uinika"; // 修改对象类型
}

update(number, object);

console.log(number); // 2022
console.log(object); // {name: 'uinika'}

函数表达式

通过函数表达式方式创建的函数不需要直接指定函数名称,例如前面的 square() 函数也可以采用如下方式进行声明:

1
2
3
const square = function (size) {
return size * size;
};

采用函数表达式,可以非常方便的将函数作为参数传递给另外一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function battleax() {
console.log("这是一枚战斧导弹!");
}

function destroyer(status, missile) {
missile();
if (status === "击中") {
console.log("驱逐舰已经被击中!");
}
}

destroyer("击中", battleax);

/*
这是一枚战斧导弹!
驱逐舰已经被击中!
*/

函数表达式的另外一个用途,则是可以根据判断条件来声明定义一个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let happy, depression;
let weather = "SUNNY";

if (weather === "SUNNY") {
happy = function () {
console.log("我的心情很开心!");
};
} else {
depression = function () {
console.log("我的心情很沮丧!");
};
}

happy(); // 我的心情很开心!
depression(); // Uncaught ReferenceError: depression is not defined

注意:如果调用不符合判断条件的函数,JavaScript 解析引擎将会提示 ReferenceError 错误。

Function 对象

每一个 JavaScript 函数本质上都是一个 Function 对象,通过 Function() 构造函数就可以动态创建一个函数对象:

1
new Function (参数1, 参数2, ..., 函数体)

参数是由 JavaScript 有效标识符组成的字符串,当具有多个参数时可以使用逗号 , 分隔。而函数体则是一个包含有函数定义语句的 JavaScript 字符串。

1
2
3
const sum = new Function("a", "b", "return a + b");

console.log(sum(1985, 2022)); // 4007

通过 Function() 创建函数会存在与 eval() 类似的安全问题和相对较小的性能问题,并且 Function 创建的函数只能在全局作用域当中运行,只能访问到全局变量和自身的局部变量,并不能访问 Function() 构造函数所在作用域当中的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let local = 1985;

const year1 = function () {
let local = 2022;
return new Function("return local;"); // 此处的变量 local 指向全局作用域的 1985
};

const year2 = function () {
let local = 2022;
return function () {
return local; // 此处的变量 local 指向函数 year2 作用域的 2022
};
};

let resut1 = year1();
console.log(resut1()); // 1985

let resut2 = year2();
console.log(resut2()); // 2022

虽然上述代码可以在 Web 浏览器当中正常执行,但是在 NodeJS 当中函数 year1() 将会产生 ReferenceError: local is not defined 错误,这是由于 NodeJS 里 .js 源文件的顶级作用域并非全局作用域,函数 year1() 当中的变量 local 实质位于当前模块的作用域之中。

注意:如果当前 JavaScript 运行环境是 Web 浏览器,执行以 Function() 方式构造的函数,需要修改 Web 浏览器默认的 HTTP 内容安全策略 "content_security_policy""unsafe-eval"

调用函数

声明并且定义函数之后,就可以通过 函数名称() 的方式调用函数。这里要特别注意函数定义一定要位于调用语句所处的作用域,而函数声明则可以被提升,因而可以出现在调用语句之后。

1
2
3
4
5
console.log(square(50)); // 2500

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

函数表达式无法进行提升,所以将上面代码修改为如下格式是错误的:

1
2
3
4
5
console.log(square(50)); // Uncaught SyntaxError: Identifier 'square' has already been declared

const square = function (size) {
return size * size;
};

函数可以在自身的函数体当中进行递归调用,下面的示例当中通过函数的递归调用来计算阶乘:

1
2
3
4
5
6
7
8
9
10
function factorial(number) {
if (number == 0 || number == 1) {
return 1;
} else {
/* 递归调用函数本身 */
return number * factorial(number - 1);
}
}

factorial(3); // 6

函数的作用域

函数当中定义的局部变量只能在该函数体内部进行访问,除此之外,还可以访问到全局变量或者定义在父函数当中的局部变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 全局变量 */
let name = "Hank";
const URL = "www.uinio.com";

function printName1() {
return name;
}

function printName2() {
let name = "uinika"; // 局部变量

function getName() {
return name + "'s Blog " + URL; // 访问函数外层的局部变量以及全局变量
}

return getName();
}

console.log(printName1()); // Hank
console.log(printName2()); // uinika's Blog www.uinio.com

递归函数调用

通过函数名称arguments.callee指向该函数的变量名称,就可以在一个函数的内部调用自身,也就是所谓的递归。例如下面函数里,bar()arguments.callee()foo() 三条语句都可以调用函数自身:

1
2
3
4
5
const China = function chinese() {
China(); // 函数名称
arguments.callee();
chinese(); // 指向该函数的变量名称
};

使用递归函数的时候,需要注意加入合适的循环终止条件,避免进入到死循环,锁死当前 JavaScript 解析引擎的执行线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/* 采用 while 循环打印 0 ~ 3 范围的整数 */
let index = 0;
/* index <= 3 是循环中止条件 */
while (index <= 3) {
console.log(index);
index++;
}
/*
0
1
2
3
*/

/* 将 while 循环转换为递归函数调用 */
function loop(index) {
/* index > 3 是循环中止条件 */
if (index > 3) {
return;
}
console.log(index);
loop(index + 1);
}
loop(0);
/*
0
1
2
3
*/

当需要获取一个树结构当中的所有节点时,使用递归函数会比循环语句更为方便:

1
2
3
4
5
6
7
8
9
10
function traversalTree(node) {
if (node == null) {
return;
}
console.log("可以在这个位置操作树节点");

for (let i = 0; i < node.childNodes.length; i++) {
traversalTree(node.childNodes[i]);
}
}

除此之外,通过灵活的应用递归函数,还可以在函数当中模拟出堆栈数据结构的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function traversal(index) {
if (index < 0) {
return;
}

console.log("开始:" + index);
traversal(index - 1);
console.log("结束:" + index);
}

traversal(3);
/*
开始:3
开始:2
开始:1
开始:0
结束:0
结束:1
结束:2
结束:3
*/

嵌套函数

父函数当中可以嵌套一个子函数,此时子函数可以在父函数当中进行调用,子函数内部将会形成一个可以访问父函数参数与变量的闭包,而父函数并不能使用子函数当中的参数与变量。

1
2
3
4
5
6
7
8
9
10
11
function totalSquares(size1, size2) {
function square(size) {
return size * size;
}
// console.log(size); // Uncaught ReferenceError: size is not defined
return square(size1) + square(size2);
}

console.log(totalSquares(1, 2)); // 5
console.log(totalSquares(2, 3)); // 13
console.log(totalSquares(3, 4)); // 25

这是由于闭包可以保存其所见作用域当中的所有参数与变量,所以下面示例当中的子函数 child 被返回时,parent 的参数值 x 也会被一同保留下来:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 父函数 */
function parent(x) {
/* 子函数 */
function child(y) {
return x + y;
}
return child;
}

const childFunction = parent(2); // 使用静态变量 childFunction 接收返回的 child 函数

console.log(childFunction(6)); // 8
console.log(parent(5)(8)); // 13

JavaScript 函数支持多层嵌套,例如函数 A 可以包含函数 B,而函数 B 可以再包含函数 C。此时函数 BC 都形成了闭包,它们都同时包含着多个作用域(B 可以访问 AC 可以访问 BA),这种递归式包含的作用域称为作用域链,具体请参考下面的示例:

1
2
3
4
5
6
7
8
9
10
11
function A(x) {
function B(y) {
function C(z) {
console.log(x + y + z);
}
C(3);
}
B(2);
}

A(1); // 6

当相同闭包作用域链当中的参数或者变量出现命名冲突时,距离调用位置更近的参数或者变量会拥有更高的优先级:

1
2
3
4
5
6
7
8
9
10
function parent() {
let number = 5;

function child(number) {
return (number = 8); // 变量 number 出现命名冲突
}
return child;
}

parent()(2022); // 8

闭包 Closure

正如前面内容所提到的,JavaScript 允许函数嵌套,并且子函数可以访问父函数以及父函数所能访问作用域里的全部变量与函数,而父函数却不能访问定义在子函数当中的变量与函数。由于子函数可以访问到父函数的作用域,因此当子函数生命周期大于父函数的时候,父函数所定义变量与函数的生存周期将会比子函数更为持久。当子函数父函数作用域当中被访问时,就产生了一个闭包

1
2
3
4
5
6
7
8
9
10
/* 变量 name */
const parent = function (name) {
const child = function () {
return name; // 子函数可以访问父函数的 name 参数
};
return child; // 返回子函数,将其暴露在父函数作用域
};

const temporary = parent("Hank"); // 将返回的子函数赋值给变量 temporary
temporary(); // Hank

实际的情况可能会更加复杂,下面代码返回了一个对象,这些对象上自定义的 gettersetter 函数可以用于操作父函数 blogger 当中的内部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const blogger = function (name) {
let age;

return {
getName: function () {
return name;
},
setName: function (newName) {
name = newName;
},
getAge: function () {
return age;
},
setAge: function (newAge) {
age = newAge;
},
};
};

const article = blogger("uinika");
console.log(article.getName()); // uinika

article.setName("Hank");
console.log(article.getName()); // Hank

article.setAge(36);
console.log(article.getAge()); // 36

上述代码当中,子函数都可以获得 blogger() 父函数的 name 参数。除此之外,没有其它办法可以获得父函数内部定义的参数或者变量,从而为其提供了一个安全稳定的运行环境。例如下面代码当中,不能直接通过名称访问变量 code 的值,而只能通过 getCode() 函数获取变量 code 的值:

1
2
3
4
5
6
7
8
9
10
const getCode = (function () {
let code = "610000"; // 不希望被外部修改的 code 变量

return function () {
return code;
};
})();

console.log(getCode()); // 610000
console.log(code); // Uncaught ReferenceError: code is not defined

尽管有着充当局部变量保险箱的优点,但是在使用闭包特性仍然需要避免一些陷阱,例如子函数闭包当中定义了一个与父函数相同名称的变量,那么在该闭包作用域当中,将会无法引用父函数当中的这个同名变量。例如下面代码当中,调用 getCode() 函数时同时传递了 610000(父函数参数)和 610095(子函数参数)两个参数,而最终返回的 code 值为 610095

1
2
3
4
5
6
7
8
const getCode = function (code) {
return function (code) {
code = code;
console.log(code);
};
};

getCode(610000)(610095); // 610095

arguments 对象

函数的实际参数会被保存在一个 arguments 对象当中,这个 arguments 是一个类数组对象,同样可以采用索引 arguments[index] 访问具体的参数,也可以使用 arguments.length 属性获得参数的具体个数(无参数时为 0)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function blog(URLs) {
let string = "";
for (let index = 1; index < arguments.length; index++) {
string += arguments[index] + " ";
}
console.log(string);
console.log(arguments.length);
}

blog("www.uinio.com", "www.uinika.com", "uinika.gitee.io", "uinika.github.io");
/*
www.uinika.com uinika.gitee.io uinika.github.io
4
*/

注意arguments 对象常用于不确定函数会接收多少个参数的场景。

默认参数

JavaScript 函数参数的默认值是 undefined,通过 ECMAScript 6 提供的默认参数特性,可以为函数指定默认的参数值:

1
2
3
4
5
function profile(name, URL = "www.uinio.com") {
console.log(name + "'s Blog is " + URL);
}

profile("Hank"); // Hank's Blog is www.uinio.com

剩余参数

剩余参数同样也是 ECMAScript 6 提供的新特性,用于将不确定具体数量的参数表示为一个数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function profile(name, ...URLs) {
console.log(name);
console.log(URLs);

URLs.map((URL) => {
console.log(URL);
});
}

profile("Hank", "www.uinio.com", "www.uinika.com");
/*
Hank
['www.uinio.com', 'www.uinika.com']
www.uinio.com
www.uinika.com
*/

箭头函数

ECMAScript 6 提供的箭头函数相比于传统的 JavaScript 函数,具有更为简短的书写语法:

1
2
3
4
5
6
7
8
9
10
const printName = () => {
console.log("Hank");
};

const printURL = (URL) => {
console.log(URL);
};

printName(); // Hank
printURL("www.uinio.com"); // www.uinio.com

采用关键字 new 实例化一个普通 JavaScript 函数时,总是会将 this 指针指向全局对象 window 或者 global。而箭头函数则会从词法上自动绑定 this 指针至当前对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Author = function () {
this.name = "Hank";
console.dir(this); // Author

/* 箭头函数会自动绑定当前对象的 this 指针 */
const arrow = () => {
console.dir(this); // Author
};
arrow();

/* 传统函数会将 this 指针绑定到全局对象 window */
function tradition() {
console.dir(this); // window
}
tradition();
};

const author = new Author(); // 使用 new 关键字创建对象

如果需要让传统 JavaScript 函数当中的 this 指针指向当前对象,则可以将其赋值给一个命名为 that 或者 self 的局部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const Demo = function () {
const self = this;
const that = this;

function getThat() {
that.name = "Hank";
console.dir(that);
}
getThat();

function getSelf() {
self.URL = "www.uinio.com";
console.dir(self);
}
getSelf();
};

const demo = new Demo();
/*
Demo
URL: "www.uinio.com"
name: "Hank"
[[Prototype]]: Object
Demo
URL: "www.uinio.com"
name: "Hank"
[[Prototype]]: Object
*/

预定义函数

JavaScript 语言内置了一系列可以在全局作用域当中直接进行调用的预定义函数

预定义函数 功能描述
eval() 解析一段字符串形式的 JavaScript 代码; eval("console.log(this)"); // Window
isFinite() 判断传入的参数值是否为有限数值; isFinite(2022); // true
isNaN() 判断传入的参数值是否为 NaN isNaN(NaN); // true
parseInt() 将参数字符串解析为一个整数值; parseInt("2006"); // 2006
parseFloat() 将参数字符串解析为一个浮点数值; parseFloat("3.14"); // 3.14
encodeURI() 编码 URI 当中除 数字大小写字母 以及 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) # 以外的字符; encodeURI("www.uinika.com?blog=博客");
decodeURI() 解码 URI 当中除 数字大小写字母 以及 ; , / ? : @ & = + $ - _ . ! ~ * ' ( ) # 以外的字符; decodeURI('www.uinika.com?blog=%E5%8D%9A%E5%AE%A2');
encodeURIComponent() 编码 URI 当中除 数字大小写字母 以及 - _ . ! ~ * ' ( ) 之外的字符; encodeURIComponent("www.uinika.com?blog=博客");
decodeURIComponent() 解码 URI 当中除 数字大小写字母 以及 - _ . ! ~ * ' ( ) 之外的字符; decodeURIComponent("www.uinika.com%3Fblog%3D%E5%8D%9A%E5%AE%A2");

注意:相比于 encodeURI() 方法,encodeURIComponent() 方法的编码转义范围更为广泛,但是转换之后的 URI 地址无法直接进行访问。

对象

JavaScript 当中的对象是一系列属性和方法的集合。

创建对象

JavaScript 当中对象创建方式的不同,也会导致 this 指针的的指向有所不同,在使用时必须特别注意。

对象字面量 {}

JavaScript 允许通过字面量 {} 方式创建对象,其语法格式如下所示:

1
2
3
4
const objectName = {
propertyName: value,
functionName: function () {},
};

下面代码采用字面量方式定义了一个 Person 对象,该对象拥有一个 name 参数和一个 printName() 方法:

1
2
3
4
5
6
7
8
9
const Person = {
name: "Hank",
printName: function () {
console.log(this); // {name: 'Hank', age: 36, printName: ƒ, printAge: ƒ}
console.log(this.name);
},
};

Person.printName(); // Hank

注意:在使用字面量创建的对象当中定义箭头函数时,箭头函数当中的 this 指针将会指向全局对象 window

1
2
3
4
5
6
7
8
9
const Person = {
name: "Hank",
printAge: () => {
console.log(this); // Window {window: Window, self: Window, document: document, name: '', location: Location, …}
console.log(this.name);
},
};

Person.printAge(); // undefined

构造函数 function

采用 function 关键字同样可以创建一个对象,但是与字面量语法不同的是,在使用之前必须采用关键字 new 手动进行实例化。

1
2
3
4
5
6
7
function objectName(parameter) {
this.propertyName = parameter;
this.functionName = function () {};
this.functionName = () => {};
}

new objectName(parameter);

下面代码采用 function 构造函数方式定义了一个 Person 对象,该对象拥有一个 name 参数和一个 printName() 方法:

1
2
3
4
5
6
7
8
9
10
11
function Person(name) {
this.name = name;
/* 采用普通 function 函数定义对象方法 */
this.printName = function () {
console.log(this); // Person {name: 'uinika', printName: ƒ}
console.log(this.name);
};
}

const person = new Person("uinika");
person.printName(); // uinika

在采用 function 构造函数创建的对象当中定义箭头函数时,箭头函数内的 this 指针依然会指向当前对象 Person

1
2
3
4
5
6
7
8
9
10
11
function Person(name) {
this.name = name;
/* 采用箭头函数定义对象方法 */
this.printName = () => {
console.log(this); // Person {name: 'uinika', printName: ƒ}
console.log(this.name);
};
}

const person = new Person("uinika");
person.printName(); // uinika

Object.create() 方法

基于现有对象的 __proto__ 创建一个新的对象,而不需要使用到字面量语法 {},或者构造函数 function

1
Object.create(proto,[propertiesObject])

上面的 proto 表示新创建对象的原型(可缺省),而 propertiesObject 表示提供属性的对象,该方法执行后返回一个带有指定原型和属性的新对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
const Person = {
URL: "",
print: function () {
console.log(this); // {name: 'Hank', URL: 'www.uinika.com'}
console.log(`我是 ${this.name},我的博客是 ${this.URL}。`);
},
};

const person = Object.create(Person); // 使用 Object.create() 创建对象

person.name = "Hank";
person.URL = "www.uinika.com";
person.print(); // 我是 Hank,我的博客是 www.uinika.com。

注意prototype 是函数才会拥有的属性,而 proto 是每个对象都会拥有的属性。

创建对象实例 new

new 操作符用于创建一个自定义对象的实例

1
const objectName = new objectType([parameter1, parameter2, ..., parameterN]);

上面的 objectName 是对象名称,而 objectType 是对象参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Author(name, URL) {
this.name = name;
this.URL = URL;
this.print = () => {
console.log(this);
};
}

const author = new Author("Hank", "www.uinio.com");
author.print();
/*
Author {name: 'Hank', URL: 'www.uinio.com', print: ƒ}
URL: "www.uinio.com"
name: "Hank"
print: () => { console.log(this); }
[[Prototype]]: Object
*/

指向对象自身 this

this 关键字指向当前对象本身,可以用于引用当前对象上定义的方法或者属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 字面量对象的 this 指针 */
const Author = {
name: "Hank",
print: function () {
console.log(this);
},
};
Author.print(); // {name: 'Hank', print: ƒ}

/* 函数对象当中的 this 指针*/
const Writer = function () {
this.name = "Hank";
this.print = function () {
console.log(this);
};
};
const writer = new Writer();
writer.print(); // Writer {name: 'Hank', print: ƒ}

访问父对象 super

super 关键字可以用于引用当前对象的父对象构造函数,或者用于访问父对象上定义的方法属性

1
2
super([arguments]); // 调用父对象的构造函数
super.parentFunction([arguments]); // 调用父对象上的方法

上面的 arguments 是传递给父对象的构造函数或者普通方法的参数,而 parentFunction 指代的是父对象上定义的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/* 父对象 */
class Person {
constructor(name) {
this.name = name;
}
print() {
console.log(this);
}
}

/* 子对象 */
class Hank extends Person {
constructor(name, URL) {
super(name); // 调用父对象 Person 上的构造函数
this.URL = URL;
}
print() {
super.print(); // 调用父对象 Person 上定义的 print() 方法,但是打印结果为 Hank 对象上的 this 指针
}
}

const hank = new Hank("uinika", "www.uinio.com");
console.log(hank);
/*
Hank {name: 'uinika', URL: 'www.uinio.com'}
URL: "www.uinio.com"
name: "uinika"
[[Prototype]]: Person
*/
hank.print();
/*
Hank {name: 'uinika', URL: 'www.uinio.com'}
URL: "www.uinio.com"
name: "uinika"
[[Prototype]]: Person
*/

删除属性 delete

delete 操作符用于删除指定的对象属性(非继承)或者数组元素。如果 delete 操作成功,将会返回 true,否则返回 false

1
2
delete arrayName[index]; // 删除数组元素
delete objectName.property; // 删除对象属性

上面的 objectName 表示对象名称,arrayName 则是指数组名称,而 property 是一个对象上的属性,index 则是数组当中的元素索引。

1
2
3
4
5
6
7
8
9
10
const Author = {
name: "Hank",
url: "www.uinio.com",
age: 36,
};

let result = delete Author.age;

console.log(result); // true
console.log(Author); // {name: 'Hank', url: 'www.uinio.com'}

删除数组中的元素时,数组的长度不会发生改变,只是被删除元素所在索引会指向 undefine 值。

1
2
3
4
5
6
const trees = new Array("橡树", "松树", "柳树", "杉树", "樟树");
delete trees[2];

console.log(trees); // ['橡树', '松树', empty, '杉树', '樟树']
console.log(trees[2]); // undefined
console.log(trees.length); // 5

判断属性存在 in

in 操作符用于判断对象的属性名称或者数组的元素索引是否真实存在,如果存在就会返回 true,否则返回 false

1
2
index in arrayName; // 判断数组的元素索引是否存在
property in objectName; // 判断对象的属性名称是否存在

上面的 arrayName 是数组名称,而 index 是数组索引。objectName 是对象名称,而 property 表示对象属性。

1
2
3
4
5
6
7
8
9
10
11
/* 采用 in 操作符判断对象的属性名称是否存在 */
const Author = {
name: "Hank",
url: "www.uinio.com",
age: 36,
};
console.log("name" in Author); // true

/* 采用 in 操作符判断数组的元素索引是否存在 */
const trees = ["橡树", "松树", "柳树", "杉树", "樟树"];
console.log(2 in trees); // true

判断实例类型 instanceof

用于判断当前的实例是否为指定对象的实例,如果是就返回 true,否则返回 false

1
objectName instanceof objectType;

上面的 objectName 是需要进行判断的实例,而 objectType 则是对象的类型。例如, 下面的代码使用 instanceof 去判断 theDay 是否是一个 Date 对象. 因为 theDay 是一个 Date 对象, 所以 if 中的代码会执行.

1
2
3
4
5
const date = new Date(2022, 1, 5);

console.log(date instanceof Date); // true
console.log(date instanceof Object); // true
console.log(date instanceof String); // false

getters & setters

JavaScript 对象的 settergetter 是一种用于设置或者获取属性值的函数方法。其中,getter 方法没有参数,但是有返回值;而 setter 方法只会接受一个参数,但是没有返回值。

通过在使用字面量语法 {} 定义对象时,添加 get property()(把对象属性绑定到获取该属性时将会被调用的函数)和 set property(value)(把对象属性绑定到设置属性时将会被调用的函数)就可以完成 setter/getter 方法的添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const Author = {
name: "Hank",
get age() {
return this.name + " is 36!";
},
set blog(URL) {
this.name = URL;
},
};
console.log(Author.name); // Hank

/* getter 生效 */
console.log(Author.age); // Hank is 36!

/* setter 生效 */
Author.blog = "www.uinio.com";
console.log(Author.name); // www.uinio.com
console.log(Author);
/*
age: (...)
name: "www.uinio.com"
get age: ƒ age()
set blog: ƒ blog(URL)
[[Prototype]]: Object
*/

禁止在某个属性的 setter 方法中对该属性赋值,这样会导致递归调用的发生,从而引发 RangeError 错误:

1
2
3
4
5
6
7
8
const Author = {
blog: "",
set blog(URL) {
this.blog = URL; // Uncaught RangeError: Maximum call stack size exceeded
},
};

Author.blog = "www.uinika.com";

对于已经定义完毕的对象,可以采用 Object.defineProperty() 方法添加 setter/getter 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Author = {
name: "Hank",
};

Object.defineProperties(Author, {
age: {
get: function () {
return this.name + " is 36!";
},
},
blog: {
set: function (URL) {
this.name = URL;
},
},
});

/* getter 生效 */
console.log(Author.age); // Hank is 36!

/* setter 生效 */
Author.blog = "www.uinio.com";
console.log(Author.name); // www.uinio.com

注意Object.defineProperty(obj, prop, descriptor) 方法可以用于在对象上定义新的属性,或者修改对象上的已有属性。其中 obj 是需要定义属性的对象,而 prop 是要定义或者修改的属性名称,descriptor 则是需要定义或者修改的属性描述符,该方法的返回值为处理之后的新对象。

遍历对象属性

JavaScript 提供了 for...inObject.keys(obj)Object.getOwnPropertyNames(obj) 三种方法来遍历一个对象上的所有属性。

for...in 会依次访问指定对象及其原型链上面所有可以枚举的属性,也包括继承的可枚举属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Author = {
url: "www.uinio.com",
name: "Hank",
age: 36,
};

for (propertyName in Author) {
console.log("属性名称:" + propertyName);
console.log("属性值:" + Author[propertyName]);
}
/*
属性名称:url
属性值:www.uinio.com
属性名称:name
属性值:Hank
属性名称:age
属性值:36
*/

Object.keys(obj) 会返回参数对象 obj 包含(不包括原型链)的所有可枚举属性的名称的数组,数组当中属性名称的排列顺序和正常循环遍历该对象的顺序保持一致。

1
2
3
4
5
6
7
const Author = {
url: "www.uinio.com",
name: "Hank",
age: 36,
};

console.log(Object.keys(Author)); // ['url', 'name', 'age']

Object.getOwnPropertyNames(obj) 会返回参数对象 obj(不包括原型链)上全部属性(无论该属性是否可枚举)名称所构成的数组。

1
2
3
4
5
6
7
const Author = {
url: "www.uinio.com",
name: "Hank",
age: 36,
};

console.log(Object.getOwnPropertyNames(Author)); // ['url', 'name', 'age']

原型继承

JavaScript 是一种基于原型(Prototype)而非基于(Class)的面向对象语言。

  • 对于 Java 和 C++ 等基于的面向对象语言,会应用关键字 class 声明一个类,然后通过构造函数(用于在创建类时初始化属性值),使用 new 关键字创建该类的实例(Instance);
  • JavaScript 的面向对象是基于原型的,只存在对象而没有的概念,原型可以被视为一个创建对象的模板,任何对象都可以作为另外一个对象的原型,从而允许后者共享前者的属性

注意:传统 ES5 当中,可以通过对象字面量 {}、构造函数 functionObject.create() 三种方式来创建对象;而 ES6 当中新引入的 class 类定义语法,其实质是现有原型继承方式的语法糖。

接下来的内容当中,将会基于如下的对象继承结构,建立 EmployeeManagerWorkerBeeEngineerSalesPerson 共 5 个对象:

  1. Employee 拥有默认值为空字符串的 name 属性和默认值为 "General"department 属性;
  2. ManagerEmployee 的子类,其增加了一个默认值为空数组的 reports 属性,后面会将其赋值为 Employee 对象数组;
  3. WorkerBeeEmployee 的子类,其增加了一个默认值为空数组的 projects 属性;
  4. Engineer 也是 WorkerBee 的子类,其增加了一个默认值为空字符串的 machine 属性,同时将 department 属性重载为 "Engineering"
  5. SalesPersonWorkerBee 的子类,其增加了一个默认值为 100quota 属性,并且将继承来的 department 属性重载为 "sales",表示所有销售人员都属于相同部门;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/*===== Employee =====*/
function Employee() {
this.name = "";
this.department = "General";
}

/*===== Manager =====*/
function Manager() {
Employee.call(this);
this.reports = [];
}
Manager.prototype = Object.create(Employee.prototype);

/*===== WorkerBee =====*/
function WorkerBee() {
Employee.call(this);
this.projects = [];
}
WorkerBee.prototype = Object.create(Employee.prototype);

/*===== SalesPerson =====*/
function SalesPerson() {
WorkerBee.call(this);
this.department = "sales";
this.quota = 100;
}
SalesPerson.prototype = Object.create(WorkerBee.prototype);

/*===== Engineer =====*/
function Engineer() {
WorkerBee.call(this);
this.department = "Engineering";
this.machine = "";
}
Engineer.prototype = Object.create(WorkerBee.prototype);

const jim = new Employee();
console.log(jim);
/*
Employee {name: '', department: 'General'}
department: "General"
name: ""
[[Prototype]]: Object
constructor: ƒ Employee()
[[Prototype]]: Object
*/

const sally = new Manager();
console.log(sally);
/*
Manager {name: '', department: 'General', reports: Array(0)}
department: "General"
name: ""
reports: []
[[Prototype]]: Employee
[[Prototype]]: Object
constructor: ƒ Employee()
[[Prototype]]: Object
*/

const mark = new WorkerBee();
console.log(mark);
/*
WorkerBee {name: '', department: 'General', projects: Array(0)}
department: "General"
name: ""
projects: []
[[Prototype]]: Employee
[[Prototype]]: Object
constructor: ƒ Employee()
[[Prototype]]: Object
*/

const fred = new SalesPerson();
console.log(fred);
/*
SalesPerson {name: '', department: 'sales', projects: Array(0), quota: 100}
department: "sales"
name: ""
projects: []
quota: 100
[[Prototype]]: Employee
[[Prototype]]: Employee
[[Prototype]]: Object
constructor: ƒ Employee()
[[Prototype]]: Object
*/

const jane = new Engineer();
console.log(jane);
/*
Engineer {name: '', department: 'Engineering', projects: Array(0), machine: ''}
department: "Engineering"
machine: ""
name: ""
projects: []
[[Prototype]]: Employee
[[Prototype]]: Employee
[[Prototype]]: Object
constructor: ƒ Employee()
[[Prototype]]: Object
*/

注意:如果上面代码里的构造函数不需要接受任何参数,则可以省略构造函数名称后面的圆括号()

继承属性

下面代码创建了一个 mark 对象作为 WorkerBee 的实例,当 JavaScript 执行 new 操作符的时候,首先会创建一个对象,并且将该对象当中的 [[prototype]] 指向 WorkerBee.prototype,然后再将该对象设置为执行 WorkerBee 构造函数时的 this 值。该对象的 [[Prototype]] 决定了其用于检索属性的原型链。当构造函数执行完毕之后,所有属性都完成了初始化,就可以将其引用赋值给一个 mark 变量:

1
const mark = new WorkerBee();

上述过程不会显式将 mark 所继承的原型链属性,作为 mark 对象的本地属性。访问属性时,首先 JavaScript 会检查对象自身是否存在该属性,如果存在就返回属性值,如果不存在则会通过 [[Prototype]] 检查原型链。如果原型链当中依然未能查找出该属性,则会返回一个 undefined。当 worker 对象实例化完成之后,其属性值状态如下所示:

1
2
3
mark.name = "";
mark.department = "General";
mark.projects = [];

对象 mark 通过 mark.__proto__Employee 继承了 namedepartment 属性,并且定义了一个新的 projects 属性,下面的示例代码修改了这些属性的默认值:

1
2
3
4
5
6
7
8
9
10
11
12
13
mark.name = "Hank";
mark.department = "Blog";
mark.projects = ["www.uinio.com"];

console.log(mark);
/*
WorkerBee {name: 'Hank', department: 'Blog', projects: Array(1)}
department: "Blog"
name: "Hank"
projects: ['www.uinio.com']
[[Prototype]]: Employee
[[Prototype]]: Object
*/

添加属性

JavaScript 允许在运行时为对象添加新的属性,例如通过 mark.age = 18 添加一个 bonus 属性,此时,除了 mark 对象之外的其它 WorkerBee 对象不会拥有该属性。

1
2
3
4
5
6
7
/* mark 对象 */
const mark = new WorkerBee();
mark.age = 18;

/* hank 对象 */
const hank = new WorkerBee();
console.log(hank.age); // undefined

如果向对象的原型添加新的属性,那么该属性将会添加至从该原型继承属性的所有对象当中,例如下面代码会向所有 Employee 的对象添加 specialty 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Employee.prototype.specialty = "none";

const mark = new WorkerBee();
console.log(mark);
/*
WorkerBee {name: '', department: 'General', projects: Array(0)}
department: "General"
name: ""
projects: []
[[Prototype]]: Employee
[[Prototype]]: Object
specialty: "none"
constructor: ƒ Employee()
[[Prototype]]: Object
*/

Employee 原型当中添加 specialty 属性之后,可以在 Engineer 的原型里对该属性进行重载,将其默认值 none 重新修改为 code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Engineer.prototype.specialty = "code";
const jane = new Engineer();
console.log(jane);

/*
Engineer {name: '', department: 'Engineering', projects: Array(0), machine: ''}
department: "Engineering"
machine: ""
name: ""
projects: Array(0)
length: 0
[[Prototype]]: Array(0)
[[Prototype]]: Employee
specialty: "code"
[[Prototype]]: Employee
[[Prototype]]: Object
specialty: "none"
constructor: ƒ Employee()
[[Prototype]]: Object
*/

属性默认值 & 构造函数参数

JavaScript 的逻辑或操作符 || 会判断第 1 个参数,如果其结果为 true 则返回该值,否则返回第 2 个参数的值,利用该特性可以巧妙定义对象属性的默认值:

1
this.propertyName = newValue || defaultValue;

除此之外,采用 function 构造函数声明对象时,还可以同时对属性的参数值进行赋值:

1
2
3
function Name(newValue) {
this.propertyName = newValue || defaultValue;
}

接下来,修改开头示例代码当中的 EmployeeWorkerBeeEngineer 类为下面的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*===== Employee =====*/
function Employee(name, department) {
this.name = name || "";
this.department = department || "General";
}

/*===== WorkerBee =====*/
function WorkerBee(projects) {
this.projects = projects || [];
}
WorkerBee.prototype = new Employee();

/*===== Engineer =====*/
function Engineer(machine) {
this.department = "Engineering";
this.machine = machine || "";
}
Engineer.prototype = new WorkerBee();

这样在创建 Engineer 对象的实例时,就可以通过构造函数指定属性的初始值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const hank = new Engineer("Keyboard");
console.log(hank);
/*
Engineer {department: 'Engineering', machine: 'Keyboard'}
department: "Engineering"
machine: "Keyboard"
[[Prototype]]: Employee
projects: []
[[Prototype]]: Employee
department: "General"
name: ""
[[Prototype]]: Object
constructor: ƒ Employee(name, department)
[[Prototype]]: Object
*/

通过 function 构造函数可以为对象指定本地的属性值,但是无法为经过原型链继承而来的属性进行赋值,因而需要调整 WorkerBeeEngineer 的构造函数定义,使得父对象的构造函数成为子对象base 成员方法,并且立刻在子对象当中进行调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*===== Employee =====*/
function Employee(name, department) {
this.name = name || "";
this.department = department || "General";
}

/*===== WorkerBee =====*/
function WorkerBee(name, department, projects) {
this.base = Employee; // 指向父对象构造函数
this.base(name, department);
this.projects = projects || [];
}
WorkerBee.prototype = new Employee();

/*===== Engineer =====*/
function Engineer(name, projects, machine) {
this.base = WorkerBee; // 指向父对象构造函数
this.base(name, "Engineering", projects);
this.machine = machine || "";
}
Engineer.prototype = new WorkerBee();

const hank = new Engineer("Hank", ["www.uinio.com", "www.uinika.com"], "Keyboard");
console.log(hank);
/*
Engineer {name: 'Hank', department: 'Engineering', projects: Array(2), machine: 'Keyboard', base: ƒ}
base: ƒ Employee(name, department)
department: "Engineering"
machine: "Keyboard"
name: "Hank"
projects: (2) ['www.uinio.com', 'www.uinika.com']
[[Prototype]]: Employee
base: ƒ Employee(name, department)
department: "General"
name: ""
projects: []
[[Prototype]]: Employee
department: "General"
name: ""
[[Prototype]]: Object
constructor: ƒ Employee(name, department)
[[Prototype]]: Object
*/

除了使用上述的 function 构造函数之外,还可以通过 call() 或者 apply() 函数来实现继承,例如可以将上面的 Engineer 对象等价的修改为下面形式:

1
2
3
4
function Engineer(name, projects, machine) {
WorkerBee.call(this, name, "Engineering", projects); // 这种方式比定义 base 属性更加简洁
this.machine = machine || "";
}

本地属性 & 继承属性

JavaScript 当中访问一个对象的属性时,将会执行如下两个步骤:

  1. 检查该属于是否存在于对象本地,如果存在,则直接返回属性值;
  2. 如果不存在,就会通过 __proto__ 属性检查原型链,如果存在就返回值,不存在则返回 undefined

例如下面代码当中的 amy 对象具有本地属性 projects,而 namedepartment 属性则是通过 __proto__ 获得:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*===== Employee =====*/
function Employee() {
this.name = "";
this.department = "General";
}

/*===== WorkerBee =====*/
function WorkerBee() {
this.projects = [];
}
WorkerBee.prototype = new Employee();

const amy = new WorkerBee();
console.log(amy);
/*
WorkerBee {projects: Array(0)}
projects: []
[[Prototype]]: Employee
department: "General"
name: ""
[[Prototype]]: Object
constructor: ƒ Employee()
[[Prototype]]: Object
*/

此时如果修改父对象 Employee 关联原型当中的 name 属性,这种影响并不会传递到子对象通过原型继承而来的 name 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*===== Employee =====*/
function Employee() {
this.name = "";
this.department = "General";
}

/*===== WorkerBee =====*/
function WorkerBee() {
this.projects = [];
}
WorkerBee.prototype = new Employee();

const amy = new WorkerBee();
Employee.prototype.name = "Unknown"; // 修改父对象 Employee 关联原型当中的 name 属性值
console.log(amy);
/*
WorkerBee {projects: Array(0)}
projects: []
[[Prototype]]: Employee
department: "General"
name: ""
[[Prototype]]: Object
name: "Unknown"
constructor: ƒ Employee()
[[Prototype]]: Object
*/

如果在修改父对象的属性值时,希望这种修改操作可以被所有的子对象继承,那么就不能将其定义为本地属性,而应该将其添加至关联的原型当中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Employee() {
this.department = "General";
}
Employee.prototype.name = ""; // 将 name 属性定义在原型上

function WorkerBee() {
this.projects = [];
}
WorkerBee.prototype = new Employee();

const amy = new WorkerBee();
Employee.prototype.name = "Unknown";
console.log(amy);
/*
WorkerBee {projects: Array(0)}
projects: Array(0)
length: 0
[[Prototype]]: Array(0)
[[Prototype]]: Employee
department: "General"
[[Prototype]]: Object
name: "Unknown"
constructor: ƒ Employee()
[[Prototype]]: Object
*/

JavaScript 会首先在对象自身的本地属性当中进行查找,如果没有找到则会到对象的特殊属性 __proto__ 当中查找,这个过程称为在对象的原型链中查找,这就是 JavaScript 的对象属性查找机制。

prototype 与 __proto__

__proto__ 是一个用于访问对象内部 [[Prototype]] 的访问器属性,该属性已经在 ECMAScript 6 规范当中得到了标准化。通过 Object.prototype.__proto__ 方式访问 __proto__ 属性的方法已经逐渐被废弃,Mozilla 官方更加推荐使用 Object.getPrototypeOf 方式进行访问。而 prototype 表示的是原型对象,所有的 JavaScript 对象都继承自 Object.prototype,该对象当中的默认属性列表如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
__proto__: (…)
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
}

除了 Object 对象以外,无论是采用对象字面量 {},还是采用构造函数 function,或者是采用 Object.create() 方法创建的对象,都会拥有一个 __proto__ 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/*===== 采用字面量方式创建的对象 =====*/
const test1 = {};
console.log(test1.prototype); // undefined
console.log(test1.__proto__);
/*
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
__proto__: (…)
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
*/

/*===== 采用构造函数 function 方式创建的对象 =====*/
const temp = function () {};
const test2 = new temp();
console.log(test2.prototype); // undefined
console.log(test2.__proto__);
/*
{constructor: ƒ}
constructor: ƒ ()
arguments: null
caller: null
length: 0
name: "temp"
prototype: {constructor: ƒ}
[[FunctionLocation]]: VM2674:1
[[Prototype]]: ƒ ()
[[Scopes]]: Scopes[2]
[[Prototype]]: Object
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
__proto__: (…)
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
*/

/*===== 采用 Object.create() 方式创建的对象 =====*/
const test3 = Object.create({});
console.log(test3.__proto__);
/*
{}
[[Prototype]]: Object
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
__proto__: (…)
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
*/

JavaScript 函数的 prototype 属性实际上指的是 Function.prototype,其属性值为解析引擎的原生代码。而下面示例里打印出的 prototype 属性,则是 JavaScript 解析引擎将 test2 视为构造函数之后的处理结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const test2 = function () {};

console.log(test2.__proto__); // ƒ () { [native code] }
console.log(test2.prototype);
/*
{constructor: ƒ}
constructor: ƒ ()
arguments: null
caller: null
length: 0
name: "test2"
prototype: {constructor: ƒ}
[[FunctionLocation]]: VM1976:1
[[Prototype]]: ƒ ()
[[Scopes]]: Scopes[2]
[[Prototype]]: Object
constructor: ƒ Object()
hasOwnProperty: ƒ hasOwnProperty()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toLocaleString: ƒ toLocaleString()
toString: ƒ toString()
valueOf: ƒ valueOf()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
__proto__: (…)
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
*/

对于本节内容开始时提到的 Engineer 类,如果在这里创建一个对象 hank

1
const hank = new Engineer("Hank", ["www.uinio.com", "www.uinika.com"], "Keyboard");

则对于 hank 对象而言,下面语句的返回结果全部为 true

1
2
3
4
5
hank.__proto__ == Engineer.prototype; // true
hank.__proto__.__proto__ == WorkerBee.prototype; // true
hank.__proto__.__proto__.__proto__ == Employee.prototype; // true
hank.__proto__.__proto__.__proto__.__proto__ == Object.prototype; // true
hank.__proto__.__proto__.__proto__.__proto__.__proto__ == null; // true

instanceof 判断对象的实例

除了 Object 对象之外,每一个对象都会拥有 __proto__ 属性,而每一个函数都会拥有 prototype 属性。创建对象时 JavaScript 会将特殊的 __proto__ 属性设置为构造函数的 prototype 属性值,所以表达式 new Example() 时会创建一个对象,该对象的 __proto__ 属性等于 Example.prototype。如果修改 Example.prototype 的值,就会改变所有通过 new Example() 创建的对象。

通过比较对象__proto__ 属性和函数prototype 属性,就可以检测出对象之间的继承关系。JavaScript 提供了便捷的 instanceof 操作符,用于检测对象与构造函数之间的关系,如果对象继承自构造函数的原型,则返回 true,否则就会返回 false

1
2
3
4
5
6
7
8
const empty = {};
console.log(empty instanceof Object); // true
console.log(empty instanceof Function); // false

function None() {}
const none = new None();
console.log(none instanceof None); // true
console.log(none instanceof Object); // true

多重继承

由于 JavaScript 的继承是基于对象的原型链实现的,每个对象只会存在一个与之关联的原型,所以 JavaScript 不支持多重继承。但是,可以通过在构造函数当中调用多个其它构造器函数,从而模拟出一种多重继承的假象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/*===== Employee =====*/
function Employee(name, department) {
this.name = name || "";
this.department = department || "General";
}

/*===== WorkerBee =====*/
function WorkerBee(name, department, projects) {
this.base = Employee; // 指向父对象构造函数
this.base(name, department);
this.projects = projects || [];
}
WorkerBee.prototype = new Employee();

/*===== Hobbyist =====*/
function Hobbyist(hobby) {
this.hobby = hobby || "Football";
}

/*===== Engineer =====*/
function Engineer(name, projects, machine, hobby) {
/* 调用 WorkerBee 构造函数*/
this.base1 = WorkerBee;
this.base1(name, "Engineering", projects);
/* 调用 Hobbyist 构造函数*/
this.base2 = Hobbyist;
this.base2(hobby);
this.machine = machine || "";
}
Engineer.prototype = new WorkerBee();

const uinika = new Engineer("Hank", ["Blog"], "Electronics");
console.log(uinika);
/*
Engineer {name: 'Hank', department: 'Engineering', projects: Array(1), base1: ƒ, base: ƒ, …}
base: ƒ Employee(name, department)
base1: ƒ WorkerBee(name, department, projects)
base2: ƒ Hobbyist(hobby)
department: "Engineering"
hobby: "Football"
machine: "Electronics"
name: "Hank"
projects: ['Blog']
[[Prototype]]: Employee
base: ƒ Employee(name, department)
department: "General"
name: ""
projects: []
[[Prototype]]: Employee
department: "General"
name: ""
[[Prototype]]: Object
*/

上面代码当中,对象 uinika 确实从 Hobbyist() 构造函数当中获得了 hobby 属性,但是如果通过 Hobbyist.prototype.equipment = ["Oscilloscope", "Multimeter", "Solder"] 添加一个新的 equipment 属性到 Hobbyist 构造函数的原型,那么对象 uinika不会自动继承该属性

类 class

类是用于创建对象的模板,ES6 规范当中提出的 class 依然是建立在原型基础之上的。

类的声明与定义

在 ES6 规范当中,通过 class 关键字就可以声明一个 JavaScript 类,该类同样属于 Object 的子类:

1
2
3
4
5
6
7
8
9
class Rectangle {
constructor(height, width) {
this.height = height;
this.width = width;
}
}

const rectangle = new Rectangle();
console.log(rectangle instanceof Object); // true

上面代码当中的 constructor() 是一个构造函数,该方法用于创建和初始化 class 类的对象,每个类都只能拥有一个构造函数

与函数声明不同,必须要先声明之后才能够进行访问,否则就会抛出 ReferenceError 错误:

1
2
3
let p = new Rectangle(); // Uncaught ReferenceError: Rectangle is not defined

class Rectangle {}

类表达式是定义 class 类的另外一种方法:

1
2
3
4
5
6
const Rectangle = class {
constructor(height, width) {
this.height = height;
this.width = width;
}
};

类体

类体放置在一对花括号 {} 当中,用于定义成员属性成员函数以及构造函数,类体当中的代码总是在 JavaScript 严格模式下执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Rectangle {
/* 构造函数 */
constructor(height, width) {
this.height = height;
this.width = width;
}
/* 成员函数 */
calcArea() {
return this.height * this.width;
}
/* Getter 方法 */
get area() {
return this.calcArea();
}
}

const square = new Rectangle(10, 10);
console.log(square.area); // 100

static 静态成员

static 关键字用于在 class 类当中定义一个静态的成员函数或者属性,调用静态方法时不需要进行实例化,直接通过的名称就可以调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
/* 静态成员属性 */
static displayName = "Point";
/* 静态成员方法 */
static distance(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.hypot(dx, dy); // 返回直角三角形的斜边长度
}
}

/* 通过对象的实例调用静态成员是错误的,会直接返回 undefined */
const p1 = new Point(5, 5);
const p2 = new Point(10, 10);
console.log(p1.displayName); // undefined
console.log(p1.distance); // undefined

/* 通过对象直接调用静态成员是正确的 */
console.log(Point.displayName); // Point
console.log(Point.distance(p1, p2)); // 7.0710678118654755

注意:静态方法通常用于为 class 类创建工具函数。

私有属性

class 类当中声明属性,默认都是公有属性

1
2
3
4
5
6
7
8
class Rectangle {
width;
height = 0;
constructor(height, width) {
this.width = width;
this.height = height;
}
}

通过在成员属性前面添加 # 符号,就可以将其声明为私有属性

1
2
3
4
5
6
7
8
9
10
11
class Rectangle {
#width;
#height = 0;
constructor(height, width) {
this.#width = width;
this.#height = height;
}
}

const rectangle = new Rectangle(10, 10);
rectangle.#width; // Uncaught SyntaxError: Private field '#width' must be declared in an enclosing class

extends 关键字

通过使用 extends 关键字,可以定义父类子类之间的继承关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*===== 父类 =====*/
class Animal {
constructor(name) {
this.name = name;
}
bark() {
console.log(`${this.name}发出声音`);
}
}

/*===== 子类 =====*/
class Dog extends Animal {
constructor(name) {
super(name); // 调用父类构造函数,并且传入 name 参数
}
bark() {
console.log(`${this.name}正在汪汪汪`);
}
}

const dog = new Dog("豆豆");
dog.bark(); // 豆豆正在汪汪汪

注意:如果子类当中定义有构造函数,那么这个子类必须调用 super() 之后才能够使用 this 指针。

ES6 规范的 class 同样也可以继承 ES5 当中基于构造函数 function 定义的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Animal(name) {
this.name = name;
}
Animal.prototype.bark = function () {
console.log(this.name + "发出声音");
};

class Dog extends Animal {
bark() {
super.bark();
console.log(this.name + "正在汪汪汪");
}
}

const dog = new Dog("豆豆");
dog.bark();
/*
豆豆发出声音
豆豆正在汪汪汪
*/

但是要注意,class 类不能继承没有构造函数的字面量 {} 对象,这样会引发 TypeError 错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Animal = {
bark: function () {
console.log("豆豆发出声音");
},
};

/* Uncaught TypeError: Class extends value #<Object> is not a constructor or null */
class Dog extends Animal {
bark() {
super.bark();
console.log("豆豆正在汪汪汪");
}
}

可以通过 Object.setPrototypeOf() 方法设置原型的方式,实现字面量对象 {}class 类的继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Animal = {
bark() {
console.log(this.name + "发出声音");
},
};

class Dog {
constructor(name) {
this.name = name;
}
}

Object.setPrototypeOf(Dog.prototype, Animal); // 通过原型链实现继承,避免调用 bark 时会返回 TypeError

const dog = new Dog("豆豆");
dog.bark(); // 豆豆发出声音

super 指向父类

class 当中的关键字 super 可以用于调用父类当中的成员函数或者构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Cat {
constructor(name) {
this.name = name;
}
meowth() {
console.log(this.name + "喵喵叫");
}
}

class Lion extends Cat {
constructor() {
super("木法沙"); // 在子类构造函数当中,调用父类构造函数
}
meowth() {
super.meowth(); // 调用父类的成员函数
console.log(this.name + "正在咆哮");
}
}

const lion = new Lion("辛巴"); // 这里的参数 “辛巴“ 会被丢弃,取而代之的是 Lion 类构造函数当中的 “木法沙“
lion.meowth();
/*
木法沙喵喵叫
木法沙正在咆哮
*/

异常处理

JavaScript 当中可以使用 throw 语句抛出一个异常,然后再用 try...catch 进行捕获。

异常类型

JavaScript 可以抛出任意对象作为错误信息,但是更加规范的做法是抛出如下的错误对象类型:

  • EvalError:全局函数 eval() 产生的错误实例;
  • RangeError:数值变量或者参数不在有效范围的错误实例;
  • ReferenceError:引用无效的错误实例;
  • SyntaxError:语法相关的错误实例;
  • TypeError:变量或参数的类型不合法的错误实例;
  • URIError:当 encodeURI() 或者 decodeURI() 传入无效参数时发生的错误实例;
  • AggregateError:当操作包含多个错误时(例如Promise.any()),可用于包装多个错误实例;
  • InternalError:JavaScript 解析引擎抛出的错误实例,例如在递归过多的时候;
  • DOMException:Web 浏览器 DOM 操作异常;

throw 语句

throw 语句用于抛出一个异常表达式 expression

1
throw expression;

这里的异常表达式 expression 可以是任意类型:

1
2
3
4
5
6
7
8
9
10
11
throw "Error"; // 抛出字符串类型

throw 0; // 抛出数值类型

throw false; // 抛出布尔类型

throw {
toString: function () {
return "抛出对象类型";
},
};

可以在抛出异常时声明一个对象,这样就可以在 catch 代码块中读取到这个对象的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 创建一个用户异常对象 UserException */
function UserException(info) {
this.name = "UserException";
this.info = info;
}

/* 将异常信息格式化为字符串 */
UserException.prototype.toString = function () {
return this.name + ': "' + this.info + '"';
};

/* 创建并且抛出 UserException 实例 */
throw new UserException("发生用户异常!"); // UserException {name: 'UserException', info: '发生用户异常!'}

try...catch 语句

如果 try 代码块当中的语句抛出异常,那么程序的执行流程就会立即进入 catch 代码块。如果 try 代码块没有抛出异常,那么程序的执行流程就会跳过 catch 代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let result;

function verify(component) {
if (component === "Transistor") {
return "是晶体管";
} else {
throw "不是晶体管";
}
}

try {
verify();
} catch (exception) {
console.log(exception); // 不是晶体管
}

catch 代码块用于捕捉所有发生在 try 代码块当中的异常,其中的 exception 参数用于接收由 throw 语句抛出的异常对象或者错误信息。

finally 语句

finally 代码块通常会放置在 try...catch 语句之后,无论是否抛出异常都会得到执行,通常用于优雅的退出操作或者释放资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
throw "发生异常";
} catch (exception) {
console.log(exception);
console.log("处理异常");
} finally {
console.log("异常处理完毕");
}

/*
发生异常
处理异常
异常处理完毕
*/

finally 语句块里使用 return 关键字返回的结果,也将是整个 try...catch...finally 语句块的返回值,这里无需关心 try 或者 catch 语句块里的返回值。

1
2
3
4
5
6
7
8
9
10
11
function test() {
try {
return "try 语句";
} catch (exception) {
return "catch 语句";
} finally {
return "finally 语句";
}
}

console.log(test()); // 'finally 语句'

Error 对象

Error 对象包含有 name(错误名称)和 message(错误信息)两个属性,通过该对象提供的 Error() 构造函数,可以非常方便的自定义错误对象。

1
2
3
4
5
6
7
8
9
10
function test() {
throw new Error("错误");
}

try {
test();
} catch (exception) {
console.log(exception.name); // Error
console.log(exception.message); // 错误
}

Promise 对象

早期的 Web 浏览器端的异步编程主要依靠 回调函数事件监听发布/订阅Promise 的各种 polyfill 这四种方式来进行,当 ES6 规范正式推出之后,在完整实现 Promise 规范的同时,还引入了 Generator 函数及其衍生的 async/await 语法糖,将 JavaScript 异步编程带入了全新的时代。

ES6 Promises 规范源自于开源社区的 Promises/A+,是浏览器 JavaScript 以及 NodeJS 上较为早期的异步事件处理方案,也是目前较常使用的异步处理机制。

Promise 的 3 种状态

ES6 Promises 使用一个 Promise 对象来代表一个异步操作,它可以包含如下三种状态:

状 态 描 述
fulfilled 操作成功,then()方法的onFulfilled函数被调用。
rejected 操作失败,then()方法的onRejected函数被调用。
pending 初始状态,可能触发fulfilledrejected状态中的一种。

pending状态的 Promise 对象可能触发fulfilled状态并传递一个值给相应的状态处理方法,也可能触发失败状态rejected并传递一个失败信息。当其中任意一种情况出现时,Promise对象then方法所绑定的处理函数(onfulfilled()onrejected())就会被调用。当Promise状态为fulfilled时调用onfulfilled(),当Promise状态为rejected时调用onrejected(),三种状态之间的转换图如下:

Promise.prototype.then()Promise.prototype.catch()方法会返回一个全新的 Promise 对象,因此它们之间可以进行链式调用。

可以通过new关键字调用构造函数Promise()去实例化一个 Promise 异步对象。

1
2
3
const promise = new Promise((resolve, reject) => {
// ... ...
});

Promise 示例代码

一个简单易于理解的使用 Promise 的示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function asyncFunction() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("你好,异步操作!");
}, 5000);
});
}

asyncFunction()
.then(function (value) {
console.info(value); // 打印"你好,异步操作!"
})
.catch(function (error) {
console.error(error); // 打印错误信息
});

下面的代码通过 Promise 的异步处理方式来获取 XMLHttpRequest 的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getURL(URL) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open("GET", URL, true);
xhr.onload = function () {
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(new Error(xhr.statusText));
}
};
xhr.onerror = function () {
reject(new Error(xhr.statusText));
};
xhr.send();
});
}

const URL = "http://www.sina.cn/demo";

getURL(URL)
.then(function onFulfilled(value) {
console.info(value);
})
.catch(function onRejected(error) {
console.error(error);
});

Promise 构造函数上的原型

Promise.prototype.constructor

表示 Promise 构造函数的原型,以便于在原型上放置then()catch()finally()方法。

1
2
3
let promise = new Promise((resolve, reject) => {
// resolve('') or reject('')
});

Promise.prototype.then(onFulfilled, onRejected)

then()方法返回一个Promise,两个参数分别是 Promise 成功或者失败状态的回调函数。如果忽略某个状态的回调函数参数,或者提供非函数参数,then()方法将会丢失该状态的回调信息,但是并不会产生错误。如果调用then()的 Promise 的状态发生改变,但是then()中并没有对应状态的回调函数,则then()最终将创建一个未经回调函数处理的全新 Promise 对象,并使用最初的 Promise 状态来作为这个全新 Promise 的状态。

1
2
3
4
5
6
7
8
9
10
11
12
let promise = new Promise((resolve, reject) => {
resolve("传递给then内的value值。");
});

promise.then(
(value) => {
console.info(value); // 打印“传递给then内的value值。”
},
(error) => {
console.error(error);
}
);

Promise.prototype.catch(onRejected)

catch()只处理rejected的情况,该方法返回一个 Promise,实质是Promise.prototype.then(undefined, onRejected)的语法糖。

1
2
3
4
5
6
7
8
9
10
11
12
let promise = new Promise((resolve, reject) => {
resolve("传递给then内的value值。");
});

promise.then(
(value) => {
console.info(value); // 打印“传递给then内的value值。”
},
(error) => {
console.error(error);
}
);

Promise.prototype.finally(onFinally)

finally()方法返回一个 Promise,在then()catch()执行完成之后,都会调用finally指定的回调函数,这样可以避免一些重复的语句同时出现在then()catch()当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let promise = new Promise((resolve, reject) => {
resolve("传递给then内的value值。");
reject("传递给catch内error的值。"); // 此处reject并未被执行,因为Promise状态已经在上面一条语句resolve。
});

promise
.then((value) => {
console.info(value);
})
.catch((error) => {
console.error(error);
})
.finally(() => {
console.log("执行到finally方法。");
});

/* 打印结果 */
// 传递给then内的value值。
// 执行到finally方法。

包括 Eage 在内的 IE 系列浏览器目前都不支持finally()方法。

Promise 对象实例方法

Promise.resolve(value)

该方法会根据参数的不同,返回不同的 Promise 对象。当接收Promise对象作为参数的时候,返回的还是接收到的Promise对象;

1
2
3
4
5
6
7
let jqueryPromise = $.ajax("http://gc.ditu.aliyun.com/geocoding?a=成都市"); // jquery返回的promise对象

let nativePromise = Promise.resolve(jqueryPromise);

nativePromise.then((value) => {
console.info(value); // API接口的返回值
});

当接收到 thenable 类型对象(由 ES6 Promise 提出的 Thenable 是指具有.then()方法的对象)的时候,返回一个具有该 then 方法的全新 Promise 对象;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let thenablePromise = Promise.resolve(
// thenable类型对象
{
then: (onFulfill, onReject) => {
onFulfill("fulfilled!");
},
}
);

console.log(thenablePromise instanceof Promise); // 打印true

thenablePromise.then(
(value) => {
console.info(value);
},
function (error) {
console.error(error);
}
);

// true
// fulfilled!

当接收其它数据类型参数的时候(字符串或null)将会返回一个将该参数作为值的全新 Promise 对象。

1
2
3
4
5
let promise = Promise.resolve([1, 2, 3]);

promise.then((values) => {
console.info(values[0]); // 1
});

Promise.reject(reason)

返回一个被reason拒绝的 Promise,但是与resolve()不同之处在于,即使reject()接收的参数是一个 Promise 对象,该函数依然会返回一个全新的 Promise。

1
2
3
4
5
let rejectedPromise = Promise.reject(new Error("this is a error!"));

console.info(
rejectedPromise === Promise.reject(rejectedPromise) // 打印false
);

Promise.all(iterable)

iterable参数中所有 Promise 都被resolve, 或者参数并不包含 Promise 时, all()方法返一个回resolve的全新 Promise 对象。当iterable中一个 Promise 返回拒绝reject时, all()方法会立即终止并返回一个reject的全新 Promise 对象。

1
2
3
4
5
6
7
let promise1 = Promise.resolve(1),
promise2 = Promise.resolve(2),
promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3]).then((results) => {
console.info(results); // 打印[1, 2, 3]
});

Promise.race(iterable)

iterable参数数组中的任意一个 Promise 对象变为resolve或者reject状态,该函数就会立刻返回一个全新的 Promise 对象,并使用该 Promise 对象进行resolve或者reject

1
2
3
4
5
6
7
let promise1 = Promise.resolve(1),
promise2 = Promise.resolve(2),
promise3 = Promise.resolve(3);

Promise.race([promise1, promise2, promise3]).then((results) => {
console.info(results); // 打印 1
});

Promise 方法链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function taskA() {
console.log("Task A");
}
function taskB() {
console.log("Task B");
}
function onRejected(error) {
console.log("Catch Error: A or B", error);
}
function finalTask() {
console.log("Final Task");
}

let promise = Promise.resolve();

promise.then(taskA).then(taskB).catch(onRejected).then(finalTask);

如果taskA()发生异常的话,会按照taskA() → onRejected() → finalTask()这个流程进行处理,taskB()并不会被调用的。

在上面代码taskA()taskB()的处理中,如果发生异常或者返回了Rejected状态的Promise对象,就会调用onRejected()方法。

Promise 中的各个 Task 相互独立,如果taskA()想为taskB()传递参数,需要在taskA()当中return相应的值,该值将会作为taskB()的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function increment(value) {
return value + 1;
}

function doubleUp(value) {
return value * 2;
}

function output(value) {
console.log(value); // => (1 + 1) * 2
}

let promise = Promise.resolve(1);

promise
.then(increment)
.then(doubleUp)
.then(output)
.catch(function (error) {
console.error(error); // Promise Chain发生异常时调用
});

return的值不仅只局限于字符串或者数值,也可以是普通对象或者 Promise。return值经由Promise.resolve( return返回值 );包装处理,无论回调函数内部返回什么,then()方法总是返回一个新建的 Promise 对象。

then()方法内的函数是异步调用的

执行Promise.resolve(value)等方法时,如果 Promise 对象立刻进入resolve状态,则是否意味.then()里指定的函数是同步调用的?

1
2
3
4
5
6
7
8
9
10
11
12
var promise = new Promise((resolve) => {
console.log("Inner Promise"); // 1
resolve(2018);
});
promise.then((value) => {
console.log(value); // 3
});
console.log("Outer Promise"); // 2

// Inner Promise
// Outer Promise
// 2018

上面的示例代码说明:传入then()内的函数是被异步执行的。JavaScript 代码编写原则之一是尽量不要对异步回调函数进行同步调用,否则处理顺序可能会与预期不符,甚至导致栈溢出或异常处理错乱;如果需要在将来某个时刻调用异步回调函数,可以使用setTimeout()setInterval()等异步 API(绑定的函数不会立刻执行,而是延迟到队列的最后)。

为了避免同时使用同步、异步调用可能引起的混乱,Promise 规范约定只能使用异步调用方式 。

每次调用 then()都会返回新建的 Promise

Promise 之所以能够进行链式的方法调用,是由于无论then()还是catch()都会返回一个全新的 Promise 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let promise = new Promise((resolve) => {
resolve(2018);
});

let thenPromise = promise.then((value) => {
console.log(value);
});

let catchPromise = thenPromise.catch((error) => {
console.error(error);
});

console.info(promise === thenPromise); // false
console.info(thenPromise === catchPromise); // false

通过===进行严格相等比较,可以看出上述 3 个 Promise 对象是互不相同的,也就证明then()catch()都返回了不同的 Promise 对象。

下面是一个通过then()返回新创建 Promise 对象的错误使用方法:

1
2
3
4
5
6
7
8
9
10
/* 错误的返回方式 */
function incorrectAsyncCall() {
let promise = Promise.resolve();

promise.then(() => {
return value;
});

return promise;
}

上述写法存在诸多问题,首先在promise.then()中产生的异常不会被外部捕获,其次也不能得到then()的返回值。由于每次调用promise.then()都会返回一个新创建的 Promise 对象,因此需要通过Promise Chain将调用链式化,修改后的代码如下:

1
2
3
4
5
6
7
8
9
/* 正确的做法 */
function correctAsyncCall() {
let promise = Promise.resolve();

// 直接将链式调用的then()返回
return promise.then(() => {
return value;
});
}

Promise.then()类似,包括Promise.all()Promise.race()都会接收Promise对象作为参数,然后返回一个与接收参数不同的全新 Promise 对象,使用时应多加注意。

使用 reject 而不是 throw

Promise 的构造函数以及then()中执行的函数都可以认为是在try...catch块中运行,因此即便使用了throw,程序本身也不会因为抛出异常而终止。

1
2
3
4
5
6
7
let promise = new Promise((resolve, reject) => {
throw new Error("throw message");
});

promise.catch((error) => {
console.error(error); // => "throw message"
});

上面的代码运行时没有任何问题,但是如果需要把 Promise 对象状态设置为Rejected的话,相比throw关键字reject()方法会更加合理。接下来,我们方便的通过 Promise 构造函数中的reject参数,将 Promise 对象的状态设置为Rejected

1
2
3
4
5
6
7
let promise = new Promise((resolve, reject) => {
reject(new Error("reject message")); // 发生错误的时候可以向reject()传递Error对象
});

promise.catch((error) => {
console.error(error); // => "reject message"
});

rejectthrow更安全另一个原因在于,有些场景下很难届定throw是开发人员主动抛出,还因为真正的代码异常导致的。

当需要在then()中执行reject操作的时候,可以通过then()当中的回调函数return一个自定义的 Promise 对象。然后根据这个自定义 Promise 对象的状态,下一个then()中注册的的onFulfilled()onRejected()回调函数会相应进行调用,从而实现then()中不通过throw关键字也能进行reject操作。

1
2
3
4
5
6
7
8
9
10
let promise = Promise.resolve();

promise
.then(() => {
let newPromise = new Promise((resolve, reject) => {
// resolve或者reject的状态决定了下一个then()当中的onFulfilled或onRejected哪个会被调用
});
return newPromise;
})
.then(onFulfilled, onRejected);

例如下面代码中,newPromise对象状态为Rejected的时候,后续catch()中的onRejected方法会被调用。

1
2
3
4
5
6
7
8
9
10
11
12
let onRejected = console.error.bind(console);

let promise = Promise.resolve();

promise
.then(() => {
let newPromise = new Promise((resolve, reject) => {
reject(new Error("Promise is rejected !"));
});
return newPromise;
})
.catch(onRejected);

接下来,再通过Promise.reject()来简化代码:

1
2
3
4
5
6
7
8
9
let onRejected = console.error.bind(console);

let promise = Promise.resolve();

promise
.then(() => {
return Promise.reject(new Error("Promise is rejected !"));
})
.catch(onRejected);

Promise 的缺点在于一旦新建就会立即执行,并且无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

Generator 函数

Generator 函数是 ES6 提供的一种异步编程解决方案,Generator 函数是一个封装了多个内部状态的状态机,执行 Generator 函数会返回一个遍历器对象,可以通过该对象遍历 Generator 函数内部的每个状态。Generator 函数与普通函数的区别在于:声明时在function关键字与函数名称之间添加*号,函数体内部使用yield[jild] n.产出,收益)表达式定义不同的内部状态。

下面代码定义了一个 Generator 函数myGenerator(),其内部拥有HiGenerator两个yield表达式,该函数共拥有HiGenerator!三个状态。与普通函数不同的是,Generator 函数被调用后并不立刻执行,返回的也并非函数执行结果,而是一个指向内部状态的遍历器对象(Iterator Object)。通过调用遍历器对象的next()方法,可以将执行流程移动至下一状态(即接下来的yield表达式或return语句)。换而言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next()方法可以恢复执行

1
2
3
4
5
6
7
8
9
10
11
12
function* myGenerator() {
yield "Hi";
yield "Generator";
return "!";
}

let generator = myGenerator();

generator.next(); //{ value: 'Hi', done: false }
generator.next(); //{ value: 'Generator', done: false }
generator.next(); //{ value: '!', done: true }
generator.next(); //{ value: undefined, done: true }

每次调用遍历器对象的next()就会返回一个拥有valuedone属性的对象,其中value属性表示当前内部状态的值(yield后面表达式的值);done属性是一个布尔值(用来表示遍历是否结束)。只有调用next()方法之后,对应yield关键字后的表达式才会被执行(即惰性求值 Lazy Evaluation)。

ES6 规范没有指定function关键字与函数名称之间星号*出现的位置,所以下面的写法都是等效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* demo(x, y) {
// 星号靠左
}

function* demo(x, y) {
// 星号靠右
}

function* demo(x, y) {
// 星号居中
}

function* demo(x, y) {
// 直接连接function关键字与函数名称
}

Generator 函数与 Iterator

Generator 函数返回的是遍历器对象,因此将其赋值给一个对象的Symbol.iterator属性,使该对象具备 Iterator 接口,从而能够通过...运算符进行遍历。

1
2
3
4
5
6
7
8
9
10
11
12
let myIterator = {};

myIterator[Symbol.iterator] = function* () {
yield "A";
yield "B";
yield "C";
yield 1;
yield 2;
yield 3;
};

[...myIterator]; // ["A", "B", "C", 1, 2, 3]

Generator 函数执行后返回的遍历器对象本身就具有Symbol.iterator属性,执行后将会返回自身。

1
2
3
4
5
6
7
8
9
function* generator() {
// ...
}

/* 生成遍历器对象 */
let generated = generator();

/* 遍历器对象的Symbol.iterator属性调用后将会返回自身 */
generated[Symbol.iterator]() === generated; // true