🤔 首先思考一个问题
为什么基本数据类型也能像对象一样调用方法,例如 string 的 slice 方法
其实在需要使用时会创建提供额外功能的特殊“对象包装器”,使用后即被销毁。
“对象包装器”对于每种原始类型都是不同的,它们被称为 String、Number、Boolean、Symbol 和 BigInt。因此,它们提供了不同的方法。
下面会怎么工作
let str = "Hello";
str.test = 5;
alert(str.test);
answer
根据你是否开启了严格模式 use strict,会得到如下结果:
- undefined(非严格模式)
- 报错(严格模式)。
当访问 str 的属性时,一个“对象包装器”被创建了。
在严格模式下,向其写入内容会报错。
否则,将继续执行带有属性的操作,该对象将获得 test 属性,但是此后,“对象包装器”将消失,因此在最后一行,str 并没有该属性的踪迹。
Number
常用方法
舍入(rounding)是使用数字时最常用的操作之一。
这里有几个对数字进行舍入的内建函数,都是变成整数:
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。
如果我们想将数字舍入到小数点后 n 位
函数 toFixed(n) 将数字舍入到小数点后 n 位,并以字符串形式返回结果,与 Math.round 一样四舍五入.
不精确的计算 0.1+0.2
就像在十进制中无法精确表示 1/3, 二进制中也会出现相同的情况
使用二进制数字系统无法 精确 存储 0.1 或 0.2,就像没有办法将三分之一存储为十进制小数一样。
IEEE-754 数字格式通过将数字舍入到最接近的可能数字来解决此问题。这些舍入规则通常不允许我们看到“极小的精度损失”,但是它确实存在。
我们可以看到:
alert((0.1).toFixed(20)); // 0.10000000000000000555
当我们对两个数字进行求和时,它们的“精度损失”会叠加起来。
这就是为什么 0.1 + 0.2 不等于 0.3。
我们能解决这个问题吗?当然,最可靠的方法是借助方法 toFixed(n) 对结果进行舍入:
let sum = 0.1 + 0.2;
alert(sum.toFixed(2)); // "0.30"
🤔 思考为什么会出现这种情况
alert((1.35).toFixed(1)); // 1.4
alert((6.35).toFixed(1)); // 6.3
🚀 原因
在内部,6.35 的小数部分是一个无限的二进制。在这种情况下,它的存储会造成精度损失。
让我们来看看:
alert((6.35).toFixed(20)); // 6.34999999999999964473
精度损失可能会导致数字的增加和减小。在这种特殊的情况下,数字变小了一点,这就是它向下舍入的原因。
那么 1.35 会怎样呢?
alert((1.35).toFixed(20)); // 1.35000000000000008882
在这里,精度损失使得这个数字稍微大了一些,因此其向上舍入。
如果我们希望以正确的方式进行舍入,我们应该如何解决 6.35 的舍入问题呢?
在进行舍入前,我们应该使其更接近整数:
alert((6.35 * 10).toFixed(20)); // 63.50000000000000000000
请注意,63.5 完全没有精度损失。这是因为小数部分 0.5 实际上是 1/2。以 2 的整数次幂为分母的小数在二进制数字系统中可以被精确地表示,现在我们可以对它进行舍入:
alert(Math.round(6.35 * 10) / 10); // 6.35 -> 63.5 -> 64(rounded) -> 6.4
String
查找子字符串
str.indexOf(substr, pos)
它从给定位置 pos 开始,在 str 中查找 substr,如果没有找到,则返回 -1,否则返回匹配成功的位置。
如果我们对所有存在位置都感兴趣,可以在一个循环中使用 indexOf。每一次新的调用都发生在上一匹配位置之后:
let str = "As sly as a fox, as strong as an ox";
let target = "as"; // 这是我们要查找的目标
let pos = 0;
while (true) {
let foundPos = str.indexOf(target, pos);
if (foundPos == -1) break;
alert(`Found at ${foundPos}`);
pos = foundPos + 1; // 继续从下一个位置查找
}
🚀
str.lastIndexOf(substr, pos) 还有一个类似的方法 str.lastIndexOf(substr, position),它从字符串的末尾开始搜索到开头。
它会以相反的顺序列出这些事件。
🚀 按位(bitwise)NOT 技巧
只有当 n == -1 时,~n 才为零
let str = "Widget";
if (~str.indexOf("Widget")) {
alert( 'Found it!' ); // 正常运行
}
只要记住:if (~str.indexOf(...)) 读作 “if found”。
includes,startsWith,endsWith
更现代的方法 str.includes(substr, pos) 根据 str 中是否包含 substr 来返回 true/false。
第二个可选参数是开始搜索的起始位置
如果我们需要检测匹配,但不需要它的位置,那么这是正确的选择:
获取子字符串
方法 | 选择方式 | 负值参数处理 |
---|---|---|
slice(start, end) | 从 start 到 end (不包含 end ) | 允许 |
substring(start, end) | 从 start 到 end (不包含 end ) | 负值被视为 0 |
substr(start, length) | 从 start 开始获取长度为 length 的字符串 | 允许 start 为负数,表示从末尾开始计算 |
代理对
所有常用的字符都是一个 2 字节的代码.但 2 字节只允许 65536 个组合,这对于表示每个可能的符号是不够的。所以稀有的符号被称为“代理对”的一对 2 字节的符号编码。
这些符号的长度是 2:
alert("𝒳".length); // 2,大写数学符号 X
alert("😂".length); // 2,笑哭表情
alert("𩷶".length); // 2,罕见的中国象形文字
alert("𝒳"[0]); // 奇怪的符号……
alert("𝒳"[1]); // ……代理对的一块
请注意,代理对的各部分没有任何意义。因此,上述示例中的 alert 显示的实际上是垃圾信息。
数组
使用 “at” 获取最后一个元素
arr.at(i):
- 如果 i >= 0,则与 arr[i] 完全相同。
- 对于 i 为负数的情况,它则从数组的尾部向前数。
内部
数组是一种特殊的对象。使用方括号来访问属性 arr[0] 实际上是来自于对象的语法。它其实与 obj[key] 相同,其中 arr 是对象,而数字用作键(key)。
:rocrkt:
清空数组最简单的方法就是: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"
数组没有 Symbol.toPrimitive,也没有 valueOf,它们只能执行 toString 进行转换,所以这里 [] 就变成了一个空字符串,[1] 变成了 "1",[1,2] 变成了 "1,2"。
🤔 对象转化成原始值
alert(0 == []); // true
alert("0" == []); // false
数组方法
splice
arr.splice(start[, deleteCount, elem1, ..., elemN])
它从索引 start 开始修改 arr:删除 deleteCount 个元素并在当前位置插入 elem1, ..., elemN。最后返回被删除的元素所组成的数组。
let arr = ["I", "study", "JavaScript"];
// 从索引 2 开始
// 删除 0 个元素
// 然后插入 "complex" 和 "language"
arr.splice(2, 0, "complex", "language");
alert(arr); // "I", "study", "complex", "language", "JavaScript"
🚀 允许负向索引
在这里和其他数组方法中,负向索引都是被允许的。它们从数组末尾计算位置,如下所示:
let arr = [1, 2, 5];
// 从索引 -1(尾端前一位)
// 删除 0 个元素,
// 然后插入 3 和 4
arr.splice(-1, 0, 3, 4);
alert(arr); // 1,2,3,4,5
slice
arr.slice([start], [end]);
它会返回一个新数组,将所有从索引 start 到 end(不包括 end)的数组项复制到一个新的数组。start 和 end 都可以是负数,在这种情况下,从末尾计算索引。
它和字符串的 str.slice 方法有点像,就是把子字符串替换成子数组。
let arr = ["t", "e", "s", "t"];
alert(arr.slice(1, 3)); // e,s(复制从位置 1 到位置 3 的元素)
alert(arr.slice(-2)); // s,t(复制从位置 -2 到尾端的元素)
我们也可以不带参数地调用它:arr.slice() 会创建一个 arr 的副本。其通常用于获取副本,以进行不影响原始数组的进一步转换。
concat
创建一个新数组,其中包含来自于其他数组和其他项的值
它接受任意数量的参数 —— 数组或值都可以。
🚀 但对于类数组的处理
let arr = [1, 2];
let arrayLike = {
0: "something",
length: 1,
};
alert(arr.concat(arrayLike)); // 1,2,[object Object]
……但是,如果类数组对象具有 Symbol.isConcatSpreadable 属性,那么它就会被 concat 当作一个数组来处理:此对象中的元素将被添加:
let arr = [1, 2];
let arrayLike = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2,
};
alert(arr.concat(arrayLike)); // 1,2,something,else
Iterable object(可迭代对象)
可迭代(Iterable) 对象是数组的泛化。这个概念是说任何对象都可以被定制为可在 for..of 循环中使用的对象。
Symbol.iterator
对于普通对象不是可迭代的.但只需要实现 Symbol.iterator 就是可迭代
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 是下一个值。
🤔 尝试一下
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let start = this.from;
let last = this.to;
return {
next() {
if (start < last) {
return { done: false, value: start++ };
} else {
return { done: true };
}
},
};
},
};
// 我们希望 for..of 这样运行:
for (let num of range) {
console.log(num);
}
字符串是可迭代的
对于代理对(surrogate pairs),它也能正常工作!
let str = "𝒳😂";
for (let char of str) {
alert(char); // 𝒳,然后是 😂
}
可迭代(iterable)和类数组(array-like)
- Iterable 如上所述,是实现了 Symbol.iterator 方法的对象。
- Array-like 是有索引和 length 属性的对象,所以它们看起来很像数组
一个可迭代对象也许不是类数组对象。反之亦然,类数组对象可能不可迭代。
可以两者兼有的,例如,字符串即是可迭代的(for..of 对它们有效),又是类数组的(它们有数值索引和 length 属性)。
🤔 思考
可迭代对象和类数组对象通常都 不是数组,它们没有 push 和 pop 等方法。如果我们有一个这样的对象,并想像数组那样操作它,那就非常不方便。例如,我们想使用数组方法操作 range,应该如何实现呢?
Array.from
有一个全局方法 Array.from 可以接受一个可迭代或类数组的值,并从中获取一个“真正的”数组。然后我们就可以对其调用数组方法了。
Array.from 方法接受对象,检查它是一个可迭代对象或类数组对象,然后创建一个新数组,并将该对象的所有元素复制到这个新数组。
与 str.split 方法不同,它依赖于字符串的可迭代特性。因此,就像 for..of 一样,可以正确地处理代理对(surrogate pair)。(译注:代理对也就是 UTF-16 扩展字符。)
Map
🤔 Map 是怎么比较键的?
Map 使用 SameValueZero 算法来比较键是否相等。它和严格等于 === 差不多,但区别是 NaN 被看成是等于 NaN。所以 NaN 也可以被用作键。
这个算法不能被改变或者自定义。
Map 迭代
- map.keys() —— 遍历并返回一个包含所有键的
可迭代对象
, - map.values() —— 遍历并返回一个包含所有值的
可迭代对象
, - map.entries() —— 遍历并返回一个包含所有实体 [key, value] 的可迭代对象,for..of 在默认情况下使用的就是这个。
除此之外,Map 有内建的 forEach 方法
从对象创建 Map
当创建一个 Map 后,我们可以传入一个带有键值对的数组(或其它可迭代对象)来进行初始化,如下所示:
// 键值对 [key, value] 数组
let map = new Map([
["1", "str1"],
[1, "num1"],
[true, "bool1"],
]);
alert(map.get("1")); // str1
如果我们想从一个已有的普通对象(plain object)来创建一个 Map,那么我们可以使用内建方法 Object.entries(obj),该方法返回对象的键/值对数组,该数组格式完全按照 Map 所需的格式。
所以可以像下面这样从一个对象创建一个 Map:
let obj = {
name: "John",
age: 30,
};
let map = new Map(Object.entries(obj));
alert(map.get("name")); // John
这里,Object.entries 返回键/值对数组:[ ["name","John"], ["age", 30] ]。这就是 Map 所需要的格式。
从 Map 创建对象
我们刚刚已经学习了如何使用 Object.entries(obj) 从普通对象(plain object)创建 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
我们可以使用 Object.fromEntries 从 Map 得到一个普通对象(plain object)。
例如,我们在 Map 中存储了一些数据,但是我们需要把这些数据传给需要普通对象(plain object)的第三方代码。
我们来开始:
let map = new Map();
map.set("banana", 1);
map.set("orange", 2);
map.set("meat", 4);
let obj = Object.fromEntries(map.entries()); // 创建一个普通对象(plain object)(*)
// 完成了!
// obj = { banana: 1, orange: 2, meat: 4 }
alert(obj.orange); // 2
调用 map.entries() 将返回一个可迭代的键/值对,这刚好是 Object.fromEntries 所需要的格式。
我们可以把带 (*) 这一行写得更短:
let obj = Object.fromEntries(map); // 省掉 .entries()
上面的代码作用也是一样的,因为 Object.fromEntries 期望得到一个可迭代对象作为参数,而不一定是数组。并且 map 的标准迭代会返回跟 map.entries() 一样的键/值对。因此,我们可以获得一个普通对象(plain object),其键/值对与 map 相同。
Set
Set 迭代
我们可以使用 for..of 或 forEach 来遍历 Set:
Map 中用于迭代的方法在 Set 中也同样支持:
- set.keys() —— 遍历并返回一个包含所有值的可迭代对象,
- set.values() —— 与 set.keys() 作用相同,这是为了兼容 Map,
- set.entries() —— 遍历并返回一个包含所有的实体 [value, value] 的可迭代对象,它的存在也是为了兼容 Map
weakMap weakSet
之前提到 JavaScript 引擎在值“可达”和可能被使用时会将其保持在内存中。
当对象、数组之类的数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都被认为是可达的。
例如,如果把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用。
就像这样:
let john = { name: "John" };
let array = [john];
john = null; // 覆盖引用
// 前面由 john 所引用的那个对象被存储在了 array 中
// 所以它不会被垃圾回收机制回收
// 我们可以通过 array[0] 获取到它
🚀
WeakMap 的键必须是对象,不能是原始值
类似的,如果我们使用对象作为常规 Map 的键,那么当 Map 存在时,该对象也将存在。它会占用内存,并且不会被(垃圾回收机制)回收。
let john = { name: "John" };
let map = new Map();
map.set(john, "...");
john = null; // 覆盖引用
// john 被存储在了 map 中,
// 我们可以使用 map.keys() 来获取它
如果我们在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。
WeakMap 不支持迭代以及 keys(),values() 和 entries() 方法。所以没有办法获取 WeakMap 的所有键或值。
🚀
vue中使用它作为建立data与副作用的映射
解构
它“拆开”了数组或对象,将其中的各元素复制给一些变量。原来的数组或对象自身没有被修改。
以下两种写法是等价的
// let [firstName, surname] = arr;
let firstName = arr[0];
let surname = arr[1];
数组解构
🚀 忽略使用逗号的元素
// 不需要第二个元素
let [firstName, , title] = [
"Julius",
"Caesar",
"Consul",
"of the Roman Republic",
];
alert(title); // Consul
🚀 等号右侧可以是任何可迭代对象
我们可以将其与任何可迭代对象一起使用,而不仅限于数组:
let [a, b, c] = "abc"; // ["a", "b", "c"]
let [one, two, three] = new Set([1, 2, 3]);
🚀 赋值给等号左侧的任何内容
我们可以在等号左侧使用任何“可以被赋值的”东西。
例如,一个对象的属性:
let user = {};
[user.name, user.surname] = "John Smith".split(" ");
alert(user.name); // John
alert(user.surname); // Smith
let user = new Map();
user.set("name", "John");
user.set("age", "30");
// Map 是以 [key, value] 对的形式进行迭代的,非常便于解构
for (let [key, value] of user) {
alert(`${key}:${value}`); // name:John, then age:30
}
其余的 ‘…’
rest 的值就是数组中剩下的元素组成的数组
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
默认值
// 默认值
let [name = "Guest", surname = "Anonymous"] = ["Julius"];
alert(name); // Julius(来自数组的值)
alert(surname); // Anonymous(默认值被使用了)
对象解构
属性重命名
let options = {
title: "Menu",
};
let { width: w = 100, height: h = 200, title } = options;
alert(title); // Menu
alert(w); // 100
alert(h); // 200
使用已用的变量
在上面的示例中,变量都是在赋值中通过正确方式声明的:let {…} = {…}。当然,我们也可以使用已有的变量,而不用 let
⚠️ 不使用 let 时的陷阱
let title, width, height;
// 这一行发生了错误
{title, width, height} = {title: "Menu", width: 200, height: 100};
问题在于 JavaScript 把主代码流(即不在其他表达式中)的 {...} 当做一个代码块。这样的代码块可以用于对语句分组,如下所示:
{
// 一个代码块
let message = "Hello";
// ...
alert(message);
}
因此,这里 JavaScript 假定我们有一个代码块,这就是报错的原因。我们需要解构它。
为了告诉 JavaScript 这不是一个代码块,我们可以把整个赋值表达式用括号 (...) 包起来:
let title, width, height;
// 现在就可以了
({ title, width, height } = { title: "Menu", width: 200, height: 100 });
alert(title); // Menu
额外提一下
function foo(){
return //这里默认会加一个分号
{
a:123,
b:444
}
}
console.log(foo())
嵌套解构
如果一个对象或数组嵌套了其他的对象和数组,我们可以在等号左侧使用更复杂的模式(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
对象 options 的所有属性,除了 extra 属性在等号左侧不存在,都被赋值给了对应的变量:
最终,我们得到了 width、height、item1、item2 和具有默认值的 title 变量。
注意,size 和 items 没有对应的变量,因为我们取的是它们的内容。