JavaScript数据类型学习笔记[Web开发]


JavaScript数据类型

本文为学习笔记,参考Javascript.info,菜鸟等文章编写。

跳转连接

  1. JavaScript基础知识学习笔记[Web开发]
  2. JavaScript对象学习笔记[Web开发]
  3. JavaScript数据类型学习笔记[Web开发]
  4. JavaScript函数学习笔记[Web开发]

文章目录

原始类型的方法

JavaScript 允许我们像使用对象一样使用原始类型(字符串,数字等)。JavaScript 还提供了这样的调用方法。

对象之间的关键区别

一个原始值:

  • 是原始类型中的一种值。
  • 在 JavaScript 中有 7 种原始类型:stringnumberbigintbooleansymbolnullundefined

一个对象:

  • 能够存储多个值作为属性。
  • 可以使用大括号 {} 创建对象,例如:{name: "John", age: 30}。JavaScript 中还有其他种类的对象,例如函数就是对象。
当作对象的原始类型

“对象包装器”对于每种原始类型都是不同的,它们被称为 StringNumberBooleanSymbolBigInt。因此,它们提供了不同的方法。

例如,字符串方法 str.toUpperCase() 返回一个大写化处理的字符串。

let str = "Hello";

alert( str.toUpperCase() ); // HELLO

以下是 str.toUpperCase() 中实际发生的情况:

  1. 字符串 str 是一个原始值。因此,在访问其属性时,会创建一个包含字符串字面值的特殊对象,并且具有可用的方法,例如 toUpperCase()
  2. 该方法运行并返回一个新的字符串(由 alert 显示)。
  3. 特殊对象被销毁,只留下原始值 str

所以原始类型可以提供方法,但它们依然是轻量级的。

JavaScript 引擎高度优化了这个过程。它甚至可能跳过创建额外的对象。但是它仍然必须遵守规范,并且表现得好像它创建了一样。

构造器 String/Number/Boolean 仅供内部使用

像 Java 这样的一些语言允许我们使用 new Number(1)new Boolean(false) 等语法,明确地为原始类型创建“对象包装器”。

在 JavaScript 中,由于历史原因,这也是可以的,但极其 不推荐。因为这样会出问题.

另一方面,调用不带 new(关键字)的 String/Number/Boolean 函数是可以的且有效的。它们将一个值转换为相应的类型:转成字符串、数字或布尔值(原始类型)。

例如,下面完全是有效的:

let num = Number("123"); // 将字符串转成数字

数字类型

两种类型:

  1. 常规数字 以 64 位的格式 IEEE-754 存储,也被称为“双精度浮点数”。常规整数不能安全地超过 (253-1) 或小于 -(253-1),若超出范围会造成精度问题 。
  2. BigInt 用于表示任意长度的整数。

常规数字

编写数字的更多方法

① 可以使用下划线 _ 作为分隔符

let billion = 1_000_000_000; 

这里的下划线 _ 扮演了“语法糖”的角色,使得数字具有更强的可读性。JavaScript 引擎会直接忽略数字之间的 _

② 通过在数字后面附加字母 "e" 并指定零的个数来缩短数字

let billion = 1e9;  // 10 亿,字面意思:数字 1 后面跟 9 个 0

③ 十六进制,二进制和八进制数字

十六进制 数字在 JavaScript 中被广泛用于表示颜色,编码字符以及其他许多东西。有一种较短的写方法:0x,然后是数字。(x大小写都可以)

二进制和八进制数字系统很少使用,但也支持使用 0b0o 前缀

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..9A..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.10.2 这样的小数,实际上在二进制形式中是无限循环小数。使用二进制数字系统无法 精确 存储 0.10.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是一个特殊的内建方法 ,类似于 === 对值进行比较,但它对于两种边缘情况更可靠:

  1. 它适用于 NaNObject.is(NaN, NaN) === true,这是件好事。
  2. 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.startsWithstr.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:在数组的首端添加元素

pushunshift 方法都可以一次添加多个元素

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 操作必须做三件事:

  1. 移除索引为 0 的元素。
  2. 把所有的元素向左移动,把索引 1 改成 02 改成 1 以此类推,对其重新编号。
  3. 更新 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]):返回一个新数组,将所有从索引 startend(不包括 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

调用带有空参数 ssplit(s),会将字符串拆分为字母数组

arr.join(glue)

split 相反,它会在它们之间创建一串由 glue 粘合的 arr 项。

let arr = ['Bilbo', 'Gandalf', 'Nazgul'];
let str = arr.join(';'); // 使用分号 ; 将数组粘合成字符串
alert( str ); // Bilbo;Gandalf;Nazgul
reduce/reduceRight

当我们需要遍历一个数组时 —— 我们可以使用 forEachforfor..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 是可选的最后一个参数。

几乎所有调用函数的数组方法 —— 比如 findfiltermap,除了 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)。

  1. for..of 循环启动时,它会调用这个方法(如果没找到,就会报错)。这个方法必须返回一个 迭代器(iterator) —— 一个有 next 方法的对象。
  2. 从此开始,for..of 仅适用于这个被返回的对象
  3. for..of 循环希望取得下一个数值,它就调用这个对象的 next() 方法。
  4. 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) {}

可迭代对象和类数组对象通常都 不是数组,它们没有 pushpop 等方法。如果我们有一个这样的对象,并想像数组那样操作它,那就非常不方便。

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..offorEach 来遍历 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 支持 addhasdelete 方法,但不支持 sizekeys(),并且不可迭代。

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,例如 981998 相同,但强烈建议始终使用 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-DDYYYY-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() ); // 现在正常运行了!
  • 22
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值