JavaScript笔记(四)| 青训营笔记

59 阅读15分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 8 天

JavaScript数据类型

一个原始值:

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

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

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

用法演示如下:

let str = "Hello";
​
alert( str.toUpperCase() ); // HELLO

数字类型

在现代 JavaScript 中,数字(number)有两种类型:

  1. JavaScript 中的常规数字以 64 位的格式 IEEE-754 存储,也被称为“双精度浮点数”。这是我们大多数时候所使用的数字,我们将在本章中学习它们。
  2. 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 变成 33.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.10.2 这样的小数,实际上在二进制形式中是无限循环小数。

在十进制数字系统中,可以保证以 10 的整数次幂作为除数能够正常工作,但是以 3 作为除数则不能。也是同样的原因,在二进制数字系统中,可以保证以 2 的整数次幂作为除数时能够正常工作,但 1/10 就变成了一个无限循环的二进制小数。

使用二进制数字系统无法 精确 存储 0.10.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 中有三种获取字符串的方法:substringsubstrslice

  • 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)startend(不含 end允许
substring(start, end)startend(不含 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

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

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 比较慢

潜在问题存在:

  1. for..in 循环会遍历 所有属性,不仅仅是这些数字属性。

    在浏览器和其它环境中有一种称为“类数组”的对象,它们 看似是数组。也就是说,它们有 length 和索引属性,但是也可能有其它的非数字的属性和方法,这通常是我们不需要的。for..in 循环会把它们都列出来。所以如果我们需要处理类数组对象,这些“额外”的属性就会存在问题。

  2. for..in 循环适用于普通对象,并且做了对应的优化。但是不适用于数组,因此速度要慢 10-100 倍。当然即使是这样也依然非常快。只有在遇到瓶颈时可能会有问题。但是我们仍然应该了解这其中的不同。

通常来说,我们不应该用 for..in 来处理数组。

  • 循环方式

    1. for (let i = 0; i < arr.length; i++) {
        alert( arr[i] );
      }
      
    2. for (let fruit of fruits) {
        alert( fruit );
      }
      
    3. 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 设置为 0splice 方法就能够插入元素而不用删除任何元素:

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])

它会返回一个新数组,将所有从索引 startend(不包括 end)的数组项复制到一个新的数组。startend 都可以是负数,在这种情况下,从末尾计算索引。

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 的第一个值/所有值。
    • findIndexfind 类似,但返回索引而不是值。
  • 遍历元素:

    • 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

请注意,sortreversesplice 方法修改的是数组本身。

这些是最常用的方法,它们覆盖 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 对象具有两个主要属性:

  • name

    Error 名称。例如,对于一个未定义的变量,名称是 "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
}