现代JavaScript教程 读书笔记

70 阅读14分钟

本篇文章主要为了记录读现代JavaScript教程中我自己遇到的少用、晦涩的知识,目前状态为读书中,以后会持续更新,更新速度不定,可能很长

空值合并运算符 '??'

当一个值既不是 null 也不是 undefined 时,我们将其称为“已定义的(defined)”

a ?? b 的结果是:

  • 如果 a 是已定义的,**则结果为 **a
  • 如果 a 不是已定义的,则结果为 b

换句话说,如果第一个参数不是 null/undefined,则 ?? 返回第一个参数。否则,返回第二个参数。

数字转化,一元运算符 +

加号 + 应用于单个值,对数字没有任何作用。但是如果运算元不是数字,加号 + 则会将其转化为数字。

 // 对数字无效
let x = 1;
alert( +x ); // 1

let y = -2;
alert( +y ); // -2

// 转化非数字
alert( +true ); // 1
alert( +"" );   // 0

想实现两个数字字符串相加的两种方式

  let apples = "2";
  let oranges = "3";
  
  alert( apples + oranges ); // "23",二元运算符加号合并字符串
  alert( +apples + +oranges ); // 5

对象

计算属性

在对象字面量中使用方括号。这叫做 计算属性

let fruit = prompt("Which fruit to buy?", "apple"); 
let bag = { 
    [fruit]: 5, // 属性名是从 fruit 变量中得到的* 
}; 
alert( bag.apple ); // 5 如果 fruit="apple"

本质上,这跟下面的语法效果相同

let fruit = prompt("Which fruit to buy?", "apple"); 
let bag = {}; // 从 fruit 变量中获取值 
bag[fruit] = 5;

对象顺序

我们遍历一个对象,获取属性是有“特别的顺序”,整数属性会被进行排序,其他属性则按照创建的顺序显示

let codes = { 
    "49": "Germany", 
    "41": "Switzerland", 
    "44": "Great Britain", 
    // .., 
    "1": "USA" 
}; 
for(let code in codes) { 
    alert(code); 
    // 1, 41, 44, 49 
}

当数字 0 被用作对象的属性的键时,会被转换为字符串 "0"

let obj = { 
    0: "test" // 等同于 "0": "test" 
};// 都会输出相同的属性(数字 0 被转为字符串 "0") 
alert( obj["0"] ); // test 
alert( obj[0] ); // test (相同的属性)

这里的“整数属性”指的是一个可以在不做任何更改的情况下与一个整数进行相互转换的字符串。

所以,"49" 是一个整数属性名,因为我们把它转换成整数,再转换回来,它还是一样的。但是 “+49” 和 “1.2” 就不行了

// Number(...) 显式转换为数字 
// Math.trunc 是内建的去除小数部分的方法。 
alert( String(Math.trunc(Number("49"))) ); // "49",相同,整数属性
alert( String(Math.trunc(Number("+49"))) ); // "49",不同于 "+49" ⇒ 不是整数属性 
alert( String(Math.trunc(Number("1.2"))) ); // "1",不同于 "1.2" ⇒ 不是整数属性

如果属性名不是整数,那它们就按照创建时的顺序来排序

那么如果属性是整数字符串,但是又不想自动排序即只想用创建的顺序,方法就是结合+

let codes = {
    "+49": "Germany",
    "+41": "Switzerland",
    "+44": "Great Britain",
    // ..,
    "+1": "USA"
  };
  
  for(let code in codes) {
    console.log(+code); // 49, 41, 44, 1
  }

垃圾回收

js的垃圾回收是由js引擎做的,以下以Chrome V8引擎为例

可达性

简而言之,“可达”是那些以某种方式可访问或可用的值。它们一定是存储在内存中的。

