这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天
JavaScript数据类型
一个原始值:
- 是原始类型中的一种值。
- 在 JavaScript 中有 7 种原始类型:
string,number,bigint,boolean,symbol,null和undefined。
“对象包装器”对于每种原始类型都是不同的,它们被称为 String、Number、Boolean、Symbol 和 BigInt。因此,它们提供了不同的方法。
例如,字符串方法 str.toUpperCase() 返回一个大写化处理的字符串。
用法演示如下:
let str = "Hello";
alert( str.toUpperCase() ); // HELLO
数字类型
在现代 JavaScript 中,数字(number)有两种类型:
- JavaScript 中的常规数字以 64 位的格式 IEEE-754 存储,也被称为“双精度浮点数”。这是我们大多数时候所使用的数字,我们将在本章中学习它们。
- BigInt 用于表示任意长度的整数。有时会需要它们,因为正如我们在前面的章节 数据类型 中提到的,常规整数不能安全地超过
(253-1)或小于-(253-1)。由于仅在少数特殊领域才会用到 BigInt,因此我们在特殊的章节 BigInt 中对其进行了介绍。
假如我们需要表示 10 亿。显然,我们可以这样写:
let billion = 1000000000;
我们也可以使用下划线 _ 作为分隔符:
let billion = 1_000_000_000;
这里的下划线 _ 扮演了“语法糖”的角色,使得数字具有更强的可读性。JavaScript 引擎会直接忽略数字之间的 _,所以 上面两个例子其实是一样的。
在 JavaScript 中,我们可以通过在数字后面附加字母 "e" 并指定零的个数来缩短数字:
请注意 123456..toString(36) 中的两个点不是打错了。如果我们想直接在一个数字上调用一个方法,比如上面例子中的 toString,那么我们需要在它后面放置两个点 ..。
如果我们放置一个点:123456.toString(36),那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。
也可以写成 (123456).toString(36)。
舍入
这里有几个对数字进行舍入的内建函数:
-
Math.floor向下舍入:
3.1变成3,-1.1变成-2。 -
Math.ceil向上舍入:
3.1变成4,-1.1变成-1。 -
Math.round向最近的整数舍入:
3.1变成3,3.6变成4,中间值3.5变成4。 -
Math.trunc(IE 浏览器不支持这个方法)移除小数点后的所有内容而没有舍入:
3.1变成3,-1.1变成-1。
有 1.2345,并且想把它舍入到小数点后两位,仅得到 1.23。
函数 toFixed(n) 将数字舍入到小数点后 n 位,并以字符串形式返回结果。
在内部,数字是以 64 位格式 IEEE-754 表示的,所以正好有 64 位可以存储一个数字:其中 52 位被用于存储这些数字,其中 11 位用于存储小数点的位置,而 1 位用于符号。
如果一个数字真的很大,则可能会溢出 64 位存储,变成一个特殊的数值 Infinity:
alert( 1e500 ); // Infinity
经常发生的是:精度损失
alert( 0.1 + 0.2 == 0.3 ); // false
一个数字以其二进制的形式存储在内存中,一个 1 和 0 的序列。但是在十进制数字系统中看起来很简单的 0.1,0.2 这样的小数,实际上在二进制形式中是无限循环小数。
在十进制数字系统中,可以保证以 10 的整数次幂作为除数能够正常工作,但是以 3 作为除数则不能。也是同样的原因,在二进制数字系统中,可以保证以 2 的整数次幂作为除数时能够正常工作,但 1/10 就变成了一个无限循环的二进制小数。
使用二进制数字系统无法 精确 存储 0.1 或 0.2,就像没有办法将三分之一存储为十进制小数一样。
我们能解决这个问题吗?当然,最可靠的方法是借助方法 toFixed(n) 对结果进行舍入:
们可以使用一元加号将其强制转换为一个数字:
isNaN(value) 将其参数转换为数字,然后测试它是否为 NaN:
alert( isNaN(NaN) ); // true
alert( isNaN("str") ); // true
为什么不能直接NaN===NaN呢,因为值 “NaN” 是独一无二的,它不等于任何东西,包括它自身:
alert( NaN === NaN ); // false
-
isFinite(value)将其参数转换为数字,如果是常规数字而不是NaN/Infinity/-Infinity,则返回true:alert( isFinite("15") ); // true alert( isFinite("str") ); // false,因为是一个特殊的值:NaN alert( isFinite(Infinity) ); // false,因为是一个特殊的值:Infinity有时
isFinite被用于验证字符串值是否为常规数字:let num = +prompt("Enter a number", ''); // 结果会是 true,除非你输入的是 Infinity、-Infinity 或不是数字 alert( isFinite(num) );
在所有数字函数中,包括
isFinite,空字符串或仅有空格的字符串均被视为0
parseInt parseFloat
可以从字符串中“读取”数字,直到无法读取为止。如果发生 error,则返回收集到的数字。函数 parseInt 返回一个整数,而 parseFloat 返回一个浮点数
某些情况下,parseInt/parseFloat 会返回 NaN。当没有数字可读时会发生这种情况:
alert( parseInt('a123') ); // NaN,第一个符号停止了读取
parseInt() 函数具有可选的第二个参数。它指定了数字系统的基数,因此 parseInt 还可以解析十六进制数字、二进制数字等的字符串:
alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255,没有 0x 仍然有效
alert( parseInt('2n9c', 36) ); // 123456
处理小数时避免使用相等性检查,这样会面临精度问题
字符串
引号
让我们回忆一下引号的种类。
字符串可以包含在单引号、双引号或反引号中:
let single = 'single-quoted';
let double = "double-quoted";
let backticks = `backticks`;
单引号和双引号基本相同。但是,反引号允许我们通过 ${…} 将任何表达式嵌入到字符串中:
function sum(a, b) {
return a + b;
}
alert(`1 + 2 = ${sum(1, 2)}.`); // 1 + 2 = 3.
特殊字符:\n ....
length 属性表示字符串长度:
alert( `My\n`.length ); // 3
注意 \n 是一个单独的“特殊”字符,所以长度确实是 3。
字符串不可更改
在 JavaScript 中,字符串不可更改。改变字符是不可能的。
我们证明一下为什么不可能:
let str = 'Hi';
str[0] = 'h'; // error
alert( str[0] ); // 无法运行
通常的解决方法是创建一个新的字符串,并将其分配给 str 而不是以前的字符串。
例如:
let str = 'Hi';
str = 'h' + str[1]; // 替换字符串
alert( str ); // hi
在接下来的章节,我们将看到更多相关示例。
改变大小写
toLowerCase() 和 toUpperCase() 方法可以改变大小写:
alert( 'Interface'.toUpperCase() ); // INTERFACE
alert( 'Interface'.toLowerCase() ); // interface
查找子字符串
第一个方法是 str.indexOf(substr, pos)。
它从给定位置 pos 开始,在 str 中查找 substr,如果没有找到,则返回 -1,否则返回匹配成功的位置。
例如:
let str = 'Widget with id';
alert( str.indexOf('Widget') ); // 0,因为 'Widget' 一开始就被找到
alert( str.indexOf('widget') ); // -1,没有找到,检索是大小写敏感的
alert( str.indexOf("id") ); // 1,"id" 在位置 1 处(……idget 和 id)
还有一个类似的方法 str.lastIndexOf(substr, position),它从字符串的末尾开始搜索到开头。
它会以相反的顺序列出这些事件。
if (str.indexOf("Widget"))
这种使用方法不对,可能返回位置为0的,那就这样:
if (str.indexOf("Widget") != -1)
按位异或
includes,startsWith,endsWith
根据 str 中是否包含 substr 来返回 true/false。
如果我们需要检测匹配,但不需要它的位置,那么这是正确的选择:
获取子字符串
JavaScript 中有三种获取字符串的方法:substring、substr 和 slice。
-
str.slice(start [, end])返回字符串从
start到(但不包括)end的部分。例如:let str = "stringify"; alert( str.slice(0, 5) ); // 'strin',从 0 到 5 的子字符串(不包括 5) alert( str.slice(0, 1) ); // 's',从 0 到 1,但不包括 1,所以只有在 0 处的字符
| 方法 | 选择方式…… | 负值参数 |
|---|---|---|
slice(start, end) | 从 start 到 end(不含 end) | 允许 |
substring(start, end) | 从 start 到 end(不含 end) | 负值被视为 0 |
substr(start, length) | 从 start 开始获取长为 length 的字符串 | 允许 start 为负数 |
数组
创建一个空数组
let arr = [];
数组可以存储任何类型的元素。
let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];
获取最后一个元素
我们可以显式地计算最后一个元素的索引,然后访问它:fruits[fruits.length - 1]。
有一个更简短的语法 fruits.at(-1):
pop/push, shift/unshift 方法
队列(queue)是最常见的使用数组的方法之一。在计算机科学中,这表示支持两个操作的一个有序元素的集合:
push在末端添加一个元素.shift取出队列首端的一个元素,整个队列往前移,这样原先排第二的元素现在排在了第一。
数组还有另一个用例,就是数据结构 栈。
它支持两种操作:
push在末端添加一个元素.pop从末端取出一个元素.
JavaScript 中的数组既可以用作队列,也可以用作栈。它们允许你从首端/末端来添加/删除元素。
这在计算机科学中,允许这样的操作的数据结构被称为 双端队列(deque)。
fruits.pop() 和 fruits.at(-1) 都返回数组的最后一个元素,但 fruits.pop() 同时也删除了数组的最后一个元素,进而修改了原数组。
作用于数组首端的方法:
-
shift取出数组的第一个元素并返回它:
let fruits = ["Apple", "Orange", "Pear"]; alert( fruits.shift() ); // 移除 Apple 然后 alert 显示出来 alert( fruits ); // Orange, Pear -
unshift在数组的首端添加元素:
let fruits = ["Orange", "Pear"]; fruits.unshift('Apple'); alert( fruits ); // Apple, Orange, Pear
push 和 unshift 方法都可以一次添加多个元素:
let fruits = ["Apple"];
fruits.push("Orange", "Peach");
fruits.unshift("Pineapple", "Lemon");
// ["Pineapple", "Lemon", "Apple", "Orange", "Peach"]
alert( fruits );
数组是一种特殊的对象。使用方括号来访问属性 arr[0] 实际上是来自于对象的语法。它其实与 obj[key] 相同,其中 arr 是对象,而数字用作键(key)。
它的行为也像对象,复制也是复制的引用。数组真正特殊的是它的内部实现,JavaScript引擎针对其的优化是基于连续存储(有序集合)的,如果像常规使用对象一样使用数组,那么那些优化就荡然无存了。
性能
push/pop 方法运行的比较快,而 shift/unshift 比较慢
潜在问题存在:
-
for..in循环会遍历 所有属性,不仅仅是这些数字属性。在浏览器和其它环境中有一种称为“类数组”的对象,它们 看似是数组。也就是说,它们有
length和索引属性,但是也可能有其它的非数字的属性和方法,这通常是我们不需要的。for..in循环会把它们都列出来。所以如果我们需要处理类数组对象,这些“额外”的属性就会存在问题。 -
for..in循环适用于普通对象,并且做了对应的优化。但是不适用于数组,因此速度要慢 10-100 倍。当然即使是这样也依然非常快。只有在遇到瓶颈时可能会有问题。但是我们仍然应该了解这其中的不同。
通常来说,我们不应该用 for..in 来处理数组。
-
循环方式
-
for (let i = 0; i < arr.length; i++) { alert( arr[i] ); } -
for (let fruit of fruits) { alert( fruit ); } -
for (let key in arr) { alert( arr[key] ); // Apple, Orange, Pear }
-
多维数组
.....
数组方法
splice
arr.splice(start[, deleteCount, elem1, ..., elemN])
arr.splice(1, 1); // 从索引 1 开始删除 1 个元素
let arr = ["I", "study", "JavaScript", "right", "now"];
// 删除数组的前三项,并使用其他内容代替它们
arr.splice(0, 3, "Let's", "dance");
alert( arr ) // 现在 ["Let's", "dance", "right", "now"]
可以将 deleteCount 设置为 0,splice 方法就能够插入元素而不用删除任何元素:
let arr = ["I", "study", "JavaScript"];
// 从索引 2 开始
// 删除 0 个元素
// 然后插入 "complex" 和 "language"
arr.splice(2, 0, "complex", "language");
alert( arr ); // "I", "study", "complex", "language", "JavaScript"
slice
arr.slice([start], [end])
它会返回一个新数组,将所有从索引 start 到 end(不包括 end)的数组项复制到一个新的数组。start 和 end 都可以是负数,在这种情况下,从末尾计算索引。
concat
创建一个新数组,其中包含来自于其他数组和其他项的值。
语法:
arr.concat(arg1, arg2...)
forEach
arr.forEach 方法允许为数组的每个元素都运行一个函数。
["Bilbo", "Gandalf", "Nazgul"].forEach((item, index, array) => {
alert(`${item} is at index ${index} in ${array}`);
});
!!!数组方法太多了! 数组方法 (javascript.info)
数组方法备忘单:
-
添加/删除元素:
push(...items)—— 向尾端添加元素,pop()—— 从尾端提取一个元素,shift()—— 从首端提取一个元素,unshift(...items)—— 向首端添加元素,splice(pos, deleteCount, ...items)—— 从pos开始删除deleteCount个元素,并插入items。slice(start, end)—— 创建一个新数组,将从索引start到索引end(但不包括end)的元素复制进去。concat(...items)—— 返回一个新数组:复制当前数组的所有元素,并向其中添加items。如果items中的任意一项是一个数组,那么就取其元素。
-
搜索元素:
indexOf/lastIndexOf(item, pos)—— 从索引pos开始搜索item,搜索到则返回该项的索引,否则返回-1。includes(value)—— 如果数组有value,则返回true,否则返回false。find/filter(func)—— 通过func过滤元素,返回使func返回true的第一个值/所有值。findIndex和find类似,但返回索引而不是值。
-
遍历元素:
forEach(func)—— 对每个元素都调用func,不返回任何内容。
-
转换数组:
map(func)—— 根据对每个元素调用func的结果创建一个新数组。sort(func)—— 对数组进行原位(in-place)排序,然后返回它。reverse()—— 原位(in-place)反转数组,然后返回它。split/join—— 将字符串转换为数组并返回。reduce/reduceRight(func, initial)—— 通过对每个元素调用func计算数组上的单个值,并在调用之间传递中间结果。
-
其他:
Array.isArray(value)检查value是否是一个数组,如果是则返回true,否则返回false。
请注意,sort,reverse 和 splice 方法修改的是数组本身。
这些是最常用的方法,它们覆盖 99% 的用例。但是还有其他几个:
Iterable object(可迭代对象)
从技术上讲,对象不是数组,而是表示某物的集合(列表,集合),for..of 是一个能够遍历它的很好的语法,因此,让我们来看看如何使其发挥作用。
为了让 range 对象可迭代(也就让 for..of 可以运行)我们需要为对象添加一个名为 Symbol.iterator 的方法(一个专门用于使对象可迭代的内建 symbol)。
这是带有注释的 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会为迭代生成值。
Map 和 Set
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—— 返回当前元素个数。
“解构”并不意味着“破坏”
这种语法被叫做“解构赋值”,是因为它“拆开”了数组或对象,将其中的各元素复制给一些变量。原来的数组或对象自身没有被修改。
换句话说,解构赋值只是写起来简洁一点。以下两种写法是等价的:
可以通过添加额外的逗号来丢弃数组中不想要的元素:
// 不需要第二个元素
let [firstName, , title] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
JSON(JavaScript Object Notation)是表示值和对象的通用格式。在 RFC 4627 标准中有对其的描述。最初它是为 JavaScript 而创建的,但许多其他编程语言也有用于处理它的库。因此,当客户端使用 JavaScript 而服务器端是使用 Ruby/PHP/Java 等语言编写的时,使用 JSON 可以很容易地进行数据交换。
JavaScript 提供了如下方法:
JSON.stringify将对象转换为 JSON。JSON.parse将 JSON 转换回对象。
习如何编写支持传入任意数量参数的函数,以及如何将数组作为参数传递给这类函数。
当在函数调用中使用 ...arr 时,它会把可迭代对象 arr “展开”到参数列表中。
以 Math.max 为例:
let arr = [3, 5, 1];
alert( Math.max(...arr) ); // 5(spread 语法把数组转换为参数列表)
对于所有内建的 error,error 对象具有两个主要属性:
-
nameError 名称。例如,对于一个未定义的变量,名称是
"ReferenceError"。 -
message关于 error 的详细文字描述。
还有其他非标准的属性在大多数环境中可用。其中被最广泛使用和支持的是:
-
stack当前的调用栈:用于调试目的的一个字符串,其中包含有关导致 error 的嵌套调用序列的信息。
例如:
try {
lalala; // error, variable is not defined!
} catch (err) {
alert(err.name); // ReferenceError
alert(err.message); // lalala is not defined
alert(err.stack); // ReferenceError: lalala is not defined at (...call stack)
// 也可以将一个 error 作为整体显示出来
// error 信息被转换为像 "name: message" 这样的字符串
alert(err); // ReferenceError: lalala is not defined
}