本文为学习笔记,参考Javascript.info,菜鸟等文章编写。
跳转连接
文章目录
原始类型的方法
JavaScript 允许我们像使用对象一样使用原始类型(字符串,数字等)。JavaScript 还提供了这样的调用方法。
对象之间的关键区别
一个原始值:
- 是原始类型中的一种值。
- 在 JavaScript 中有 7 种原始类型:
string
,number
,bigint
,boolean
,symbol
,null
和undefined
。
一个对象:
- 能够存储多个值作为属性。
- 可以使用大括号
{}
创建对象,例如:{name: "John", age: 30}
。JavaScript 中还有其他种类的对象,例如函数就是对象。
当作对象的原始类型
“对象包装器”对于每种原始类型都是不同的,它们被称为 String
、Number
、Boolean
、Symbol
和 BigInt
。因此,它们提供了不同的方法。
例如,字符串方法
str.toUpperCase()
返回一个大写化处理的字符串。let str = "Hello"; alert( str.toUpperCase() ); // HELLO
以下是
str.toUpperCase()
中实际发生的情况:
- 字符串
str
是一个原始值。因此,在访问其属性时,会创建一个包含字符串字面值的特殊对象,并且具有可用的方法,例如toUpperCase()
。- 该方法运行并返回一个新的字符串(由
alert
显示)。- 特殊对象被销毁,只留下原始值
str
。所以原始类型可以提供方法,但它们依然是轻量级的。
JavaScript 引擎高度优化了这个过程。它甚至可能跳过创建额外的对象。但是它仍然必须遵守规范,并且表现得好像它创建了一样。
构造器 String/Number/Boolean 仅供内部使用
像 Java 这样的一些语言允许我们使用 new Number(1)
或 new Boolean(false)
等语法,明确地为原始类型创建“对象包装器”。
在 JavaScript 中,由于历史原因,这也是可以的,但极其 不推荐。因为这样会出问题.
另一方面,调用不带 new
(关键字)的 String/Number/Boolean
函数是可以的且有效的。它们将一个值转换为相应的类型:转成字符串、数字或布尔值(原始类型)。
例如,下面完全是有效的:
let num = Number("123"); // 将字符串转成数字
数字类型
两种类型:
- 常规数字 以 64 位的格式 IEEE-754 存储,也被称为“双精度浮点数”。常规整数不能安全地超过 (253-1) 或小于 -(253-1),若超出范围会造成精度问题 。
- BigInt 用于表示任意长度的整数。
常规数字
编写数字的更多方法
① 可以使用下划线 _
作为分隔符
let billion = 1_000_000_000;
这里的下划线 _
扮演了“语法糖”的角色,使得数字具有更强的可读性。JavaScript 引擎会直接忽略数字之间的 _
② 通过在数字后面附加字母 "e"
并指定零的个数来缩短数字
let billion = 1e9; // 10 亿,字面意思:数字 1 后面跟 9 个 0
③ 十六进制,二进制和八进制数字
十六进制 数字在 JavaScript 中被广泛用于表示颜色,编码字符以及其他许多东西。有一种较短的写方法:0x
,然后是数字。(x大小写都可以)
二进制和八进制数字系统很少使用,但也支持使用 0b
和 0o
前缀
alert( 0xff ); // 十六进制形式的 255
let a = 0b11111111; // 二进制形式的 255
let b = 0o377; // 八进制形式的 255
toString(base)
方法 num.toString(base)
返回在给定 base
进制数字系统中 num
的字符串表示形式。
let num = 255;
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
base=36 是最大进制,数字可以是 0..9
或 A..Z
。所有拉丁字母都被用于了表示数字。
对于 36
进制来说,一个有趣且有用的例子是,当我们需要将一个较长的数字标识符转换成较短的时候,例如做一个短的 URL。可以简单地使用基数为 36
的数字系统表示
使用两个点来调用一个方法 …
如果我们想直接在一个数字上调用一个方法,那么我们需要在它后面放置两个点 ..
。
123456..toString(36)
//或
(123456).toString(36)
如果我们放置一个点:123456.toString(36)
,那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。
Math库
JavaScript 有一个内建的 Math 对象,它包含了一个小型的数学函数和常量库。
舍入
舍入(rounding)是使用数字时最常用的操作之一。
舍入到整数:
Math.floor
向下舍入Math.ceil
向上舍入Math.round
向最近的整数舍入Math.trunc
移除小数点后的所有内容而没有舍入
舍入到小数:
① 乘除法
let num = 1.23456;
alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
② 函数 toFixed(n)
将数字舍入到小数点后 n
位,并以字符串形式返回结果。这会向上或向下舍入到最接近的值。
let num = 12.34;
alert( num.toFixed(1) ); // "12.3"
请注意,toFixed
总是返回一个字符串。它确保小数点后有 2 位数字。
不精确的计算
在内部,数字是以 64 位格式 IEEE-754 表示的,所以正好有 64 位可以存储一个数字。
其中 52 位被用于存储这些数字,其中 11 位用于存储小数点的位置,而 1 位用于符号。
① 如果一个数字真的很大,则可能会溢出 64 位存储,变成一个特殊的数值 Infinity
② 精度的损失
alert( 0.1 + 0.2 == 0.3 ); // false
原因:一个数字以其二进制的形式存储在内存中,一个 1 和 0 的序列。但是在十进制数字系统中看起来很简单的 0.1
,0.2
这样的小数,实际上在二进制形式中是无限循环小数。使用二进制数字系统无法 精确 存储 0.1 或 0.2,就像没有办法将三分之一存储为十进制小数一样。
IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题。这些舍入规则通常不允许我们看到“极小的精度损失”,但是它确实存在。
alert( 0.1.toFixed(20) ); // 0.10000000000000000555
解决方法
① 借助方法 toFixed(n)
对结果进行舍入
let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // "0.30"
② 将小数先转为整数,得到结果后再转为小数
alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001
测试:isFinite 和 isNaN
两个特殊值属于 number
类型,但不是“普通”数字
Infinity
(和-Infinity
)是一个特殊的数值,比任何数值都大(小)。NaN
代表一个 error。
为了将他们从数字中区分出来,这里有用于检查它们的特殊函数
-
isNaN(value)
将其参数转换为数字,然后测试它是否为NaN
(值 “NaN” 是独一无二的,它不等于任何东西,包括它自身,所以=== NaN
无效)alert( NaN === NaN ); // false alert( isNaN(NaN) ); // true alert( isNaN("str") ); // true
-
isFinite(value)
将其参数转换为数字,如果是常规数字而不是NaN/Infinity/-Infinity
,则返回true
:alert( isFinite("15") ); // true alert( isFinite("str") ); // false,因为是一个特殊的值:NaN alert( isFinite(Infinity) ); // false,因为是一个特殊的值:Infinity
有时 isFinite
被用于验证字符串值是否为常规数字(在所有数字函数中,包括 isFinite
,空字符串或仅有空格的字符串均被视为 0
)
let num = +prompt("Enter a number", '');
// 结果会是 true,除非你输入的是 Infinity、-Infinity 或不是数字
alert( isFinite(num) );
**
Object.is
*
Object.is
是一个特殊的内建方法 ,类似于===
对值进行比较,但它对于两种边缘情况更可靠:
- 它适用于
NaN
:Object.is(NaN, NaN) === true
,这是件好事。- 值
0
和-0
是不同的:Object.is(0, -0) === false
,从技术上讲这是对的,因为在内部,数字的符号位可能会不同,即使其他所有位均为零。在所有其他情况下,
Object.is(a, b)
与a === b
相同。这种比较方式经常被用在 JavaScript 规范中。当内部算法需要比较两个值是否完全相同时,它使用
Object.is
。
BigInt
BigInt
是一种特殊的数字类型,它提供了对任意长度整数的支持。
创建 bigint 的方式有两种:在一个整数字面量后面加 n
或者调用 BigInt
函数,该函数从字符串、数字等中生成 bigint。
数学运算符
BigInt
大多数情况下可以像常规数字类型一样使用。
但是,不可以把 bigint 和常规数字类型混合使用。如果有需要,我们应该显式地转换它们:使用 BigInt()
或者 Number()
alert(1n + 2n); // 3
alert(1n + 2); // Error: Cannot mix BigInt and other types
et bigint = 1n;
let number = 2;
// 将 number 转换为 bigint
alert(bigint + BigInt(number)); // 3
// 将 bigint 转换为 number
alert(Number(bigint) + number); // 3
比较运算符
比较运算符,例如 <
和 >
,使用它们来对 bigint 和 number 类型的数字进行比较没有问题。
但是请注意,由于 number 和 bigint 属于不同类型,它们可能在进行 ==
比较时相等,但在进行 ===
(严格相等)比较时不相等
alert( 2n > 1n ); // true
alert( 2n > 1 ); // true
alert( 1 == 1n ); // true
alert( 1 === 1n ); // false
布尔运算
布尔运算中时,bigint 的行为类似于 number。( if、||、&&)。
字符串
所有的字符串都使用 UTF-16 编码。即:每个字符都有对应的数字代码。有特殊的方法可以获取代码表示的字符,以及字符对应的代码。
引号(Quotes)
字符串可以包含在单引号、双引号或反引号中
单引号和双引号基本相同。
反引号允许我们通过 ${…}
将任何表达式嵌入到字符串中
function sum(a, b) {
return a + b;
}
alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3.
使用反引号允许字符串跨行(单引号和双引号不能这样做)
let guestList = `Guests:
* John
* Pete
* Mary
`;
alert(guestList); // 客人清单,多行
特殊字符
所有的特殊字符都以反斜杠字符 \
开始。它也被称为“转义字符”。
字符 | 描述 |
---|---|
\n | 换行 |
\r | 在 Windows 文本文件中,两个字符 \r\n 的组合代表一个换行。而在非 Windows 操作系统上,它就是 \n 。这是历史原因造成的,大多数的 Windows 软件也理解 \n 。 |
\' , \" | 引号 |
\\ | 反斜线 |
\t | 制表符 |
\b , \f , \v | 退格,换页,垂直标签 —— 为了兼容性,现在已经不使用了。 |
\xXX | 具有给定十六进制 Unicode XX 的 Unicode 字符,例如:'\x7A' 和 'z' 相同。 |
\uXXXX | 以 UTF-16 编码的十六进制代码 XXXX 的 Unicode 字符,例如 \u00A9 —— 是版权符号 © 的 Unicode。它必须正好是 4 个十六进制数字。 |
\u{X…XXXXXX} (1 到 6 个十六进制字符) | 具有给定 UTF-32 编码的 Unicode 符号。一些罕见的字符用两个 Unicode 符号编码,占用 4 个字节。这样我们就可以插入长代码了。 |
字符串长度
length
属性表示字符串长度
length
是一个属性。掌握其他编程语言的人,有时会错误地调用str.length()
而不是str.length
。
访问字符
要获取在 pos
位置的一个字符,可以使用方括号 [pos]
或者调用 str.charAt(pos)
方法。第一个字符从零位置开始
方括号是获取字符的一种现代化方法,而 charAt
是历史原因才存在的。
它们之间的唯一区别是,如果没有找到字符,[]
返回 undefined
,而 charAt
返回一个空字符串
str.codePointAt(pos)
:返回在 pos
位置的字符代码
String.fromCodePoint(code)
:通过数字 code
创建字符
\u
后跟十六进制代码,添加 Unicode 字符
查找子字符串
str.indexOf(substr, pos)
:从给定位置 pos
开始,在 str
中查找 substr
,若没找到,则返回 -1
,否则返回匹配成功的位置。
str.lastIndexOf(substr, pos)
:从字符串的末尾开始搜索到开头
str.includes(substr, pos)
:根据 str
中是否包含 substr
来返回 true/false
。
str.startsWith
和 str.endsWith
:检测字符串是否用某个字符串开头/结尾。例如**“Widget”.startsWith(“Wid”)**
如果我们对所有存在位置都感兴趣,可以在一个循环中使用 indexOf
。每一次新的调用都发生在上一匹配位置之后:
let str = "As sly as a fox, as strong as an ox";
let target = "as";
let pos = -1;
while ((pos = str.indexOf(target, pos + 1)) != -1) {
alert( pos );
}
小技巧
按位(bitwise)NOT 技巧 :
~n
等于-(n+1)
。在
if
测试中indexOf
有一点不方便,如果找不到位置返回的数是-1,所以无法直接放进去if()中。let str = "Widget with id"; if (str.indexOf("Widget")) { alert("We found it"); // 不工作! } if ((str.indexOf("Widget")) != -1) { alert("We found it"); //工作 } if (~str.indexOf("Widget")) { alert( 'Found it!' ); // 正常运行,且缩短代码 }
获取子字符串
str.slice(start [, end])
:返回字符串从 start
到(但不包括)end
的部分。支持负参数
str.substring(start [, end])
:返回字符串从 start
到(但不包括)end
的部分。这与 slice
几乎相同,但它允许 start
大于 end
。不支持负参数
str.substr(start [, length])
:返回字符串从 start
开始的给定 length
的部分。
比较字符串
字符串按字母顺序逐字比较。
小写字母总是大于大写字母
带变音符号的字母存在“乱序”的情况
数组
数组是一种特殊的对象。
数组在对象的基础上提供了特殊的方法来处理有序的数据集合以及 length
属性,并做内部实现的优化(这些元素一个接一个地存储在连续的内存区域),以使数组运行得非常快。
使用方括号来访问属性 arr[0]
实际上是来自于对象的语法。它其实与 obj[key]
相同,其中 arr
是对象,而数字用作键(key)。
创建一个空数组有两种语法,绝大多数情况下使用的都是第二种语法。
let arr = new Array();
let arr = [];
访问元素与修改
使用方括号 [ ]
可以访问数组元素
let fruits = ["Apple", "Orange", "Plum"];
// 访问
alert( fruits[0] ); // Apple
// 修改
fruits[2] = 'Pear';
使用 “at” 获取最后一个元素
在 JavaScript 中,使用方括号访问数组时,括号内不能为负数。若为负数,结果将是 undefined
,因为方括号中的索引是被按照其字面意思处理的。
换种思路,可以如下访问:arr[arr.length - 1]
。
这里有一个更简短的语法 arr.at(-1)
pop/push, shift/unshift 方法
**队列(queue)**是最常见的使用数组的方法之一。在计算机科学中,这表示支持两个操作的一个有序元素的集合。
push
在数组末端添加元素.shift
取出队列首端的一个元素,整个队列往前移,这样原先排第二的元素现在排在了第一。
栈 是数组的另一个用例
push
在数组末端添加元素.pop
取出并返回数组的最后一个元素.
JavaScript 中的数组既可以用作队列,也可以用作栈。它们允许你从首端/末端来添加/删除元素。
这在计算机科学中,允许这样的操作的数据结构被称为 双端队列(deque)。
unshift
:在数组的首端添加元素
push
和 unshift
方法都可以一次添加多个元素
let fruits = ["Apple"];
fruits.push("Orange", "Peach");
fruits.unshift("Pineapple", "Lemon");
alert( fruits ); // ["Pineapple", "Lemon", "Apple", "Orange", "Peach"]
性能
push/pop
方法运行的比较快,而 shift/unshift
比较慢。
push/pop
不需要移动任何东西,添加元素时只需增加/清除索引值并修改length。
shift
操作必须做三件事:
- 移除索引为
0
的元素。 - 把所有的元素向左移动,把索引
1
改成0
,2
改成1
以此类推,对其重新编号。 - 更新
length
属性。
数组里的元素越多,移动它们就要花越多的时间,也就意味着越多的内存操作。
unshift
同理
关于 “length”
当我们修改数组的时候,length
属性会自动更新。准确来说,它实际上不是数组里元素的个数,而是最大的数字索引值加一。
一个数组只有一个元素,但是这个元素的索引值很大,那么这个数组的 length
也会很大
length
属性是可写的。
如果我们手动增加它,则不会发生任何有趣的事儿。但是如果我们减少它,数组就会被截断。
let arr = [1, 2, 3, 4, 5];
arr.length = 2; // 截断到只剩 2 个元素
alert( arr ); // [1, 2]
arr.length = 5; // 又把 length 加回来
alert( arr[3] ); // undefined:被截断的那些数值并没有回来
所以,清空数组最简单的方法就是:arr.length = 0;
。
toString
数组有自己的 toString
方法的实现,会返回以逗号隔开的元素列表。
let arr = [1, 2, 3];
alert( arr ); // 1,2,3
alert( String(arr) === '1,2,3' ); // true
alert( [] + 1 ); // "1"
alert( [1] + 1 ); // "11"
alert( [1,2] + 1 ); // "1,21"
数组方法
添加/移除数组元素
我们已经学了从数组的首端或尾端添加和删除元素的方法:arr.push(...items)
arr.pop()
arr.shift()
arr.unshift(...items)
① 使用delete
删除:元素被删除,对应位置变为undefined,但数组length不变,且元素位置没有改变。
② arr.splice(start[, deleteCount, elem1, ..., elemN])
:从索引 start
开始修改 arr
:删除 deleteCount
个元素并在当前位置插入 elem1, ..., elemN
。最后返回被删除的元素所组成的数组。
let arr = ["I", "study", "JavaScript", "right", "now"];
// 删除数组的前三项,并使用其他内容代替它们
let aaa = arr.splice(0, 3, "Let's", "dance");
alert( arr ) // ["Let's", "dance", "right", "now"]
alert ( aaa )// ["I", "study", "JavaScript"]
③ arr.slice([start], [end])
:返回一个新数组,将所有从索引 start
到 end
(不包括 end
)的数组项复制到一个新的数组。
④ arr.concat(arg1, arg2...)
:创建一个新数组,其中包含来自于其他数组和其他项的值。
遍历:forEach
arr.forEach
方法允许为数组的每个元素都运行一个函数。
语法:
arr.forEach(function(item, index, array) {
// ... do something with item
});
例如,下面这个程序显示了数组的每个元素:
// 对每个元素调用 alert ["Bilbo", "Gandalf", "Nazgul"].forEach(alert);
而这段代码更详细地介绍了它们在目标数组中的位置:
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => { alert(`${item} is at index ${index} in ${array}`); });
在数组中搜索
按具体内容搜
arr.indexOf(item, from)
—— 从索引from
开始搜索item
,如果找到则返回索引,否则返回-1
。arr.includes(item, from)
—— 从索引from
开始搜索item
,如果找到则返回true
(译注:如果没找到,则返回false
)。arr.lastIndexOf(item, from)
—— 与 indexOf 相同,但从右向左查找。
通常使用这些方法时只会传入一个参数:传入 item
开始搜索。默认情况下,搜索是从头开始的。
按特定条件搜
arr.find
方法,语法如下:
// item 是元素。index 是它的索引。array 是数组本身。
let result = arr.find(function(item, index, array) {
// 如果返回 true,则返回 item 并停止迭代
// 对于假值(falsy)的情况,则返回 undefined
});
arr.findIndex
方法(与 arr.find)具有相同的语法,但它返回找到的元素的索引,而不是元素本身。如果没找到,则返回 -1。
arr.findLastIndex
方法类似于 findIndex,但从右向左搜索,类似于 lastIndexOf。
filter
find
方法搜索的是使函数返回 true
的第一个(单个)元素。
如果需要匹配的有很多,我们可以使用 arr.filter(fn)
。
语法与 find
大致相同,但是 filter
返回的是所有匹配元素组成的数组:
let results = arr.filter(function(item, index, array) {
// 如果 true item 被 push 到 results,迭代继续
// 如果什么都没找到,则返回空数组
});
转换数组
arr.map
它对数组的每个元素都调用函数,并返回结果数组。
let result = arr.map(function(item, index, array) {
// 返回新值而不是当前元素
})
例如,在这里我们将每个元素转换为它的字符串长度:
let lengths = ["Bilbo", "Gandalf", "Nazgul"].map(item => item.length); alert(lengths); // 5,7,6
arr.sort
方法对数组进行 原位(in-place) 排序,更改元素的顺序。(译注:原位是指在此数组内,而非生成一个新数组。)
它还返回排序后的数组,但是返回值通常会被忽略,因为修改了 arr
本身。
**这些元素默认情况下被按字符串进行排序。**要使用我们自己的排序顺序,我们需要提供一个函数作为
arr.sort()
的参数。let arr = [ 1, 2, 15 ]; // 该方法重新排列 arr 的内容 arr.sort(); alert( arr ); // 1, 15, 2
按数字进行排序:
function compareNumeric(a, b) { if (a > b) return 1; if (a == b) return 0; if (a < b) return -1; } let arr = [ 1, 2, 15 ]; arr.sort(compareNumeric); alert(arr); // 1, 2, 15
比较函数可以返回任何数字,只需要返回一个正数表示“大于”,一个负数表示“小于”。使用箭头函数会更加简洁:
arr.sort( (a, b) => a - b );
arr.reverse
方法用于颠倒 arr
中元素的顺序。
let arr = [1, 2, 3, 4, 5];
arr.reverse();
alert( arr ); // 5,4,3,2,1
它也会返回颠倒后的数组 arr
。
str.split(delim [,number])
方法通过给定的分隔符 delim
将字符串分割成一个数组。第二个数字参数 —— 对数组长度的限制。
let names = 'Bilbo, Gandalf, Nazgul';
let arr = names.split(', ');
for (let name of arr) {
alert( `A message to ${name}.` ); // A message to Bilbo(和其他名字)
}
let arr = 'Bilbo, Gandalf, Nazgul, Saruman'.split(', ', 2);
alert(arr); // Bilbo, Gandalf
调用带有空参数 s
的 split(s)
,会将字符串拆分为字母数组
arr.join(glue)
与 split
相反,它会在它们之间创建一串由 glue
粘合的 arr
项。
let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';'); // 使用分号 ; 将数组粘合成字符串
alert( str ); // Bilbo;Gandalf;Nazgul
reduce/reduceRight
当我们需要遍历一个数组时 —— 我们可以使用 forEach
,for
或 for..of
。
当我们需要遍历并返回每个元素的数据时 —— 我们可以使用 map
。
arr.reduce
方法和 arr.reduceRight
方法和上面的种类差不多,但稍微复杂一点。它们用于根据数组计算单个值。
该函数一个接一个地应用于所有数组元素,并将其结果“搬运(carry on)”到下一个调用。
let value = arr.reduce(function(accumulator, item, index, array) {
// ...
}, [initial]);
accumulator
—— 是上一个函数调用的结果,第一次等于initial
(如果提供了initial
的话)。item
—— 当前的数组元素。index
—— 当前索引。arr
—— 数组本身。
// 例子,计算总和
let arr = [1, 2, 3, 4, 5];
let result = arr.reduce((sum, current) => sum + current, 0);
alert(result); // 15
Array.isArray
数组是基于对象的,不构成单独的语言类型。所以 typeof
不能帮助从数组中区分出普通对象。
Array.isArray(value)
:如果 value
是一个数组,则返回 true
;否则返回 false
。
大多数方法都支持 “thisArg”
thisArg 是可选的最后一个参数。
几乎所有调用函数的数组方法 —— 比如 find
,filter
,map
,除了 sort
是一个特例,都接受一个可选的附加参数 thisArg
。
thisArg
参数的值在 func
中变为 this
。
例如,在这里我们使用
army
对象方法作为过滤器,thisArg
用于传递上下文(passes the context):let army = { minAge: 18, maxAge: 27, canJoin(user) { return user.age >= this.minAge && user.age < this.maxAge; } }; let users = [ {age: 16}, {age: 20}, {age: 23}, {age: 30} ]; // 找到 army.canJoin 返回 true 的 user let soldiers = users.filter(army.canJoin, army); alert(soldiers.length); // 2 alert(soldiers[0].age); // 20 alert(soldiers[1].age); // 23
如果在上面的示例中我们使用了
users.filter(army.canJoin)
,那么army.canJoin
将被作为独立函数调用,并且这时this=undefined
,从而会导致即时错误。可以用
users.filter(user => army.canJoin(user))
替换对users.filter(army.canJoin, army)
的调用。前者的使用频率更高,因为对于大多数人来说,它更容易理解。
Iterable object(可迭代对象)
可迭代(Iterable) 对象是数组的泛化。这个概念是说任何对象都可以被定制为可在 for..of
循环中使用的对象。
Symbol.iterator
Symbol.iterator
为每一个对象定义了默认的迭代器。该迭代器可以被 for...of
循环使用。
迭代器对象和与其进行迭代的对象是分开的。
例如,我们有一个对象,它并不是数组,但是看上去很适合使用
for..of
循环。比如一个
range
对象,它代表了一个数字区间:let range = { from: 1, to: 5 }; // 我们希望 for..of 这样运行: // for(let num of range) ... num=1,2,3,4,5
为了让
range
对象可迭代(也就让for..of
可以运行)我们需要为对象添加一个名为Symbol.iterator
的方法(一个专门用于使对象可迭代的内建 symbol)。
- 当
for..of
循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有next
方法的对象。- 从此开始,
for..of
仅适用于这个被返回的对象。- 当
for..of
循环希望取得下一个数值,它就调用这个对象的next()
方法。next()
方法返回的结果的格式必须是{done: Boolean, value: any}
,当done=true
时,表示循环结束,否则value
是下一个值。这是带有注释的
range
的完整实现:let range = { from: 1, to: 5 }; // 1. for..of 调用首先会调用这个: range[Symbol.iterator] = function() { // ……它返回迭代器对象(iterator object): // 2. 接下来,for..of 仅与下面的迭代器对象一起工作,要求它提供下一个值 return { current: this.from, last: this.to, // 3. next() 在 for..of 的每一轮循环迭代中被调用 next() { // 4. 它将会返回 {done:.., value :...} 格式的对象 if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // 现在它可以运行了! for (let num of range) { alert(num); // 1, 然后是 2, 3, 4, 5 }
请注意可迭代对象的核心功能:关注点分离。
range
自身没有next()
方法。- 相反,是通过调用
range[Symbol.iterator]()
创建了另一个对象,即所谓的“迭代器”对象,并且它的next
会为迭代生成值。从技术上说,我们可以将它们合并,并使用
range
自身作为迭代器来简化代码。let range = { from: 1, to: 5, [Symbol.iterator]() { this.current = this.from; return this; }, next() { if (this.current <= this.to) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; for (let num of range) { alert(num); // 1, 然后是 2, 3, 4, 5 }
现在
range[Symbol.iterator]()
返回的是range
对象自身:它包括了必需的next()
方法,并通过this.current
记忆了当前的迭代进程。这样更短,对吗?是的。有时这样也可以。但缺点是,现在不可能同时在对象上运行两个
for..of
循环了:它们将共享迭代状态,因为只有一个迭代器,即对象本身。但是两个并行的for..of
是很罕见的,即使在异步情况下。
无穷迭代器(iterator)
无穷迭代器也是可能的。例如,将 range
设置为 range.to = Infinity
,这时 range
则成为了无穷迭代器。或者我们可以创建一个可迭代对象,它生成一个无穷伪随机数序列。也是可能的。
next
没有什么限制,它可以返回越来越多的值,这是正常的。
当然,迭代这种对象的 for..of
循环将不会停止。但是我们可以通过使用 break
来停止它。
显式调用迭代器
我们将会采用与 for..of
完全相同的方式遍历字符串,但使用的是直接调用。这段代码创建了一个字符串迭代器,并“手动”从中获取值。
let str = "Hello";
// 和 for..of 做相同的事
// for (let char of str) alert(char);
let iterator = str[Symbol.iterator]();
while (true) {
let result = iterator.next();
if (result.done) break;
alert(result.value); // 一个接一个地输出字符
}
比 for..of
给予更多的控制权。
可迭代(iterable)和类数组(array-like)
- Iterable 如上所述,是实现了
Symbol.iterator
方法的对象。 - Array-like 是有索引和
length
属性的对象,所以它们看起来很像数组。
举例说明:字符串即是可迭代的(for..of
对它们有效),又是类数组的(它们有数值索引和 length
属性)。
但是一个可迭代对象也许不是类数组对象。反之亦然,类数组对象可能不可迭代。
例如:上面例子中的 range
是可迭代的,但并非类数组对象,因为它没有索引属性,也没有 length
属性。
下面这个对象则是类数组的,但是不可迭代:
let arrayLike = { // 有索引和 length 属性 => 类数组对象
0: "Hello",
1: "World",
length: 2
};
// Error (no Symbol.iterator)
for (let item of arrayLike) {}
可迭代对象和类数组对象通常都 不是数组,它们没有 push
和 pop
等方法。如果我们有一个这样的对象,并想像数组那样操作它,那就非常不方便。
Array.from
Array.from 可以接受一个可迭代或类数组的值,并从中获取一个“真正的”数组。然后我们就可以对其调用数组方法了。
let arrayLike = {
0: "Hello",
1: "World",
length: 2
};
let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World(pop 方法有效)
如果是可迭代对象,也是同样:
// 假设 range 来自上文的例子中
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (数组的 toString 转化方法生效)
Array.from
的完整语法允许我们提供一个可选的“映射(mapping)”函数:
Array.from(obj[, mapFn, thisArg])
可选的第二个参数 mapFn
可以是一个函数,该函数会在对象中的元素被添加到数组前,被应用于每个元素,此外 thisArg
允许我们为该函数设置 this
。
// 假设 range 来自上文例子中
// 求每个数的平方
let arr = Array.from(range, num => num * num);
alert(arr); // 1,4,9,16,25
Map(映射)
Map
是一个带键的数据项的集合,就像一个 Object
一样。 但是它们最大的差别是 Map
允许任何类型的键(key)。
方法和属性如下:
new Map()
—— 创建 map。map.set(key, value)
—— 根据键存储值。map.get(key)
—— 根据键来返回值,如果map
中不存在对应的key
,则返回undefined
。map.has(key)
—— 如果key
存在则返回true
,否则返回false
。map.delete(key)
—— 删除指定键的值。map.clear()
—— 清空 map。map.size
—— 返回当前元素个数。
与对象不同,键不会被转换成字符串。键可以是任何类型。Map 还可以使用对象作为键。
// map创建语法
let map = new Map();
map.set('a', 500); // 字符串键
map.set('b', 350); // 数字键
map.set('c', 50); // 布尔值键
let map = new Map([
['a', 500],
['b', 350],
['c', 50]
]);
链式调用:每一次 map.set
调用都会返回 map 本身,所以我们可以进行“链式”调用
map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
Map 迭代
如果要在 map
里使用循环,可以使用以下三个方法:
map.keys()
—— 遍历并返回一个包含所有键的可迭代对象,map.values()
—— 遍历并返回一个包含所有值的可迭代对象,map.entries()
—— 遍历并返回一个包含所有实体[key, value]
的可迭代对象,for..of
在默认情况下使用的就是这个。
let recipeMap = new Map([
['cucumber', 500],
['tomatoes', 350],
['onion', 50]
]);
// 遍历所有的键(vegetables)
for (let vegetable of recipeMap.keys()) {
alert(vegetable); // cucumber, tomatoes, onion
}
// 遍历所有的值(amounts)
for (let amount of recipeMap.values()) {
alert(amount); // 500, 350, 50
}
// 遍历所有的实体 [key, value]
for (let entry of recipeMap) { // 与 recipeMap.entries() 相同
alert(entry); // cucumber,500 (and so on)
}
使用插入顺序:迭代的顺序与插入值的顺序相同。与普通的
Object
不同,Map
保留了此顺序。
Map
有内建的 forEach
方法
// 对每个键值对 (key, value) 运行 forEach 函数
recipeMap.forEach( (value, key, map) => {
alert(`${key}: ${value}`); // cucumber: 500 etc
});
Map与Object互转
Object.entries:从对象创建 Map
如果我们想从一个已有的普通对象(plain object)来创建一个 Map
,那么我们可以使用内建方法 Object.entries(obj)
,该方法返回对象的键/值对数组,该数组格式完全按照 Map
所需的格式。
let obj = {
name: "John",
age: 30
};
let map = new Map(Object.entries(obj));
alert( map.get('name') ); // John
Object.fromEntries:从 Map 创建对象
Object.fromEntries
方法的作用是相反的:给定一个具有 [key, value]
键值对的数组,它会根据给定数组创建一个对象
let prices = Object.fromEntries([
['banana', 1],
['orange', 2],
['meat', 4]
]);
// 现在 prices = { banana: 1, orange: 2, meat: 4 }
alert(prices.orange); // 2
Set(集合)
Set
是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次。
- 主要方法如下:
new Set(iterable)
—— 创建一个set
,如果提供了一个iterable
对象(通常是数组),将会从数组里面复制值到set
中。set.add(value)
—— 添加一个值,返回 set 本身set.delete(value)
—— 删除值,如果value
在这个方法调用的时候存在则返回true
,否则返回false
。set.has(value)
—— 如果value
在 set 中,返回true
,否则返回false
。set.clear()
—— 清空 set。set.size
—— 返回元素个数。
Set
的替代方法可以是一个用户数组,用 arr.find
在每次插入值时检查是否重复。但是这样性能会很差,因为这个方法会遍历整个数组来检查每个元素。Set
内部对唯一性检查进行了更好的优化。
Set 迭代(iteration)
Map
中用于迭代的方法在 Set
中也同样支持:
set.keys()
—— 遍历并返回一个包含所有值的可迭代对象,set.values()
—— 与set.keys()
作用相同,这是为了兼容Map
,set.entries()
—— 遍历并返回一个包含所有的实体[value, value]
的可迭代对象,它的存在也是为了兼容Map
。
我们可以使用 for..of
或 forEach
来遍历 Set:
let set = new Set(["oranges", "apples", "bananas"]);
for (let value of set) alert(value);
// 与 forEach 相同:
set.forEach((value, valueAgain, set) => {
alert(value);
});
注意一件有趣的事儿。forEach
的回调函数有三个参数:一个 value
,然后是 同一个值 valueAgain
,最后是目标对象。没错,同一个值在参数里出现了两次。
forEach
的回调函数有三个参数,是为了与 Map
兼容。当然,这看起来确实有些奇怪。但是这对在特定情况下轻松地用 Set
代替 Map
很有帮助,反之亦然。
WeakMap 和 WeakSet
WeakMap
的键必须是对象,不能是原始值
如果我们在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。
与上面常规的 Map
的例子相比,现在如果 john
仅仅是作为 WeakMap
的键而存在 —— 它将会被从 map(和内存)中自动删除。
WeakMap
不支持迭代以及 keys()
,values()
和 entries()
方法。所以没有办法获取 WeakMap
的所有键或值。
WeakMap
只有以下的方法:
weakMap.get(key)
weakMap.set(key, value)
weakMap.delete(key)
weakMap.has(key)
WeakSet
的表现类似:
- 与
Set
类似,但是我们只能向WeakSet
添加对象(而不能是原始值)。 - 对象只有在其它某个(些)地方能被访问的时候,才能留在
WeakSet
中。 - 跟
Set
一样,WeakSet
支持add
,has
和delete
方法,但不支持size
和keys()
,并且不可迭代。
Object.keys,values,entries
对于普通对象,下列这些方法是可用的:
Object.keys(obj)
—— 返回一个包含该对象所有的键的数组。Object.values(obj)
—— 返回一个包含该对象所有的值的数组。Object.entries(obj)
—— 返回一个包含该对象所有 [key, value] 键值对的数组。
Object.keys/values/entries 会忽略 symbol 属性。就像
for..in
循环一样,这些方法会忽略使用Symbol(...)
作为键的属性。若要获取Symbol 类型的键:
Object.getOwnPropertySymbols
:它会返回一个只包含 Symbol 类型的键的数组。
Reflect.ownKeys(obj)
,它会返回 所有 键。
解构赋值
当我们把它们传递给函数时,函数可能不需要整个对象/数组,而只需要其中一部分。
解构赋值 是一种特殊的语法,它使我们可以将数组或对象“拆包”至一系列变量中。有时这样做更方便。
数组解构
将数组解构到变量中的例子:
// 我们有一个存放了名字和姓氏的数组
let arr = ["John", "Smith"]
// 解构赋值
// 设置 firstName = arr[0]
// 以及 surname = arr[1]
let [firstName, surname] = arr;
alert(firstName); // John
alert(surname); // Smith
// 等价于
let firstName = arr[0];
let surname = arr[1];
// 可以通过添加额外的逗号来丢弃数组中不想要的元素:
// 不需要第二个元素:
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
当与 split
函数(或其他返回值为数组的函数)结合使用时,看起来更优雅:
let user = {};
[user.name, user.surname] = "John Smith".split(' ');
alert(user.name); // John
alert(user.surname); // Smith
等号右侧可以是任何可迭代对象
这种情况下解构赋值是通过迭代右侧的值来完成工作的。这是一种用于对在 =
右侧的值上调用 for..of
并进行赋值的操作的语法糖。
let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);
与 .entries() 方法进行循环操作
我们可以将 .entries() 方法与解构语法一同使用,来遍历一个对象的“键—值”对:
let user = {
name: "John",
age: 30
};
// 使用循环遍历键—值对
for (let [key, value] of Object.entries(user)) {
alert(`${key}:${value}`); // name:John, then age:30
}
交换变量值的技巧
这里我们创建了一个由两个变量组成的临时数组,并且立即以颠倒的顺序对其进行了解构赋值。
let guest = "Jane";
let admin = "Pete";
// 让我们来交换变量的值:使得 guest = Pete,admin = Jane
[guest, admin] = [admin, guest];
alert(`${guest} ${admin}`); // Pete Jane(成功交换!)
其余的 ‘…’
通常,如果数组比左边的列表长,那么“其余”的数组项会被省略。如果还想收集其余的数组项 ,可用 "..."
加一个参数以获取其余数组项.
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// rest 是包含从第三项开始的其余数组项的数组
alert(rest[0]); // Consul
alert(rest[1]); // of the Roman Republic
alert(rest.length); // 2
默认值
如果数组比左边的变量列表短,这里不会出现报错。缺少对应值的变量都会被赋 undefined.
如果我们想要一个“默认”值给未赋值的变量,我们可以使用 =
来提供
// 默认值
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
alert(name); // Julius(来自数组的值)
alert(surname); // Anonymous(默认值被使用了)
对象解构
解构赋值同样适用于对象。
// 基本语法
let {var1, var2} = {var1:…, var2:…}
举例:
let options = {
title: "Menu",
width: 100,
height: 200
};
let {title, width, height} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
// 改变 let {...} 中元素的顺序,等价
let {height, width, title} = { title: "Menu", height: 200, width: 100 }
等号左侧的模式(pattern)可以指定属性和变量之间的映射关系
let options = {
title: "Menu",
width: 100,
height: 200
};
// { sourceProperty: targetVariable }
let {width: w, height: h, title} = options;
// width -> w
// height -> h
// title -> title
alert(title); // Menu
alert(w); // 100
alert(h); // 200
剩余模式(pattern)“…”:类似数组
let options = {
title: "Menu",
height: 200,
width: 100
};
// title = 名为 title 的属性
// rest = 存有剩余属性的对象
let {title, ...rest} = options;
// 现在 title="Menu", rest={height: 200, width: 100}
alert(rest.height); // 200
alert(rest.width); // 100
嵌套解构
如果一个对象或数组嵌套了其他的对象和数组,我们可以在等号左侧使用更复杂的模式(pattern)来提取更深层的数据。
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
// 为了清晰起见,解构赋值语句被写成多行的形式
let {
size: { // 把 size 赋值到这里
width,
height
},
items: [item1, item2], // 把 items 赋值到这里
title = "Menu" // 在对象中不存在(使用默认值)
} = options;
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
智能函数参数
有时,一个函数有很多参数,其中大部分的参数都是可选的。对用户界面来说更是如此。想象一个创建菜单的函数。它可能具有宽度参数,高度参数,标题参数和项目列表等。可读性会变得很差。
在实际开发中,记忆如此多的参数的位置是一个很大的负担。
我们可以用一个对象来传递所有参数,而函数负责把这个对象解构成各个参数
// 我们传递一个对象给函数 let options = { title: "My menu", items: ["Item1", "Item2"] }; // ……然后函数马上把对象解构成变量 function showMenu({title = "Untitled", width = 200, height = 100, items = []}) { // title, items – 提取于 options, // width, height – 使用默认值 alert( `${title} ${width} ${height}` ); // My Menu 200 100 alert( items ); // Item1, Item2 } showMenu(options);
或
let options = { title: "My menu", items: ["Item1", "Item2"] }; function showMenu({ title = "Untitled", width: w = 100, // width goes to w height: h = 200, // height goes to h items: [item1, item2] // items first element goes to item1, second to item2 }) { alert( `${title} ${w} ${h}` ); // My Menu 100 200 alert( item1 ); // Item1 alert( item2 ); // Item2 } showMenu(options);
日期和时间
创建
调用 new Date()
来创建一个新的 Date
对象。在调用时可以带有一些参数.
-
new Date()
不带参数 —— 创建一个表示当前日期和时间的Date
对象 -
new Date(milliseconds)
创建一个Date
对象,其时间等于 1970 年 1 月 1 日 UTC+0 之后经过的毫秒数(1/1000 秒)。
传入的整数参数代表的是自 1970-01-01 00:00:00 以来经过的毫秒数,该整数被称为 时间戳。这是一种日期的轻量级数字表示形式。我们通常使用
new Date(timestamp)
通过时间戳来创建日期,并可以使用date.getTime()
将现有的Date
对象转化为时间戳 -
new Date(datestring)
只有一个参数,并且是字符串,那么它会被自动解析。该算法与Date.parse
所使用的算法相同,将在下文中进行介绍。 -
new Date(year, month, date, hours, minutes, seconds, ms)
使用当前时区中的给定组件创建日期。只有前两个参数是必须的。year
应该是四位数。为了兼容性,也接受 2 位数,并将其视为19xx
,例如98
与1998
相同,但强烈建议始终使用 4 位数。month
计数从0
(一月)开始,到11
(十二月)结束。date
是当月的具体某一天,如果缺失,则为默认值1
。- 如果
hours/minutes/seconds/ms
缺失,则均为默认值0
。
访问日期组件
从 Date
对象中访问年、月等信息有多种方式:
getFullYear()
获取年份(4 位数)getMonth()
获取月份,从 0 到 11。getDate()
获取当月的具体日期,从 1 到 31,这个方法名称可能看起来有些令人疑惑。getHours()
,getMinutes()
,getSeconds()
,getMilliseconds()
获取相应的时间组件。getDay()
获取一周中的第几天,从 0(星期日)到 6(星期六)。第一天始终是星期日,在某些国家可能不是这样的习惯,但是这不能被改变。- 当然,也有与当地时区的 UTC 对应项.
它们会返回基于 UTC+0 时区的日、月、年等:getUTCFullYear(),getUTCMonth(),getUTCDay()。
只需要在"get"
之后插入"UTC"
即可。
getTimezoneOffset() 返回 UTC 与本地时区之间的时差,以分钟为单位
设置日期组件
setFullYear(year, [month\], [date])
setMonth(month, [date\])
setDate(date)
setHours(hour, [min\], [sec], [ms])
setMinutes(min, [sec\], [ms])
setSeconds(sec, [ms\])
setMilliseconds(ms)
setTime(milliseconds)
(使用自 1970-01-01 00:00:00 UTC+0 以来的毫秒数来设置整个日期)
以上方法除了 setTime()
都有 UTC 变体,例如:setUTCHours()
。
自动校准(Autocorrection)
自动校准 是 Date 对象的一个非常方便的特性。我们可以设置超范围的数值,它会自动校准。
let date = new Date(2013, 0, 32); // 32 Jan 2013 ?!?
alert(date); // ……是 1st Feb 2013!
超出范围的日期组件将会被自动分配。
日期转化为数字,日期差值
当 Date
对象被转化为数字时,得到的是对应的时间戳,与使用 date.getTime()
的结果相同
let date = new Date();
alert(+date); // 以毫秒为单位的数值,与使用 date.getTime() 的结果相同
Date.now()
如果我们仅仅想要测量时间间隔,我们不需要 Date
对象。
有一个特殊的方法 Date.now()
,它会返回当前的时间戳。
它相当于 new Date().getTime()
,但它不会创建中间的 Date
对象。因此它更快,而且不会对垃圾回收造成额外的压力。
let start = Date.now(); // 从 1 Jan 1970 至今的时间戳
// do the job
for (let i = 0; i < 100000; i++) {
let doSomething = i * i * i;
}
let end = Date.now(); // 完成
alert( `The loop took ${end - start} ms` ); // 相减的是时间戳,而不是日期
对字符串调用 Date.parse
Date.parse(str)
方法可以从一个字符串中读取日期。
字符串的格式应该为:YYYY-MM-DDTHH:mm:ss.sssZ
,其中:
YYYY-MM-DD
—— 日期:年-月-日。- 字符
"T"
是一个分隔符。 HH:mm:ss.sss
—— 时间:小时,分钟,秒,毫秒。- 可选字符
'Z'
为+-hh:mm
格式的时区。单个字符Z
代表 UTC+0 时区。
简短形式也是可以的,比如 YYYY-MM-DD
或 YYYY-MM
,甚至可以是 YYYY
。
Date.parse(str)
调用会解析给定格式的字符串,并返回时间戳(自 1970-01-01 00:00:00 起所经过的毫秒数)。如果给定字符串的格式不正确,则返回 NaN
。
let ms = Date.parse('2012-01-26T13:51:50.417-07:00');
alert(ms); // 1327611110417 (时间戳)
// 可以通过时间戳来立即创建一个 new Date 对象
let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') );
alert(date);
JSON 方法
JSON
(JavaScript Object Notation)是表示值和对象的通用格式。在 RFC 4627 标准中有对其的描述。最初它是为 JavaScript 而创建的,但许多其他编程语言也有用于处理它的库。因此,当客户端使用 JavaScript 而服务器端是使用 Ruby/PHP/Java 等语言编写的时,使用 JSON 可以很容易地进行数据交换。
问题引入
假设我们有一个复杂的对象,我们希望将其转换为字符串,以通过网络发送,或者只是为了在日志中输出它。
当然,这样的字符串应该包含所有重要的属性。
// 可以像这样实现转换:
let user = {
name: "John",
age: 30,
toString() {
return `{name: "${this.name}", age: ${this.age}}`;
}
};
alert(user); // {name: "John", age: 30}
但在开发过程中,会新增一些属性,旧的属性会被重命名和删除。每次更新这种 toString
都会非常痛苦。我们可以尝试遍历其中的属性,但是如果对象很复杂,并且在属性中嵌套了对象呢?我们也需要对它们进行转换。
JSON 转换
JavaScript 提供了如下方法:
JSON.stringify
将对象转换为 JSON。JSON.parse
将 JSON 转换回对象。
JSON 支持以下数据类型:
- Objects
{ ... }
- Arrays
[ ... ]
- Primitives:
- strings,
- numbers,
- boolean values
true/false
, null
。
JSON 是语言无关的纯数据规范,因此一些特定于 JavaScript 的对象属性会被 JSON.stringify
跳过,即:
- 函数属性(方法)。
- Symbol 类型的键和值。
- 存储
undefined
的属性。
如果这不是我们想要的方式,自定义转换方式。最棒的是支持嵌套对象转换,并且可以自动对其进行转换。
let meetup = {
title: "Conference",
room: {
number: 23,
participants: ["john", "ann"]
}
};
alert( JSON.stringify(meetup) );
/* 整个结构都被字符串化了
{
"title":"Conference",
"room":{"number":23,"participants":["john","ann"]},
}
*/
重要的限制:不得有循环引用。
排除和转换:replacer
JSON.stringify
的完整语法是:
let json = JSON.stringify(value[, replacer, space])
- value 要编码的值。
- replacer 要编码的属性数组或映射函数
function(key, value)
。 - space 用于格式化的空格数量。
大部分情况,JSON.stringify
仅与第一个参数一起使用。但是,如果我们需要微调替换过程,比如过滤掉循环引用,我们可以使用 JSON.stringify
的第二个参数。
如果我们传递一个属性数组给它,那么只有这些属性会被编码。
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup 引用了 room
};
room.occupiedBy = meetup; // room 引用了 meetup
alert( JSON.stringify(meetup, ['title', 'participants']) );
// {"title":"Conference","participants":[{},{}]}
// 可能过于严格了。属性列表应用于了整个对象结构。所以 participants 是空的,因为 name 不在列表中。
// 除了会导致循环引用的 room.occupiedBy 之外的所有属性
alert( JSON.stringify(meetup, ['title', 'participants', 'place', 'name', 'number']) );
/*
{
"title":"Conference",
"participants":[{"name":"John"},{"name":"Alice"}],
"place":{"number":23}
}
*/
我们可以使用一个函数代替数组作为 replacer
。
该函数会为每个 (key,value)
对调用并返回“已替换”的值,该值将替换原有的值。如果值被跳过了,则为 undefined
。
replacer
函数会获取每个键/值对,包括嵌套对象和数组项。它被递归地应用。replacer
中的 this
的值是包含当前属性的对象。
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup 引用了 room
};
room.occupiedBy = meetup; // room 引用了 meetup
alert( JSON.stringify(meetup, function replacer(key, value) {
alert(`${key}: ${value}`);
return (key == 'occupiedBy') ? undefined : value;
}));
/* key:value pairs that come to replacer:
: [object Object]
title: Conference
participants: [object Object],[object Object]
0: [object Object]
name: John
1: [object Object]
name: Alice
place: [object Object]
number: 23
occupiedBy: [object Object]
*/
第一个调用很特别。它是使用特殊的“包装对象”制作的:{"": meetup}
。换句话说,第一个 (key, value)
对的键是空的,并且该值是整个目标对象。这就是上面的示例中第一行是 ":[object Object]"
的原因。
这个理念是为了给 replacer
提供尽可能多的功能:如果有必要,它有机会分析并替换/跳过整个对象。
格式化:space
JSON.stringify(value, replacer, spaces)
的第三个参数是用于优化格式的空格数量。
以前,所有字符串化的对象都没有缩进和额外的空格。如果我们想通过网络发送一个对象,那就没什么问题。space
参数专门用于调整出更美观的输出。
这里的 space = 2
告诉 JavaScript 在多行中显示嵌套的对象,对象内部缩进 2 个空格:
let user = {
name: "John",
age: 25,
roles: {
isAdmin: false,
isEditor: true
}
};
alert(JSON.stringify(user, null, 2));
/* 两个空格的缩进:
{
"name": "John",
"age": 25,
"roles": {
"isAdmin": false,
"isEditor": true
}
}
*/
第三个参数也可以是字符串。在这种情况下,字符串用于缩进,而不是空格的数量。
spaces
参数仅用于日志记录和美化输出。
自定义 “toJSON”
像 toString
进行字符串转换,对象也可以提供 toJSON
方法来进行 JSON 转换。如果可用,JSON.stringify
会自动调用它。
let room = {
number: 23,
toJSON() {
return this.number;
}
};
let meetup = {
title: "Conference",
room
};
alert( JSON.stringify(room) ); // 23
alert( JSON.stringify(meetup) );
/*
{
"title":"Conference",
"room": 23
}
*/
JSON.parse
解码 JSON 字符串。
let value = JSON.parse(str, [reviver]);
- str 要解析的 JSON 字符串。
- reviver 可选的函数 function(key,value),该函数将为每个
(key, value)
对调用,并可以对值进行转换。
// 字符串化数组
let numbers = "[0, 1, 2, 3]";
numbers = JSON.parse(numbers);
alert( numbers[1] ); // 1
let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
let user = JSON.parse(userData);
alert( user.friends[1] ); // 1
JSON 可能会非常复杂,对象和数组可以包含其他对象和数组。但是它们必须遵循相同的 JSON 格式。
let json = `{
name: "John", // 错误:属性名没有双引号
"surname": 'Smith', // 错误:值使用的是单引号(必须使用双引号)
'isAdmin': false // 错误:键使用的是单引号(必须使用双引号)
"birthday": new Date(2000, 2, 3), // 错误:不允许使用 "new",只能是裸值
"friends": [0,1,2,3] // 这个没问题
}`;
此外,JSON 不支持注释。向 JSON 添加注释无效。
还有另一种名为 JSON5 的格式,它允许未加引号的键,也允许注释等。但这是一个独立的库,不在语言的规范中。
使用 reviver
想象一下,我们从服务器上获得了一个字符串化的 meetup
对象。
我们需要对它进行 反序列(deserialize),把它转换回 JavaScript 对象。
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
let meetup = JSON.parse(str);
alert( meetup.date.getDate() ); // Error!
将 reviver 函数传递给 JSON.parse
作为第二个参数,该函数按照“原样”返回所有值,但是 date
会变成 Date
let str = '{"title":"Conference","date":"2017-11-30T12:00:00.000Z"}';
let meetup = JSON.parse(str, function(key, value) {
if (key == 'date') return new Date(value);
return value;
});
alert( meetup.date.getDate() ); // 现在正常运行了!