标记清除算法(还有其他算法,如引用计数,参考华为云垃圾回收

定期执行以下“垃圾回收”步骤:

  • 垃圾收集器找到所有的根,并“标记”(记住)它们。
  • 然后它遍历并“标记”来自它们的所有引用。
  • 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
  • ……如此操作,直到所有可达的(从根部)引用都被访问到。
  • 没有被标记的对象都会被删除。

所以可达的就是有用的代码,不可达的就是可以被回收的垃圾

可选链?.

可选链 ?. 语法有三种形式:

  1. obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined
  2. obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined
  3. obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined

Symbol

根据规范,只有两种原始类型可以用作对象属性键:

  • 字符串类型
  • symbol 类型

否则,如果使用另一种类型,例如数字,它会被自动转换为字符串。所以 obj[1] 与 obj["1"] 相同,而 obj[true] 与 obj["true"] 相同。symbol 不会被自动转换为字符串

“symbol” 值表示唯一的标识符。

可以使用 Symbol() 来创建这种类型的值

symbol 使用在哪里

模拟场景:如果我们需要在别人文件中的对象,添加一个属性,但是又不想让别人访问到,从而受到影响,这时候我们就可以用到symbol,我们用symbol添加的属性,别人遍历也不会看到,对别人是无感知的,symbol 属性不参与 for..in 循环,Object.keys(user)也会忽略它们,但是Object.assign会同时复制字符串和 symbol 属性

let user = { // 属于文件另一个代码
    name: "John"
  };
  // 下面是我的文件中使用
  let id = Symbol("id");
  user[id] = 1;
  alert( user[id] ); // 我们可以使用 symbol 作为键来访问数据

如果我们要在对象字面量 {...} 中使用 symbol,则需要使用方括号把它括起来。

let id = Symbol("id");

let user = {
  name: "John",
  [id]: 123 // 而不是 "id":123
};

通常所有的 symbol 都是不同的,即使它们有相同的名字。但有时我们想要名字相同的 symbol 具有相同的实体,这时候可以使用全局 symbol 注册表

数字类型

toString(base)

方法 num.toString(base) 返回在给定 base 进制数字系统中 num 的字符串表示形式

let num = 255;

alert( num.toString(16) );  // ff
alert( num.toString(2) );   // 11111111

使用两个点来调用一个方法:请注意 123456..toString(36) 中的两个点不是打错了。如果我们想直接在一个数字上调用一个方法,比如上面例子中的 toString,那么我们需要在它后面放置两个点 ..

如果我们放置一个点:123456.toString(36),那么就会出现一个 error,因为 JavaScript 语法隐含了第一个点之后的部分为小数部分。如果我们再放一个点,那么 JavaScript 就知道小数部分为空,现在使用该方法。

也可以写成 (123456).toString(36)

toFixed()

数字舍入到小数点后 n 位,并以字符串形式返回结果。类似于 Math.round

let num1 = 12.34; 
alert( num1.toFixed(1) ); // "12.3"
let num = 12.36; 
alert( num.toFixed(1) ); // "12.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

所以,要想精确的四舍五入,可以将其扩大相应的倍数,然后四舍五入,最后还原

isFinite

isFinite(value) 将其参数转换为数字,如果是常规数字而不是 NaN/Infinity/-Infinity,则返回 true

有时,isFinite 被用于验证字符串值是否为常规数字:

alert( isFinite("15") ); // true
alert( isFinite("str") ); // false,因为是一个特殊的值:NaN
alert( isFinite(Infinity) ); // false,因为是一个特殊的值:Infinity

===与Object.is区别

console.log(0===-0)
console.log(NaN===NaN) // “NaN” 是独一无二的,它不等于任何东西,包括它自身
console.log(Object.is(0,-0))
console.log(Object.is(NaN,-NaN)) // 可以比较NaN

parseInt、parseFloat与Number()、+区别

使用加号 + 或 Number() 的数字转换是严格的。如果一个值不完全是一个数字,就会失败,唯一的例外是字符串开头或结尾的空格,因为它们会被忽略。:

alert( +"100px" ); // NaN

parseInt 和 parseFloat 的作用是提取出数字,它们可以从字符串中“读取”数字,直到无法读取为止。如果发生 error,则返回收集到的数字。函数 parseInt 返回一个整数,而 parseFloat 返回一个浮点数:

alert( parseInt('100px') ); // 100 
alert( parseFloat('12.5em') ); // 12.5 
alert( parseInt('12.3') ); // 12,只有整数部分被返回了 
alert( parseFloat('12.3.4') ); // 12.3,在第二个点出停止了读取

某些情况下,parseInt/parseFloat 会返回 NaN。当没有数字可读时会发生这种情况:

alert( parseInt('a123') ); // NaN,第一个符号停止了读取

parseInt(str, radix)与num.toString(base)区别

parseInt(str, radix)的第二个参数指定了数字系统的基数,因此 parseInt 还可以解析十六进制数字、二进制数字等的字符串,是将字符串 str 解析为在给定的 base 数字系统中的整数,2 ≤ base ≤ 36

num.toString(base) 将数字转换为在给定的 base 数字系统中的字符串

按位取反

对于数字,它将数字转换为 32-bit 整数(如果存在小数部分,则删除小数部分),然后对其二进制表示形式中的所有位均取反。对于 32-bit 整数,~n 等于 -(n+1)。 例如:

alert( ~2 ); // -3,和 -(2+1) 相同 
alert( ~1 ); // -2,和 -(1+1) 相同 
alert( ~0 ); // -1,和 -(0+1) 相同 
alert( ~-1 ); // 0,和 -(-1+1) 相同

所以对于indexOf()及其类似函数,找不到时返回-1,所以不能写成下面这样

let str = "Widget with id";

if (str.indexOf("Widget")) {
    alert("We found it"); // 不工作!
}

现在我们代码中会写成

let str = "Widget with id"; 
if (str.indexOf("Widget") != -1) {
    alert("We found it"); // 现在工作了! 
}

但也有人写成下面这样,利用按位取反

let str = "Widget"; 
if (~str.indexOf("Widget")) { 
    alert( 'Found it!' ); // 正常运行 
}

通常不建议以非显而易见的方式使用语言特性,但这种特殊技巧在旧代码中仍被广泛使用,所以我们应该理解它

数组类型

清空数组最简单的方法就是:arr.length = 0;

数组比较

如果 == 左右两个参数之中有一个参数是对象,另一个参数是原始类型,那么该对象将会被转换为原始类型

alert( [] == [] ); // false 
alert( [0] == [0] ); // false

alert( 0 == [] ); // true 
alert('0' == [] ); // false

前两个数组比较,由于引用不同,所以返回false

后两个我们将原始类型和数组对象进行比较。因此,数组 [] 被转换为原始类型以进行比较,被转换成了一个空字符串 ''。接下来的比较就是原始类型之间的比较,0 == '',因为 '' 被转换成了数字 0,所以第三个是true,对于第四个,数组被转换成了一个空字符串 ''之后就不会转换了,所以'0'==''是false

严格比较 === 更简单,因为它不会进行类型转换。

splice 和 slice区别

splice:arr.splice(start[, deleteCount, elem1, ..., elemN])
  • []内参数可选
  • 从索引 start 开始修改 arr
  • 删除 deleteCount 元素
  • 在当前删除位置插入 elem1, ..., elemN
  • 返回被删除的元素所组成的数组
  • 原来arr数组被修改
  • 允许负向索引,从末尾开始计算

例子:删除了 3 个元素,并用另外两个元素替换它们:原来数组被修改了

let arr = [ "I", "study", "JavaScript","right", "now"]; // 删除数组的前三项,并使用其他内容代替它们 
arr.splice(0, 3, "Let's", "dance"); 
alert( arr ) // 现在 [ "Let's", "dance", "right", "now"]

例子: splice 返回了被删除的元素所组成的数组

let arr = ["I", "study", "JavaScript", "right", "now"];
// 删除前两个元素
let removed = arr.splice(0, 2);
alert( removed ); // "I", "study" <-- 被从数组中删除了的元素

我们可以将 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])
  • 不改变原来数组,返回新数组
  • 返回一个新数组
  • 将所有从索引 start 到 end(不包括 end)的数组项复制到一个新的数组
  • start 和 end 都可以是负数,在这种情况下,从末尾计算索引
  • 不带参数地调用它:arr.slice() 会创建一个 arr 的副本

