基于 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),目前已经被 Firefox 和 Chromium 以及 NodeJS 进行了较为完整的实现,所以本文将会基于此介绍 JavaScript 的重点语法特性,以及一些较为用常用的核心工具函数,本文在写作过程当中参考了 Mozilla 开发者社区的 《JavaScript Guide》 和 《JavaScript Reference》 两篇官方文档。
基础语法
语句
JavaScript 使用 Unicode
字符集,并且对于大小写敏感。源文件当中的每一条 JavaScript
语句都要使用分号 ;
进行分隔,虽然这并非必需,但是可以大大降低代码发生错误的可能。
1 | var name = "www.uinio.com"; |
注意:Javascript 源代码会被解析引擎从左至右进行扫描执行。
注释
JavaScript 的注释与许多 C/C++ 类语言相似,同样可以划分为单行与多行两种类型:
1 | // 单行注释 |
变量
变量的名称叫做标识符,虽然可以使用大部分的
ISO 8859-1 或者 Unicode
编码字符作为标识符,其必须以字母、下划线 _
、美元符号
$
作为前缀,后续可以是数字 0 ~ 9
或者大写字母
A ~ Z
以及小写字母 a ~ z
,例如:
1 | Hank _uinika $Chengdu from2021 to_2022 |
声明
ECMAScript 6 当中一共拥有
var
、let
、const
三种变量声明关键字:
var
:声明一个变量;let
:声明具有块作用域的局部变量;const
:声明具有块作用域的只读常量;
JavaScript 源代码当中可以采用如下三种方式来声明一个变量:
- 采用
var
关键字:用于声明局部变量和全局变量,例如var x = 2022
; - 采用
let
关键字:用于声明具有块作用域的局部变量,例如let y = 13
; - 直接赋值:函数外使用会产生一个全局变量,严格模式下这种方式会发生错误,所以应该避免使用,例如
x = 2021
;
求值
采用关键字 var
或者 let
声明的变量,如果没有赋予初始值,则其值将会默认为
undefined
。而访问未经过声明的变量,将会导致解析引擎抛出一个
ReferenceError
引用错误异常:
1 | var a; |
因此,可以使用 undefined
来判断一个变量是否已经赋值,下面代码当中,由于变量 input
未被赋值,所以 if
语句的求值结果为 true
:
1 | var input; |
类似的,由于下面代码当中数组 myArray
里的元素未被赋值,所以 myFunction()
函数将会得到执行:
1 | var myArray = []; |
在数值类型的计算当中,undefined
值会被自动转换为非数值 NaN
:
1 | var number; |
同样在数值计算当中,当对一个空值变量 null
进行求值时,其值会被自动转换为
0
;而如果是在布尔类型运算当中,null
值则会被作为 false
进行处理:
1 | var test = null; |
作用域
全局变量是在函数之外声明的变量,可以在源文件当中的其它位置进行访问;而局部变量是在函数内部声明的变量,其只能在当前函数的内部进行访问。
ECMAScript 6 之前的 JavaScript
不存在语句块
作用域的概念,语句块当中声明的变量将会成为语句块所在函数或者全局作用域的局部变量,例如下面代码会在控制台输出
2022
,因为变量 code
的作用域处于全局范围,而并非仅限于 if
语句块。
1 | if (true) { |
如果使用 ECMAScript 6 当中的 let
关键字进行声明,则上述行为将会发生变化:
1 | if (true) { |
变量提升
JavaScript
变量支持先使用再声明,而且不会引发任何异常,称为变量提升。但是提升之后的变量,如果没有及时的进行初始化,则依然会返回
undefined
值:
1 | /* 示例 1 */ |
注意:由于 JavaScript 变量提升特性的存在,所有
var
变量的声明语句应当尽量放置该函数顶部,从而极大的提升代码清晰度。
ECMAScript 6 提供的 let
和 const
关键字声明的变量,同样会存在变量提升的情况,但是并不会被赋予任何初始值,如果在变量声明之前引用(此时该变量处于暂时性死区,该状态会持续到该变量被声明为止),则会抛出
ReferenceError
引用错误异常。
1 | console.log(week); // Uncaught ReferenceError: week is not defined |
函数提升
JavaScript 当中的函数,只有函数声明会被提升至源文件顶部,而函数表达式并不会被提升。
1 | /* 函数声明 */ |
全局变量
全局变量本质上是全局对象的属性,Web
浏览器当中的全局对象是 window
,可以通过
window.variable
设置和访问全局变量:
1 | var URL = "www.uinio.com"; |
而 NodeJS 当中的全局对象是
global
,则需要使用 global.variable
来设置和访问全局变量。
1 | URL = "www.uinika.com"; |
常量
使用关键字 const
可以创建一个只读的常量,其命名规则与变量相同,必须以字母、下划线
_
、美元符号 $
开头,并且可以包含有字母、数字、下划线。
1 | const PI = 3.141592654; |
常量即不可以重新赋值,也不可以在代码运行时重新声明,所以声明常量的时候,必须显式的将其初始化为某个具体值。常量的作用域规则与
let
块级作用域变量相同,在同一个作用域当中,不能使用与变量名或者函数名相同的名字来命名常量:
1 | function WEEK() {} |
然而,对于常量对象的属性进行赋值并不会受到保护,所以下面语句在执行时不会产生错误:
1 | const TEST = { key: "value1" }; |
同样的,对于常量数组的元素进行赋值也不会受到保护,所以下面语句在执行时同样不会产生错误:
1 | const WEB = ["HTML", "CSS"]; |
数据类型
最新的 ECMAScript 标准当中一共定义了 8 种数据类型,其中 7 种基本数据类型,以及 1 种对象数据类型:
数据类型 | 名称 | 描述 |
---|---|---|
Boolean |
布尔值 | 拥有 true 和 false 两个值; |
null |
空值 | 表示空值,由于 JavaScript 大小写敏感,所以
null 、Null 、NULL
三者完全不同。 |
undefined |
未定义 | 表示变量未赋值时的属性; |
Number |
数值 | 用于存放整数或者浮点数; |
BigInt |
大整数 | 用于安全的存储和操作大整数; |
String |
字符串 | 表示一串文本值的字符序列; |
Symbol |
独一无二的值 | ECMAScript 6 新增加的数据类型,是一种实例唯一并且不可改变的数据类型; |
Object |
对象 | 用于充当各种数据类型的命名容器; |
数据类型转换
JavaScript 是一种动态类型语言,这意味着在声明变量时无需指定数据类型,变量的数据类型会在代码执行期间,根据需要自动进行转换:
1 | var temporary = 2022; |
如果表达式当中同时包含数字和字符串,通过加法运算符
+
可以将数值转换成字符串:
1 | let x = "The Number is " + 2021; // 'The Number is 2021' |
但是 JavaScript 当中并不能使用减法运算符 -
完成相同的任务:
1 | "36" - 16; // 20 |
字符串转换为数值
通过 parseInt(string, radix)
函数可以将字符串转换为整数(会丢弃小数部分),其中第 2
个参数 radix
表示进制或者基数,其取值范围介于
2 ~ 36
之间:
1 | parseInt("32", 16); // 50 |
而通过 parseFloat(string)
函数则可以将字符串转换为浮点数:
1 | parseFloat(3.141592654); // 3.141592654 |
注意:如果第 1 个参数
string
并非表达一个有效数值,那么上述函数就会返回一个非数NaN
。
将字符串转换为数字的另外一种方法,就是采用简单粗暴的加法运算符
+
:
1 | "1.1" + "5.5" = "1.15.5" |
各种字面量
字面量(Literals)是由特定语法表达式所定义的一组常量。
数组
数组是一个在方括号 []
当中包含有零个或者多个表达式的列表,其中每个表达式代表数组的一个元素,元素的个数就是该数组的长度。下面示例声明并且初始化了一个拥有
3 个元素的数组 motors
,同时采用 length
关键字获取数组长度,并通过索引 Array[index]
访问数组里的元素。
1 | var motors = ["Geely", "Haval", "Chery"]; |
注意:数组字面量同时也是一个
Array
对象。
初始化数组时如果连续输入两个逗号
,
,则数组当中就会产生一个初始值为 undefined
的元素。
1 | var animal = ["Lion", , "dog", "cat"]; |
如果在元素列表的尾部添加了一个逗号,则这个都好将会被忽略,数组依然保持原有的长度。
1 | var animal = ["Hank", "uinika"]; |
注意:尾部逗号在早期的 Web 浏览器当中会产生错误,所以最佳实践是移除它们。而在必要的时候,可以显式的将元素声明为
undefined
。
布尔值
布尔类型只有 true
和 false
两种字面量,下面示例通过 typeof
运算符获取变量的数据类型:
1 | let yes = true; |
注意:
Boolean
只是布尔类型的包装对象,两者不能混淆使用;
整数
整数可以采用十进制(基数为
10
)、十六进制(基数为
16
)、八进制(基数为
8
)、二进制(基数为
2
)进行表示:
- 十进制整数:由一串数值序列组成,并且没有前缀
0
,例如0
、117、
-345`; - 八进制的整数:前缀为
0
、0O
、0o
(严格模式下,必须以0o
或0O
开头),只能包含数字0 ~ 7
,例如015
、0001
、-0o77
; - 十六进制整数:以
0x
、0X
开头,可以包含数字0 ~ 9
和字母a ~ f
或者A ~ F
,例如0x1123
、0x00111
、0xF1A7
; - 二进制整数:以
0b
、0B
开头,只能包含数字0
和1
,例如0b11
、0b0011
、-0b11
;
1 | 0, 117 -345 十进制, 基数为10) |
浮点数
浮点数的字面量可以由带正负号 +/-
前缀的十进制整数部分、小数点
.
、十进制小数部分、指数部分组成。其中指数部分以
e
或者 E
作为前缀,后面紧接着一个带有正负号
+/-
的整数。
1 | -0.12345789; // -0.12345789 |
对象
对象是一种封闭在花括号 {}
当中,具有多个键值对元素的列表。下面示例创建了一个 auto
对象,将第 1 个元素 motor1
定义为变量 motor1
的值;第二个元素,属性 getCar,引用了一个函数(即
CarTypes("Honda"));第三个元素,属性 special,使用了一个已有的变量(即
Sales)。
1 | var motor1 = "BYD"; |
可以采用数值作为对象的属性名称,引用该属性的时候,则需要采用类似于数组索引
object.[property]
的访问方式:
1 | let motor = { favorites: { 1: "BYD", 2: "AION" }, 3: "Tesla" }; |
同理,如果需要采用其它不合法的属性名称,则同样可以通过数组索引的方式进行访问:
1 | let motor = { |
ECMAScript 6 支持创建对象时通过 __proto__
设置原型、简化
key: value
属性赋值、直接在对象中定义函数、支持调用父对象方法、动态计算表达式属性名称。
1 | let property = "this is a property"; |
RegExp
正则表达式字面量是一个被两条正斜线
/ ... /
包围起来的表达式。
1 | const RegExp = /ab+c/; |
字符串
字符串是由双引号 "
或者单引号 '
括起来的零个或者多个字符:
1 | "2022"; |
JavaScript 会自动将字符串字面量转换为一个 String
对象进行处理,所以可以直接调用 String.length
属性:
1 | console.log("Hank's motor".length); // 12 |
ECMAScript 6 提供一种模板字面量(Template Literals)功能,用于通过一些语法糖来构造字符串:
1 | let name = "Hank"; |
转义字符
下面的表格列举了可以在 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 | "Hank's blog is 'www.uinio.com'"; // "Hank's blog is 'www.uinio.com'" |
运算符
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 返回
true ;2022 === "2022" 返回
false ; |
左右两侧操作数相等并且数据类型相同时返回
true ; |
不严格等于 | !== |
2022 !== 2022 返回
false ;2022 !== "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 返回
true ,false && false 返回
false ,true && false 返回
false ; |
当操作数都为 true 时返回
true ,否则返回 false ; |
逻辑或 | ││ |
true ││ true 返回
true ,false ││ false 返回
false ,true ││ false 返回
true ; |
任意一个操作数为 true 时返回
true ,如果操作数都为 false 则返回
false ; |
逻辑非 | ! |
!true 返回 false
,!false 返回 true ; |
如果操作数为 true 时返回
false ,反之则会返回 true ; |
条件运算符
条件运算符是 JavaScript 当中唯一的三目运算符(需要 3 个操作数),其返回结果是根据指定条件在两个值当中选取一个。
1 | condition ? value1 : value2; |
如果条件 condition
的判断结果为
true
,则返回 value1
,否则返回
value2
。
1 | const getStage = (age) => { |
分组操作符
分组操作符 ()
可以用于控制了表达式当中计算的优先级。
1 | let x = 1; |
typeof 操作符
typeof
操作符用于返回当前操作数的数据类型,可以通过如下方式进行调用:
1 | typeof operand; |
上面的 typeof
操作符将会返回一个表示操作数
operand
数据类型的字符串。
1 | var year = new Function("2022 - 2010"); |
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 | /* 使用 + 拼接字符串 */ |
剩余参数
ES6 提供的剩余参数运算符 ...
可以用于快速原地展开一个数组或者一组函数参数。
1 | /* 展开函数参数 */ |
解构赋值
ES6 提供的解构赋值语法可以用于从数组当中提取元素,或者从对象当中提取属性。
1 | /* 提取数组当中的元素 */ |
流程控制
JavaScript 提供了一套灵活的流程控制语句,可以在应用程序当中实现大量的交互性功能。
语句块
语句块用于组合多条 JavaScript 语句,由一对大括号
{}
进行界定:
1 | { |
语句块通常与 if
、for
、while
等流程控制语句结合起来使用:
1 | let week = 1; |
ECMAScript 6 标准之前,Javascript 并不存在块作用域的概念,变量的作用域从声明位置开始,一直会覆盖到源文件的末尾:
1 | var day = 27; |
而使用 ECMAScript 6 的 let
和 const
关键字声明的变量,其作用域仅限于该声明所在的语句块:
1 | let year = 2022; |
条件判断
JavaScript 支持 if...else
和 switch
两种条件判断语句。
if...else 语句
条件表达式 condition
返回 true
或者
false
,如果返回的是 true
,就会执行
statement_1
语句;如果返回的是 false
,则会执行
statement_2
语句:
1 | if (condition) { |
通过组合使用 else if
语句,可以连续测试多种
condition_x
判断条件:
1 | if (condition_1) { |
通常不建议在条件表达式 condition
当中使用赋值语句,因为阅读代码时容易将其视为等值比较运算符
==
。如果一定要在条件表达式当中使用赋值语句,通常需要在赋值语句前后额外添加一对括号
(condition)
:
1 | let x; |
空字符串
""
、undefined
、null
、0
、NaN
在条件表达式 condition
当中,都将会被视为
false
条件:
1 | if ("" && undefined && null && 0 && NaN) { |
切记不要混淆原始布尔值 true
和 false
与
Boolean
对象的比较关系:
1 | const test = new Boolean(false); |
switch 语句
switch
语句首先会查找与 expression
匹配的
case
子句,然后执行该子句当中对应的代码;如果没有匹配值,则会执行
default
子句里的代码,通常会将 default
子句放置在 switch
语句的最后。
1 | switch (expression) { |
可选的 break
语句放置在 case
子句当中,以确保匹配语句执行之后,执行流程可以跳出 switch
语句执行后续内容。如果忽略掉 break
语句,那么就会继续执行下一条 case
子句当中的内容:
1 | const day = new Date().getDay(); |
循环
JavaScript 提供了
for
、do...while
、while
、label
、break
、continue
、for...in
、for...of
一共 8 种循环相关的语法。
for 循环
for
循环会一直重复执行,直至循环条件变为
false
。
1 | for ([initialExpression]; [condition]; [incrementExpression]) { |
- 首先,执行初始化表达式
initialExpression
,通常该表达式会初始化一个或多个循环计数器; - 然后,计算
condition
表达式的值,如果condition
为true
,那么执行循环当中的语句;如果condition
为false
,那么循环将会终止;如果省略condition
表达式,则condition
的值默认为true
; - 接着,执行循环里的
statement
语句,多条语句可以使用代码块{}
进行包裹; - 如果
incrementExpression
表达式存在更新,则会执行更新表达式,并且重新执行第 2 个步骤;
1 | for (var index = 0; index < 6; index++) { |
do...while 循环
do...while
循环同样会一直重复直执行,直至指定的条件为
false
。
1 | do { |
statement
语句会在检查 condition
条件之前会执行一次,多条 statement
语句需要放置到花括号
{}
当中。如果 condition
为
true
,那么 statement
语句将会再一次执行;而如果 condition
为
false
,则循环终止运行。
1 | let index = 0; |
while 循环
while
循环只要指定的条件 condition
为
true
,就会一直执行 statement
语句块。如果条件
condition
为 false
,则会终止循环。
1 | while (condition) { |
与 do...while
循环不同的是,while
循环的
condition
条件检测会在每次 statement
执行之前发生。
1 | let index = 0; |
label 语句
label
用于在程序当中提供一个代码标识符,然后结合
break
或 continue
语句精确的返回到代码指定的位置。
1 | label: |
这里的 label
可以是任意合法的 JavaScript 标识符,而
statement
则可以是任意需要标识的 JavaScript 语句。
1 | /* label 与 break 结合使用 */ |
break 语句
break
语句用于终止全部循环,当
break
没有指定 label
时,就会立刻终止当前所在的
while
,do...while
,for
循环语句,以及 switch
流程控制语句,并且执行这些语句之后的内容;而当使用带有
label
的 break
时,则会在终止全部循环之后,跳转至 label
所在的位置继续执行。
1 | break [label]; |
1 | for (let index = 0; index < 5; index++) { |
continue 语句
continue
语句用于终止单次循环,当
continue
没有指定 label
时,就会立刻终止本次的
while
,do...while
,for
循环,以及
switch
流程控制语句,并且继续执行下一次循环;而当使用带有
label
的 continue
时,则只会在中断本次循环之后,跳转至 label
所处位置继续执行。
1 | continue [label]; |
1 | for (let index = 0; index < 5; index++) { |
for...in 循环
for...in
循环通常用于遍历某个对象的可枚举属性:
1 | for (property in object) { |
当在 for...in
循环当中访问对象属性时,可以采用
object[property]
的语法形式:
1 | const URL = { blog: "www.uinika.com", github: "uinika.github.io", gitee: "uinika.gitee.io" }; |
除此之外,for...in
还可以用于遍历数组元素,但是此时返回的并非对象名称,而是数组索引:
1 | const URLs = ["www.uinika.com", "uinika.github.io", "uinika.gitee.io"]; |
注意:
for...in
是为了遍历对象属性而设计,由于会遍历出所有可枚举的属性,所以不建议使用它来遍历数组。
for...of 循环
for...of
用于遍历数组时,返回的结果是每一个数组元素。
1 | const URLs = ["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 | const URLs = ["www.uinika.com", "uinika.github.io", "uinika.gitee.io"]; |
数组
数组用于表示一个有序的数据集合,可以通过数组的名称和索引访问其中的元素。
创建数组
JavaScript
没有内置明确的数组数据类型,但是可以通过数组字面量语法,以及内置的
Array
对象,显式的创建拥有指定元素的数组:
1 | const array = [element0, element1, ..., elementN]; // 数组字面量方式 |
除此之外,还可以通过如下方式创建一个指定长度的空数组:
1 | const array = []; |
初始化数组
JavaScript 支持通过直接为元素赋值的方式来初始化数组:
1 | const URLs = []; |
也可以像本节内容开头所描述的那样,在创建数组的时候直接进行初始化:
1 | const fruits = ["苹果", "西柚", "橙子"]; |
引用数组元素
通过数组元素的索引,可以从 0
开始引用数组的元素:
1 | const trees = new Array("松树", "柳树", "樟树"); |
数组长度
通过数组对象上提供的 length
属性,可以获取当前数组的实际长度信息,也就是数组元素的实际个数:
1 | const fruits = ["苹果", "西柚", "橙子"]; |
通过手动设置 length
属性,还可以对数组的长度进行调整:
1 | /* 当 length 属性大于实际数组长度时,采用 undefined 填充多余元素 */ |
遍历数组
使用 for
循环遍历数组元素是一种比较常规的操作:
1 | const colors = ["红色", "绿色", "蓝色"]; |
使用 forEach()
则是另外一种遍历数组元素的方法,传递给
forEach()
的函数会在遍历数组的每个元素时执行一次,数组元素将会作为参数传递给该函数。
1 | const colors = ["红色", "绿色", "蓝色"]; |
注意:
forEach()
不会遍历出定义数组时未经初始化的元素,但是会遍历出被手动赋值为undefined
的元素:
原则上 for...in
语句是为遍历对象设计的,使用其迭代数组时,会枚举出所有的可枚举属性,生产环境下不建议使用。例如下面示例代码,就将定义在
Array
对象原型上的 blog
遍历打印了出来:
1 | Array.prototype.blog = function () { |
函数
函数是 JavaScript 的基本语法组件,由一系列用于执行计算任务的语句构成,而且与变量一样,只能在其有效的作用域当中进行调用。
函数定义
JavaScript 函数采用 function
关键字进行定义,主要由函数名称、参数列表(包围在圆括号
()
当中并且由逗号 ,
进行分隔)、函数体(采用大括号 {}
包围,并且使用 return
关键字返回结果)三个部分组成。
1 | function square(size) { |
- 值传递:基本数据类型的参数值会直接传递给函数,如果在函数当中修改了该参数的值,这种影响并不会传递到函数外部的作用域;
- 引用传递:对象类型的参数值会将引用传递给函数,如果在函数当中修改了对象的属性,这种影响就会传递到函数外部的作用域;
1 | let number = 2022; |
函数表达式
通过函数表达式方式创建的函数不需要直接指定函数名称,例如前面的
square()
函数也可以采用如下方式进行声明:
1 | const square = function (size) { |
采用函数表达式,可以非常方便的将函数作为参数传递给另外一个函数:
1 | function battleax() { |
函数表达式的另外一个用途,则是可以根据判断条件来声明定义一个函数:
1 | let happy, depression; |
注意:如果调用不符合判断条件的函数,JavaScript 解析引擎将会提示
ReferenceError
错误。
Function 对象
每一个 JavaScript 函数本质上都是一个 Function
对象,通过
Function()
构造函数就可以动态创建一个函数对象:
1 | new Function (参数1, 参数2, ..., 函数体) |
参数是由 JavaScript
有效标识符组成的字符串,当具有多个参数时可以使用逗号 ,
分隔。而函数体则是一个包含有函数定义语句的 JavaScript
字符串。
1 | const sum = new Function("a", "b", "return a + b"); |
通过 Function()
创建函数会存在与 eval()
类似的安全问题和相对较小的性能问题,并且 Function
创建的函数只能在全局作用域当中运行,只能访问到全局变量和自身的局部变量,并不能访问
Function()
构造函数所在作用域当中的变量。
1 | let local = 1985; |
虽然上述代码可以在 Web 浏览器当中正常执行,但是在 NodeJS 当中函数
year1()
将会产生
ReferenceError: local is not defined
错误,这是由于 NodeJS
里 .js
源文件的顶级作用域并非全局作用域,函数
year1()
当中的变量 local
实质位于当前模块的作用域之中。
注意:如果当前 JavaScript 运行环境是 Web 浏览器,执行以
Function()
方式构造的函数,需要修改 Web 浏览器默认的 HTTP 内容安全策略"content_security_policy"
为"unsafe-eval"
。
调用函数
声明并且定义函数之后,就可以通过 函数名称()
的方式调用函数。这里要特别注意函数定义一定要位于调用语句所处的作用域,而函数声明则可以被提升,因而可以出现在调用语句之后。
1 | console.log(square(50)); // 2500 |
函数表达式无法进行提升,所以将上面代码修改为如下格式是错误的:
1 | console.log(square(50)); // Uncaught SyntaxError: Identifier 'square' has already been declared |
函数可以在自身的函数体当中进行递归调用,下面的示例当中通过函数的递归调用来计算阶乘:
1 | function factorial(number) { |
函数的作用域
函数当中定义的局部变量只能在该函数体内部进行访问,除此之外,还可以访问到全局变量或者定义在父函数当中的局部变量。
1 | /* 全局变量 */ |
递归函数调用
通过函数名称、arguments.callee
、指向该函数的变量名称,就可以在一个函数的内部调用自身,也就是所谓的递归。例如下面函数里,bar()
、arguments.callee()
、foo()
三条语句都可以调用函数自身:
1 | const China = function chinese() { |
使用递归函数的时候,需要注意加入合适的循环终止条件,避免进入到死循环,锁死当前 JavaScript 解析引擎的执行线程。
1 | /* 采用 while 循环打印 0 ~ 3 范围的整数 */ |
当需要获取一个树结构当中的所有节点时,使用递归函数会比循环语句更为方便:
1 | function traversalTree(node) { |
除此之外,通过灵活的应用递归函数,还可以在函数当中模拟出堆栈数据结构的行为:
1 | function traversal(index) { |
嵌套函数
父函数当中可以嵌套一个子函数,此时子函数可以在父函数当中进行调用,子函数内部将会形成一个可以访问父函数参数与变量的闭包,而父函数并不能使用子函数当中的参数与变量。
1 | function totalSquares(size1, size2) { |
这是由于闭包可以保存其所见作用域当中的所有参数与变量,所以下面示例当中的子函数
child
被返回时,parent
的参数值 x
也会被一同保留下来:
1 | /* 父函数 */ |
JavaScript 函数支持多层嵌套,例如函数 A
可以包含函数
B
,而函数 B
可以再包含函数
C
。此时函数 B
和 C
都形成了闭包,它们都同时包含着多个作用域(B
可以访问 A
,C
可以访问 B
和
A
),这种递归式包含的作用域称为作用域链,具体请参考下面的示例:
1 | function A(x) { |
当相同闭包作用域链当中的参数或者变量出现命名冲突时,距离调用位置更近的参数或者变量会拥有更高的优先级:
1 | function parent() { |
闭包 Closure
正如前面内容所提到的,JavaScript 允许函数嵌套,并且子函数可以访问父函数以及父函数所能访问作用域里的全部变量与函数,而父函数却不能访问定义在子函数当中的变量与函数。由于子函数可以访问到父函数的作用域,因此当子函数生命周期大于父函数的时候,父函数所定义变量与函数的生存周期将会比子函数更为持久。当子函数在父函数作用域当中被访问时,就产生了一个闭包。
1 | /* 变量 name */ |
实际的情况可能会更加复杂,下面代码返回了一个对象,这些对象上自定义的
getter
与 setter
函数可以用于操作父函数
blogger
当中的内部变量:
1 | const blogger = function (name) { |
上述代码当中,子函数都可以获得 blogger()
父函数的
name
参数。除此之外,没有其它办法可以获得父函数内部定义的参数或者变量,从而为其提供了一个安全稳定的运行环境。例如下面代码当中,不能直接通过名称访问变量
code
的值,而只能通过 getCode()
函数获取变量
code
的值:
1 | const getCode = (function () { |
尽管有着充当局部变量保险箱的优点,但是在使用闭包特性仍然需要避免一些陷阱,例如子函数闭包当中定义了一个与父函数相同名称的变量,那么在该闭包作用域当中,将会无法引用父函数当中的这个同名变量。例如下面代码当中,调用
getCode()
函数时同时传递了
610000
(父函数参数)和
610095
(子函数参数)两个参数,而最终返回的
code
值为 610095
:
1 | const getCode = function (code) { |
arguments 对象
函数的实际参数会被保存在一个 arguments
对象当中,这个
arguments
是一个类数组对象,同样可以采用索引
arguments[index]
访问具体的参数,也可以使用
arguments.length
属性获得参数的具体个数(无参数时为
0
)。
1 | function blog(URLs) { |
注意:
arguments
对象常用于不确定函数会接收多少个参数的场景。
默认参数
JavaScript 函数参数的默认值是 undefined
,通过 ECMAScript
6 提供的默认参数特性,可以为函数指定默认的参数值:
1 | function profile(name, URL = "www.uinio.com") { |
剩余参数
剩余参数同样也是 ECMAScript 6 提供的新特性,用于将不确定具体数量的参数表示为一个数组:
1 | function profile(name, ...URLs) { |
箭头函数
ECMAScript 6 提供的箭头函数相比于传统的 JavaScript 函数,具有更为简短的书写语法:
1 | const printName = () => { |
采用关键字 new
实例化一个普通 JavaScript
函数时,总是会将 this
指针指向全局对象 window
或者 global
。而箭头函数则会从词法上自动绑定
this
指针至当前对象:
1 | const Author = function () { |
如果需要让传统 JavaScript 函数当中的 this
指针指向当前对象,则可以将其赋值给一个命名为 that
或者
self
的局部变量:
1 | const Demo = function () { |
预定义函数
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 | const objectName = { |
下面代码采用字面量方式定义了一个 Person
对象,该对象拥有一个 name
参数和一个
printName()
方法:
1 | const Person = { |
注意:在使用字面量创建的对象当中定义箭头函数时,箭头函数当中的
this
指针将会指向全局对象window
。
1 | const Person = { |
构造函数 function
采用 function
关键字同样可以创建一个对象,但是与字面量语法不同的是,在使用之前必须采用关键字
new
手动进行实例化。
1 | function objectName(parameter) { |
下面代码采用 function
构造函数方式定义了一个
Person
对象,该对象拥有一个 name
参数和一个
printName()
方法:
1 | function Person(name) { |
在采用 function
构造函数创建的对象当中定义箭头函数时,箭头函数内的 this
指针依然会指向当前对象 Person
:
1 | function Person(name) { |
Object.create() 方法
基于现有对象的 __proto__
创建一个新的对象,而不需要使用到字面量语法 {}
,或者构造函数
function
。
1 | Object.create(proto,[propertiesObject]) |
上面的 proto
表示新创建对象的原型(可缺省),而
propertiesObject
表示提供属性的对象,该方法执行后返回一个带有指定原型和属性的新对象。
1 | const Person = { |
注意:
prototype
是函数才会拥有的属性,而proto
是每个对象都会拥有的属性。
创建对象实例 new
new
操作符用于创建一个自定义对象的实例。
1 | const objectName = new objectType([parameter1, parameter2, ..., parameterN]); |
上面的 objectName
是对象名称,而 objectType
是对象参数
1 | function Author(name, URL) { |
指向对象自身 this
this
关键字指向当前对象本身,可以用于引用当前对象上定义的方法
或者属性
。
1 | /* 字面量对象的 this 指针 */ |
访问父对象 super
super
关键字可以用于引用当前对象的父对象构造函数,或者用于访问父对象上定义的方法
和属性
。
1 | super([arguments]); // 调用父对象的构造函数 |
上面的 arguments
是传递给父对象的构造函数或者普通方法的参数,而
parentFunction
指代的是父对象上定义的方法。
1 | /* 父对象 */ |
删除属性 delete
delete
操作符用于删除指定的对象属性(非继承)或者数组元素。如果
delete
操作成功,将会返回 true
,否则返回
false
。
1 | delete arrayName[index]; // 删除数组元素 |
上面的 objectName
表示对象名称,arrayName
则是指数组名称,而 property
是一个对象上的属性,index
则是数组当中的元素索引。
1 | const Author = { |
删除数组中的元素时,数组的长度不会发生改变,只是被删除元素所在索引会指向
undefine
值。
1 | const trees = new Array("橡树", "松树", "柳树", "杉树", "樟树"); |
判断属性存在 in
in
操作符用于判断对象的属性名称或者数组的元素索引是否真实存在,如果存在就会返回
true
,否则返回 false
:
1 | index in arrayName; // 判断数组的元素索引是否存在 |
上面的 arrayName
是数组名称,而 index
是数组索引。objectName
是对象名称,而 property
表示对象属性。
1 | /* 采用 in 操作符判断对象的属性名称是否存在 */ |
判断实例类型 instanceof
用于判断当前的实例是否为指定对象的实例,如果是就返回
true
,否则返回 false
:
1 | objectName instanceof objectType; |
上面的 objectName
是需要进行判断的实例,而
objectType
则是对象的类型。例如, 下面的代码使用 instanceof
去判断 theDay 是否是一个 Date 对象. 因为 theDay 是一个 Date 对象, 所以
if 中的代码会执行.
1 | const date = new Date(2022, 1, 5); |
getters & setters
JavaScript 对象的 setter
和 getter
是一种用于设置或者获取属性值的函数方法。其中,getter
方法没有参数,但是有返回值;而 setter
方法只会接受一个参数,但是没有返回值。
通过在使用字面量语法 {}
定义对象时,添加
get property()
(把对象属性绑定到获取该属性时将会被调用的函数)和
set property(value)
(把对象属性绑定到设置属性时将会被调用的函数)就可以完成
setter/getter
方法的添加:
1 | const Author = { |
禁止在某个属性的 setter
方法中对该属性赋值,这样会导致递归调用的发生,从而引发
RangeError
错误:
1 | const Author = { |
对于已经定义完毕的对象,可以采用 Object.defineProperty()
方法添加 setter/getter
方法:
1 | const Author = { |
注意:
Object.defineProperty(obj, prop, descriptor)
方法可以用于在对象上定义新的属性,或者修改对象上的已有属性。其中obj
是需要定义属性的对象,而prop
是要定义或者修改的属性名称,descriptor
则是需要定义或者修改的属性描述符,该方法的返回值为处理之后的新对象。
遍历对象属性
JavaScript 提供了
for...in
、Object.keys(obj)
、Object.getOwnPropertyNames(obj)
三种方法来遍历一个对象上的所有属性。
for...in
会依次访问指定对象及其原型链上面所有可以枚举的属性,也包括继承的可枚举属性。
1 | const Author = { |
Object.keys(obj)
会返回参数对象 obj
包含(不包括原型链)的所有可枚举属性的名称的数组,数组当中属性名称的排列顺序和正常循环遍历该对象的顺序保持一致。
1 | const Author = { |
Object.getOwnPropertyNames(obj)
会返回参数对象
obj
(不包括原型链)上全部属性(无论该属性是否可枚举)名称所构成的数组。
1 | const Author = { |
原型继承
JavaScript 是一种基于原型(Prototype)而非基于类(Class)的面向对象语言。
- 对于 Java 和 C++
等基于类的面向对象语言,会应用关键字
class
声明一个类,然后通过构造函数(用于在创建类时初始化属性值),使用new
关键字创建该类的实例(Instance); - JavaScript 的面向对象是基于原型的,只存在对象而没有类的概念,原型可以被视为一个创建对象的模板,任何对象都可以作为另外一个对象的原型,从而允许后者共享前者的属性;
注意:传统 ES5 当中,可以通过对象字面量
{}
、构造函数function
、Object.create()
三种方式来创建对象;而 ES6 当中新引入的class
类定义语法,其实质是现有原型继承方式的语法糖。
接下来的内容当中,将会基于如下的对象继承结构,建立
Employee
、Manager
、WorkerBee
、Engineer
、SalesPerson
共 5 个对象:
Employee
拥有默认值为空字符串的name
属性和默认值为"General"
的department
属性;Manager
是Employee
的子类,其增加了一个默认值为空数组的reports
属性,后面会将其赋值为Employee
对象数组;WorkerBee
是Employee
的子类,其增加了一个默认值为空数组的projects
属性;Engineer
也是WorkerBee
的子类,其增加了一个默认值为空字符串的machine
属性,同时将department
属性重载为"Engineering"
;SalesPerson
是WorkerBee
的子类,其增加了一个默认值为100
的quota
属性,并且将继承来的department
属性重载为"sales"
,表示所有销售人员都属于相同部门;
1 | /*===== Employee =====*/ |
注意:如果上面代码里的构造函数不需要接受任何参数,则可以省略构造函数名称后面的圆括号
()
。
继承属性
下面代码创建了一个 mark
对象作为 WorkerBee
的实例,当 JavaScript 执行 new
操作符的时候,首先会创建一个对象,并且将该对象当中的
[[prototype]]
指向
WorkerBee.prototype
,然后再将该对象设置为执行
WorkerBee
构造函数时的 this
值。该对象的
[[Prototype]]
决定了其用于检索属性的原型链。当构造函数执行完毕之后,所有属性都完成了初始化,就可以将其引用赋值给一个
mark
变量:
1 | const mark = new WorkerBee(); |
上述过程不会显式将 mark
所继承的原型链属性,作为
mark
对象的本地属性。访问属性时,首先 JavaScript
会检查对象自身是否存在该属性,如果存在就返回属性值,如果不存在则会通过
[[Prototype]]
检查原型链。如果原型链当中依然未能查找出该属性,则会返回一个
undefined
。当 worker
对象实例化完成之后,其属性值状态如下所示:
1 | mark.name = ""; |
对象 mark
通过 mark.__proto__
从
Employee
继承了 name
与
department
属性,并且定义了一个新的 projects
属性,下面的示例代码修改了这些属性的默认值:
1 | mark.name = "Hank"; |
添加属性
JavaScript 允许在运行时为对象添加新的属性,例如通过
mark.age = 18
添加一个 bonus
属性,此时,除了
mark
对象之外的其它 WorkerBee
对象不会拥有该属性。
1 | /* mark 对象 */ |
如果向对象的原型添加新的属性,那么该属性将会添加至从该原型继承属性的所有对象当中,例如下面代码会向所有
Employee
的对象添加 specialty
属性:
1 | Employee.prototype.specialty = "none"; |
向 Employee
原型当中添加 specialty
属性之后,可以在 Engineer
的原型里对该属性进行重载,将其默认值 none
重新修改为
code
。
1 | Engineer.prototype.specialty = "code"; |
属性默认值 & 构造函数参数
JavaScript 的逻辑或操作符 ||
会判断第 1
个参数,如果其结果为 true
则返回该值,否则返回第 2
个参数的值,利用该特性可以巧妙定义对象属性的默认值:
1 | this.propertyName = newValue || defaultValue; |
除此之外,采用 function
构造函数声明对象时,还可以同时对属性的参数值进行赋值:
1 | function Name(newValue) { |
接下来,修改开头示例代码当中的
Employee
、WorkerBee
、Engineer
类为下面的形式:
1 | /*===== Employee =====*/ |
这样在创建 Engineer
对象的实例时,就可以通过构造函数指定属性的初始值:
1 | const hank = new Engineer("Keyboard"); |
通过 function
构造函数可以为对象指定本地的属性值,但是无法为经过原型链继承而来的属性进行赋值,因而需要调整
WorkerBee
和 Engineer
的构造函数定义,使得父对象的构造函数成为子对象的
base
成员方法,并且立刻在子对象当中进行调用:
1 | /*===== Employee =====*/ |
除了使用上述的 function
构造函数之外,还可以通过
call()
或者 apply()
函数来实现继承,例如可以将上面的 Engineer
对象等价的修改为下面形式:
1 | function Engineer(name, projects, machine) { |
本地属性 & 继承属性
JavaScript 当中访问一个对象的属性时,将会执行如下两个步骤:
- 检查该属于是否存在于对象本地,如果存在,则直接返回属性值;
- 如果不存在,就会通过
__proto__
属性检查原型链,如果存在就返回值,不存在则返回undefined
;
例如下面代码当中的 amy
对象具有本地属性
projects
,而 name
和 department
属性则是通过 __proto__
获得:
1 | /*===== Employee =====*/ |
此时如果修改父对象 Employee
关联原型当中的
name
属性,这种影响并不会传递到子对象通过原型继承而来的
name
属性:
1 | /*===== Employee =====*/ |
如果在修改父对象的属性值时,希望这种修改操作可以被所有的子对象继承,那么就不能将其定义为本地属性,而应该将其添加至关联的原型当中:
1 | function Employee() { |
JavaScript
会首先在对象自身的本地属性当中进行查找,如果没有找到则会到对象的特殊属性
__proto__
当中查找,这个过程称为在对象的原型链中查找,这就是 JavaScript
的对象属性查找机制。
prototype 与 __proto__
__proto__
是一个用于访问对象内部
[[Prototype]]
的访问器属性,该属性已经在 ECMAScript 6
规范当中得到了标准化。通过 Object.prototype.__proto__
方式访问 __proto__
属性的方法已经逐渐被废弃,Mozilla
官方更加推荐使用 Object.getPrototypeOf
方式进行访问。而
prototype
表示的是原型对象,所有的
JavaScript 对象都继承自
Object.prototype
,该对象当中的默认属性列表如下所示:
1 | { |
除了 Object
对象以外,无论是采用对象字面量
{}
,还是采用构造函数 function
,或者是采用
Object.create()
方法创建的对象,都会拥有一个
__proto__
属性:
1 | /*===== 采用字面量方式创建的对象 =====*/ |
JavaScript 函数的 prototype
属性实际上指的是
Function.prototype
,其属性值为解析引擎的原生代码。而下面示例里打印出的
prototype
属性,则是 JavaScript 解析引擎将
test2
视为构造函数之后的处理结果:
1 | const test2 = function () {}; |
对于本节内容开始时提到的 Engineer
类,如果在这里创建一个对象 hank
:
1 | const hank = new Engineer("Hank", ["www.uinio.com", "www.uinika.com"], "Keyboard"); |
则对于 hank
对象而言,下面语句的返回结果全部为
true
:
1 | hank.__proto__ == Engineer.prototype; // true |
instanceof 判断对象的实例
除了 Object
对象之外,每一个对象都会拥有
__proto__
属性,而每一个函数都会拥有 prototype
属性。创建对象时 JavaScript 会将特殊的 __proto__
属性设置为构造函数的 prototype
属性值,所以表达式
new Example()
时会创建一个对象,该对象的
__proto__
属性等于
Example.prototype
。如果修改 Example.prototype
的值,就会改变所有通过 new Example()
创建的对象。
通过比较对象的 __proto__
属性和函数的 prototype
属性,就可以检测出对象之间的继承关系。JavaScript 提供了便捷的
instanceof
操作符,用于检测对象与构造函数之间的关系,如果对象继承自构造函数的原型,则返回
true
,否则就会返回 false
:
1 | const empty = {}; |
多重继承
由于 JavaScript 的继承是基于对象的原型链实现的,每个对象只会存在一个与之关联的原型,所以 JavaScript 不支持多重继承。但是,可以通过在构造函数当中调用多个其它构造器函数,从而模拟出一种多重继承的假象:
1 | /*===== Employee =====*/ |
上面代码当中,对象 uinika
确实从 Hobbyist()
构造函数当中获得了 hobby
属性,但是如果通过
Hobbyist.prototype.equipment = ["Oscilloscope", "Multimeter", "Solder"]
添加一个新的 equipment
属性到 Hobbyist
构造函数的原型,那么对象 uinika
并不会自动继承该属性。
类 class
类是用于创建对象的模板,ES6
规范当中提出的类 class
依然是建立在原型基础之上的。
类的声明与定义
在 ES6 规范当中,通过 class
关键字就可以声明一个
JavaScript 类,该类同样属于 Object
的子类:
1 | class Rectangle { |
上面代码当中的 constructor()
是一个构造函数,该方法用于创建和初始化 class
类的对象,每个类都只能拥有一个构造函数。
与函数声明不同,类必须要先声明之后才能够进行访问,否则就会抛出
ReferenceError
错误:
1 | let p = new Rectangle(); // Uncaught ReferenceError: Rectangle is not defined |
类表达式是定义 class
类的另外一种方法:
1 | const Rectangle = class { |
类体
类体放置在一对花括号 {}
当中,用于定义成员属性、成员函数以及构造函数,类体当中的代码总是在
JavaScript 严格模式下执行:
1 | class Rectangle { |
static 静态成员
static
关键字用于在 class
类当中定义一个静态的成员函数或者属性,调用静态方法时不需要进行实例化,直接通过类的名称就可以调用:
1 | class Point { |
注意:静态方法通常用于为
class
类创建工具函数。
私有属性
class
类当中声明属性,默认都是公有属性:
1 | class Rectangle { |
通过在成员属性前面添加 #
符号,就可以将其声明为私有属性:
1 | class Rectangle { |
extends 关键字
通过使用 extends
关键字,可以定义父类与子类之间的继承关系:
1 | /*===== 父类 =====*/ |
注意:如果子类当中定义有构造函数,那么这个子类必须调用
super()
之后才能够使用this
指针。
ES6 规范的 class
类同样也可以继承 ES5
当中基于构造函数 function
定义的对象:
1 | function Animal(name) { |
但是要注意,class
类不能继承没有构造函数的字面量
{}
对象,这样会引发 TypeError
错误:
1 | const Animal = { |
可以通过 Object.setPrototypeOf()
方法设置原型的方式,实现字面量对象 {}
到 class
类的继承:
1 | const Animal = { |
super 指向父类
class
当中的关键字 super
可以用于调用父类当中的成员函数或者构造函数。
1 | class Cat { |
异常处理
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 | throw "Error"; // 抛出字符串类型 |
可以在抛出异常时声明一个对象,这样就可以在 catch
代码块中读取到这个对象的属性。
1 | /* 创建一个用户异常对象 UserException */ |
try...catch 语句
如果 try
代码块当中的语句抛出异常,那么程序的执行流程就会立即进入
catch
代码块。如果 try
代码块没有抛出异常,那么程序的执行流程就会跳过 catch
代码块。
1 | let result; |
catch
代码块用于捕捉所有发生在 try
代码块当中的异常,其中的 exception
参数用于接收由
throw
语句抛出的异常对象或者错误信息。
finally 语句
finally
代码块通常会放置在 try...catch
语句之后,无论是否抛出异常都会得到执行,通常用于优雅的退出操作或者释放资源。
1 | try { |
finally
语句块里使用 return
关键字返回的结果,也将是整个 try...catch...finally
语句块的返回值,这里无需关心 try
或者 catch
语句块里的返回值。
1 | function test() { |
Error 对象
Error
对象包含有 name
(错误名称)和
message
(错误信息)两个属性,通过该对象提供的
Error()
构造函数,可以非常方便的自定义错误对象。
1 | function test() { |
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 | 初始状态,可能触发fulfilled 或rejected 状态中的一种。 |
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 | const promise = new Promise((resolve, reject) => { |
Promise 示例代码
一个简单易于理解的使用 Promise 的示例如下:
1 | function asyncFunction() { |
下面的代码通过 Promise 的异步处理方式来获取 XMLHttpRequest 的数据:
1 | function getURL(URL) { |
Promise 构造函数上的原型
Promise.prototype.constructor
表示 Promise
构造函数的原型,以便于在原型上放置then()
、catch()
、finally()
方法。
1 | let promise = new Promise((resolve, reject) => { |
Promise.prototype.then(onFulfilled, onRejected)
then()
方法返回一个Promise
,两个参数分别是
Promise
成功或者失败状态的回调函数。如果忽略某个状态的回调函数参数,或者提供非函数参数,then()
方法将会丢失该状态的回调信息,但是并不会产生错误。如果调用then()
的
Promise
的状态发生改变,但是then()
中并没有对应状态的回调函数,则then()
最终将创建一个未经回调函数处理的全新
Promise 对象,并使用最初的 Promise 状态来作为这个全新 Promise
的状态。
1 | let promise = new Promise((resolve, reject) => { |
Promise.prototype.catch(onRejected)
catch()
只处理rejected
的情况,该方法返回一个
Promise,实质是Promise.prototype.then(undefined, onRejected)
的语法糖。
1 | let promise = new Promise((resolve, reject) => { |
Promise.prototype.finally(onFinally)
finally()
方法返回一个
Promise,在then()
和catch()
执行完成之后,都会调用finally
指定的回调函数,这样可以避免一些重复的语句同时出现在then()
和catch()
当中。
1 | let promise = new Promise((resolve, reject) => { |
包括 Eage 在内的 IE 系列浏览器目前都不支持
finally()
方法。
Promise 对象实例方法
Promise.resolve(value)
该方法会根据参数的不同,返回不同的 Promise
对象。当接收Promise
对象作为参数的时候,返回的还是接收到的Promise
对象;
1 | let jqueryPromise = $.ajax("http://gc.ditu.aliyun.com/geocoding?a=成都市"); // jquery返回的promise对象 |
当接收到 thenable 类型对象(由 ES6 Promise 提出的 Thenable
是指具有.then()
方法的对象)的时候,返回一个具有该 then
方法的全新 Promise 对象;
1 | let thenablePromise = Promise.resolve( |
当接收其它数据类型参数的时候(字符串或null
)将会返回一个将该参数作为值的全新
Promise 对象。
1 | let promise = Promise.resolve([1, 2, 3]); |
Promise.reject(reason)
返回一个被reason
拒绝的
Promise,但是与resolve()
不同之处在于,即使reject()
接收的参数是一个
Promise 对象,该函数依然会返回一个全新的 Promise。
1 | let rejectedPromise = Promise.reject(new Error("this is a error!")); |
Promise.all(iterable)
当iterable
参数中所有 Promise 都被resolve
,
或者参数并不包含 Promise 时,
all()
方法返一个回resolve
的全新 Promise
对象。当iterable
中一个 Promise
返回拒绝reject
时,
all()
方法会立即终止并返回一个reject
的全新
Promise 对象。
1 | let promise1 = Promise.resolve(1), |
Promise.race(iterable)
当iterable
参数数组中的任意一个 Promise
对象变为resolve
或者reject
状态,该函数就会立刻返回一个全新的
Promise 对象,并使用该 Promise
对象进行resolve
或者reject
。
1 | let promise1 = Promise.resolve(1), |
Promise 方法链
1 | function taskA() { |
如果taskA()
发生异常的话,会按照taskA() → onRejected() → finalTask()
这个流程进行处理,taskB()
并不会被调用的。
在上面代码
taskA()
或taskB()
的处理中,如果发生异常或者返回了Rejected
状态的Promise
对象,就会调用onRejected()
方法。
Promise 中的各个 Task
相互独立,如果taskA()
想为taskB()
传递参数,需要在taskA()
当中return
相应的值,该值将会作为taskB()
的参数。
1 | function increment(value) { |
return
的值不仅只局限于字符串或者数值,也可以是普通对象或者 Promise。return
值经由Promise.resolve( return返回值 );
包装处理,无论回调函数内部返回什么,then()
方法总是返回一个新建的 Promise 对象。
then()方法内的函数是异步调用的
执行Promise.resolve(value)
等方法时,如果 Promise
对象立刻进入resolve
状态,则是否意味.then()
里指定的函数是同步调用的?
1 | var promise = new Promise((resolve) => { |
上面的示例代码说明:传入then()
内的函数是被异步执行的。JavaScript
代码编写原则之一是尽量不要对异步回调函数进行同步调用,否则处理顺序可能会与预期不符,甚至导致栈溢出或异常处理错乱;如果需要在将来某个时刻调用异步回调函数,可以使用setTimeout()
、setInterval()
等异步
API(绑定的函数不会立刻执行,而是延迟到队列的最后)。
为了避免同时使用同步、异步调用可能引起的混乱,Promise 规范约定只能使用异步调用方式 。
每次调用 then()都会返回新建的 Promise
Promise
之所以能够进行链式的方法调用,是由于无论then()
还是catch()
都会返回一个全新的
Promise 对象。
1 | let promise = new Promise((resolve) => { |
通过===
进行严格相等比较,可以看出上述 3 个 Promise
对象是互不相同的,也就证明then()
和catch()
都返回了不同的
Promise 对象。
下面是一个通过then()
返回新创建 Promise
对象的错误使用方法:
1 | /* 错误的返回方式 */ |
上述写法存在诸多问题,首先在promise.then()
中产生的异常不会被外部捕获,其次也不能得到then()
的返回值。由于每次调用promise.then()
都会返回一个新创建的
Promise 对象,因此需要通过Promise
Chain将调用链式化,修改后的代码如下:
1 | /* 正确的做法 */ |
与
Promise.then()
类似,包括Promise.all()
和Promise.race()
都会接收Promise
对象作为参数,然后返回一个与接收参数不同的全新 Promise 对象,使用时应多加注意。
使用 reject 而不是 throw
Promise
的构造函数以及then()
中执行的函数都可以认为是在try...catch
块中运行,因此即便使用了throw
,程序本身也不会因为抛出异常而终止。
1 | let promise = new Promise((resolve, reject) => { |
上面的代码运行时没有任何问题,但是如果需要把 Promise
对象状态设置为Rejected
的话,相比throw
关键字reject()
方法会更加合理。接下来,我们方便的通过
Promise 构造函数中的reject
参数,将 Promise
对象的状态设置为Rejected
。
1 | let promise = new Promise((resolve, reject) => { |
reject
比throw
更安全另一个原因在于,有些场景下很难届定throw
是开发人员主动抛出,还因为真正的代码异常导致的。
当需要在then()
中执行reject
操作的时候,可以通过then()
当中的回调函数return
一个自定义的
Promise 对象。然后根据这个自定义 Promise
对象的状态,下一个then()
中注册的的onFulfilled()
或onRejected()
回调函数会相应进行调用,从而实现then()
中不通过throw
关键字也能进行reject
操作。
1 | let promise = Promise.resolve(); |
例如下面代码中,newPromise
对象状态为Rejected
的时候,后续catch()
中的onRejected
方法会被调用。
1 | let onRejected = console.error.bind(console); |
接下来,再通过Promise.reject()
来简化代码:
1 | let onRejected = console.error.bind(console); |
Promise 的缺点在于一旦新建就会立即执行,并且无法中途取消。其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。第三,当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Generator 函数
Generator 函数是 ES6 提供的一种异步编程解决方案,Generator
函数是一个封装了多个内部状态的状态机,执行 Generator
函数会返回一个遍历器对象,可以通过该对象遍历 Generator
函数内部的每个状态。Generator
函数与普通函数的区别在于:声明时在function
关键字与函数名称之间添加*
号,函数体内部使用yield
([jild]
n.产出,收益)表达式定义不同的内部状态。
下面代码定义了一个 Generator
函数myGenerator()
,其内部拥有Hi
和Generator
两个yield
表达式,该函数共拥有Hi
、Generator
、!
三个状态。与普通函数不同的是,Generator
函数被调用后并不立刻执行,返回的也并非函数执行结果,而是一个指向内部状态的遍历器对象(Iterator
Object)。通过调用遍历器对象的next()
方法,可以将执行流程移动至下一状态(即接下来的yield
表达式或return
语句)。换而言之,Generator
函数是分段执行的,yield
表达式是暂停执行的标记,而next()
方法可以恢复执行。
1 | function* myGenerator() { |
每次调用遍历器对象的next()
就会返回一个拥有value
和done
属性的对象,其中value
属性表示当前内部状态的值(即yield
后面表达式的值);done
属性是一个布尔值(用来表示遍历是否结束)。只有调用next()
方法之后,对应yield
关键字后的表达式才会被执行(即惰性求值
Lazy Evaluation)。
ES6
规范没有指定function
关键字与函数名称之间星号*
出现的位置,所以下面的写法都是等效的:
1 | function* demo(x, y) { |
Generator 函数与 Iterator
Generator
函数返回的是遍历器对象,因此将其赋值给一个对象的Symbol.iterator
属性,使该对象具备
Iterator 接口,从而能够通过...
运算符进行遍历。
1 | let myIterator = {}; |
Generator
函数执行后返回的遍历器对象本身就具有Symbol.iterator
属性,执行后将会返回自身。
1 | function* generator() { |
基于 ES6 的 JavaScript 简明语法书