Map、Set和对象、数组区别

Map

Map 是一个带键的数据项的集合,就像一个 Object 一样,差别是

  • Map 允许任何类型的键,而对象的键是字符串,其他类型也会自动转换为字符串

Map的方法和属性如下:

  • 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.keys() —— 遍历并返回一个包含所有键的可迭代对象,
  • map.values() —— 遍历并返回一个包含所有值的可迭代对象,
  • map.entries() —— 遍历并返回一个包含所有实体 [key, value] 的可迭代对象,for..of 在默认情况下使用的就是这个。
  • Object.entries:从对象创建 Map
  • Object.fromEntries:从 Map 创建对象

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 迭代(iteration)

  • for..of 
  • forEach
  • set.keys() —— 遍历并返回一个包含所有值的可迭代对象,
  • set.values() —— 与 set.keys() 作用相同,这是为了兼容 Map
  • set.entries() —— 遍历并返回一个包含所有的实体 [value, value] 的可迭代对象,它的存在也是为了兼容 Map

WeakMap、WeakSet(弱映射和弱集合)和Map、Set区别

WeakMap

  • WeakMap 和 Map 的第一个不同点就是,WeakMap 的键必须是对象,不能是原始值
  • 当对象、数组之类的数据结构在内存中时,它们的子元素,如对象的属性、数组的元素都被认为是可达的。

例如,如果把一个对象放入到数组中,那么只要这个数组存在,那么这个对象也就存在,即使没有其他对该对象的引用。

let john = { name: "John" };

let array = [ john ];

john = null; // 覆盖引用
console.log('john', john) // null
console.log('array', array) // [ { name: 'John' } ]

类似的,如果我们使用对象作为常规 Map 的键,那么当 Map 存在时,该对象也将存在。它会占用内存,并且不会被(垃圾回收机制)回收。

let john = { name: "John" };

let map = new Map();
map.set(john, "...");

john = null; // 覆盖引用

// john 被存储在了 map 中,
// 我们可以使用 map.keys() 来获取它

WeakMap 在这方面有着根本上的不同。它不会阻止垃圾回收机制对作为键的对象(key object)的回收。

  • 如果我们在 weakMap 中使用一个对象作为键,并且没有其他对这个对象的引用 —— 该对象将会被从内存(和map)中自动清除。
let john = { name: "John" };

let weakMap = new WeakMap();
weakMap.set(john, "...");

john = null; // 覆盖引用
// john 被从内存中删除了!

WeakMap 不支持迭代以及 keys()values() 和 entries() 方法。所以没有办法获取 WeakMap 的所有键或值。

WeakMap 只有以下的方法:

  • weakMap.get(key)
  • weakMap.set(key, value)
  • weakMap.delete(key)
  • weakMap.has(key)

WeakSet

  • 与 Set 类似,但是我们只能向 WeakSet 添加对象(而不能是原始值)
  • 跟 Set 一样,WeakSet 支持 addhas 和 delete 方法,但不支持 size 和 keys(),并且不可迭代。

函数进阶内容

var 与 let/const 有两个主要的区别:

  1. var 声明的变量没有块级作用域,它们仅在当前函数内可见,或者全局可见(如果变量是在函数外声明的)。
  2. var 变量声明在函数开头就会被处理(脚本启动对应全局变量)。声明会被提升,但是赋值不会。

 Array.from(obj) 和 [...obj] 存在一个细微的差别:

  • Array.from 适用于类数组(拥有length属性,其属性(索引)为非负整数,不具有数组的所具有的方法)对象也适用于可迭代对象。
  • Spread 语法只适用于可迭代对象。

闭包

建议通读