23. JS高级-ES7至ES12新特性详解合集

625 阅读49分钟

该系列文章连载于公众号coderwhy和掘金XiaoYu2002中

  • 对该系列知识感兴趣和想要一起交流的可以添加wx:coderwhy666,拉你进群参与共学计划,一起成长进步
  • 课程对照进度:JavaScript高级系列117-125集(coderwhy)
  • 后续JavaScript高级知识技术会持续更新,如果喜欢我们的文章,欢迎关注、点赞、转发、评论,大家的支持是我们最大的动力

脉络探索

  • 在前面,我们对ES6的知识就告一段落,并进行了一个简短的总结,让我们清晰的知道ES6还未结束,有些内容属于非常重要的部分,会抽离出来在后面单独开模块进行说明学习
    • 而在本章节中,我们将会一口气学ES7-ES12的常见新特性,而之后的ES13-ES15的常见新特性则会在最后进行补充
    • 在这里需要说明,ES系列每年最新更新的内容都具备一定的兼容性问题,不是所有的浏览器都支持对应新API的,往往在3年之后,对应的API才会开始逐步流行和适用起来
    • 这个时间节点往往不止在于API方面,在框架中也能够得到对应的体现,例如Vue2在2023年12月31日终止支持,这一时间节点可以与Vue3.0的发布时间(2020年9月18日)做一个比对,会发现在Vue3出现3年后,开始全面从2跨越到3版本,这个时间还是相当微妙的
  • 但不管怎么说,在JS高级文章的ES系列中,我们都会学习到最新的版本位置,只是ES13后的部分,会放在后面进行学习

一、ES7新特性

1.1 Array Includes

  • 这是一个数组方法,用于判断数组中是否包含指定的元素,并根据结果返回一个布尔值(truefalse
    • 这其实和上一章所学的has方法很像,可以印证着使用
    • 该方法一共两个参数,参数1:searchElement指要在数组中查找的元素
    • 参数2:开始查找的位置的索引。如果为负值,则表示从数组末尾开始向前计算的索引。如果计算后的索引值小于 0,则整个数组都会被搜索。默认值为 0
array.includes(searchElement[, fromIndex])
  • 在以前,是通过indexOf获取索引值的方式,如果不存在则返回 -1
    • 这种方式来进行获取,是利用了indexOf的判定特性来决定,这需要我们对indexOf的判定特性足够熟悉才能确定该做法所代表的含义,是一种取巧的方式,在做法是,是不如includes直接的,indexOf() 检查元素是否存在需要额外的比较操作
const names = ['小余', 'coderwhy']

if (names.indexOf('小余') !== -1) {
  console.log("数组存在该元素");
}

if (names.includes('coderwhy')) {
  console.log("数组存在该元素");
}
  • NaN 的比较includes() 方法可以正确检查数组中的 NaN 值,这是 indexOf() 方法无法做到的。indexOf() 不能用来查找 NaN,因为在 JavaScript 中,NaN 是不等于自身的
console.log([NaN].includes(NaN)); // 输出:true
console.log([NaN].indexOf(NaN));  // 输出:-1
  • 且从性能角度去考虑的话,如果目标对象是大数组(数据信息很多)且操作频繁,可以考虑用更高效的数据结构,比如上一章节的Set进行过滤一层,这样去除其相同的元素内容,在该基础上继续使用includes方法,并不影响我们的判断效果

    • 因为在判断数据是否存在时,不管数据是单个还是复数,都不影响
    • 虽然includes方法有针对这类操作进行优化,但依旧需要去考虑对应的性能问题
  • 想要思考对应的性能,这需要我们对这两种方法(indexOf与includes)的查找元素方式有一定的了解

    • includes() 方法的基本工作原理与 indexOf() 方法类似,但有一些关键的差异,比如在处理 NaN(Not-a-Number)值时。includes() 使用的是“SameValueZero”比较算法。这是一种在 ECMAScript 规范中定义的比较逻辑,与 ===(严格等于)运算符相似

    • 从这个算法中,能够得到两个重要信息:

    1. NaN 的处理NaN === NaN 的结果是 false,前面已经说过NaN不等于自身,而在includes方法中可以做到。这是因为 “SameValueZero” 比较算法特别处理了 NaN,做到了 NaN 可以与自身比较为 true
    2. 零的比较:在“SameValueZero”算法中,0-0 被认为是相等的,虽然在某些 JS 引擎中,它们在底层是有区别的,但效果是相同的,因此includes方法无法正确区分0和-0,在该算法的眼中,这两者是一样的
//判断通过
if ([-0].includes(0)) {
  console.log("数组存在该元素");
}

1.1.1 底层操作步骤

  • 在进行includes方法判断是否包含时,主要分为以下四个步骤:
  1. 检查起始索引:如果提供了 fromIndex 参数,并且它是负数,则从数组尾部向前计算起始位置。如果计算后的索引小于 0,则整个数组都会被搜索
  2. 遍历数组:从计算得到的起始索引开始,遍历数组的元素
  3. 应用比较算法:对于每个元素,使用“SameValueZero”算法与指定的搜索值进行比较:
    • 如果元素是 NaN,并且搜索值也是 NaN,则视为找到匹配
    • 如果元素与搜索值严格相等(考虑到 0-0 在这里被视为相等),则视为找到匹配
  4. 返回结果:如果在数组中找到匹配的元素,返回 true;如果遍历完整个数组仍未找到匹配,返回 false
  • 因此我们可以回顾性能问题,includes做出的遍历优化是可以指定对应的遍历区域,而不是每次都完整遍历

    • 这也是第一步先检查起始索引的目的,但遍历数组的方式是相同的
    • 指定遍历区域可以减少遍历范围,减少重复数据,可以二次减少遍历范围。从数据的角度出发,在处理数据前,先一步整理好数据,从而提升性能,这就是includes所做的事情
    • 从这个数据角度出发的话,数据量其实还可以进一步缩减,例如当我们找到后就停止继续遍历,而这直接使用Set中的has方法进一步操作就可以实现,在作用上has方法和includes方法是类似的
  • 我们使用以下检索数字5的案例来验证我们的想法

    • 不同的方式,适用于不同的数据场景,通过比较这些方法,我们可以看到不同情况下的性能权衡,并根据具体需求选择合适的方法

    1. 直接使用 includes:适用于小数组或对性能要求不高的场景
    2. 使用 Setincludes:适用于有大量重复元素的大数组,可以减少后续操作的数据量
    3. 进一步优化的 Set 方法:在数据量大且性能要求高的场景下更为有效,特别是当数组中很早就包含了目标元素时,可以大幅减少不必要的操作

遍历数据处理对比

图23-1 遍历数据处理对比

//假设我们有以下数组,该数组中可能包含重复的数字
const numbers = [1, 2, 3, 4, 5, 5, 6, 7, 5, 8, 9, 10, 5];

//1、正常使用 includes 确认数组是否包含数字 5
function checkWithIncludes(array, value) {
return array.includes(value);
}
console.log(checkWithIncludes(numbers, 5)); // 输出:true

// 2、提前使用 Set 处理数组后确认数组是否包含数字 5
function checkWithSetAndIncludes(array, value) {
const uniqueArray = Array.from(new Set(array));
return uniqueArray.includes(value);
}

console.log(checkWithSetAndIncludes(numbers, 5)); // 输出:true

//3、以直接在创建 Set 的同时检查元素,从而避免再次转换为数组和使用 includes
//这种方法在遍历数组的同时进行 Set 的构建和检查,可以在找到目标值时立即停止,从而提高效率
function checkDirectlyWithSet(array, value) {
const elementsSet = new Set();
for (let elem of array) {
elementsSet.add(elem);
if (elementsSet.has(value)) {
return true;
}
}
return false;
}

console.log(checkDirectlyWithSet(numbers, 5)); // 输出:true

1.2 指数运算符

  • 指数运算符是关于数学计算的一个小更新,在ES7更新的版本中,使用角度上会更加方便
    • 在ES7之前,计算数字的乘方需要通过 Math.pow 方法来完成。
    • 在ES7中,增加了 ** 运算符,可以对数字来计算乘方
  • 这个作为一个简单了解即可,该系列API或者运算符,在平时开发中较少用到,除非是与数学有关的领域中
//两个都是3的3次方
const result1 = Math.pow(3,3)
const result2 = 3 ** 3
console.log(result1,result2);//27 27

二、ES8新特性

2.1 Object values

  • 之前我们可以通过 Object.keys 获取一个对象所有的key,在ES8中提供了 Object.values 来获取所有的value值
    • 这是配套的API,通过keys方法可以联想到values方法,反之亦然
    • Object.values() 方法返回一个包含给定对象自己的可枚举属性值的数组,数组中值的排列顺序与使用 for...in 循环遍历该对象时返回的顺序相同(区别在于 for...in 循环还会枚举其原型链上的属性)
    • 可枚举属性值在我们学习属性描述符时,已经有对应实践过
const obj = {
  name:"小余",
  age:20
}
//获取对象所有key
console.log(Object.keys(obj));//[ 'name', 'age' ]
//获取对象所有值
console.log(Object.values(obj));//[ '小余', 20 ]

//也可以传入字符串,会进行拆分
console.log(Object.values("你是我的小呀小苹果"));
//['你', '是', '我', '的', '小', '呀', '小', '苹', '果']
  • 主要的应用场景在于数据处理和动态属性
    • 数据处理主要针对处理对象值,根据实际场景去进行不同处理
    • 而对于属性名是动态定义的对象,Object.values() 能快速取所有属性值,特别在处理表单字段或服务器响应数据时非常有用
// 场景一:数据处理
// 假设我们有一个对象,其中包含一些用户信息
const userInfo = {
  name: "coderwhy",
  age: 35,
};

// 使用 Object.values() 来获取所有属性值,并进行迭代处理
const userInfoValues = Object.values(userInfo);
console.log("User Info Values:", userInfoValues);  // 输出用户信息的所有值

userInfoValues.forEach(value => {
  // 对每个值进行处理,例如打印或其他逻辑
  console.log(value);
});

// 场景二:动态属性处理
// 假设我们有一个表单数据对象,属性名是动态生成的,可能来自动态表单字段
const formData = {
  ["field_" + new Date().getTime()]: "Value1",
  ["field_" + new Date().getTime() + 1]: "Value2"
};

// 使用 Object.values() 来获取所有表单的值
const formValues = Object.values(formData);
console.log("Form Values:", formValues);  // 输出所有表单字段的值

// 通常这些值会被用来验证表单数据或发送到服务器
formValues.forEach(value => {
  // 进行验证或其他逻辑处理
  console.log("Processing form value:", value);
});
  • 虽然Object.values方法大多数情况下传入对象,但实际上其他类型也可以传递进去
    • 非对象参数会强制转换为对象undefinednull不能被强制转换为对象,会立即抛出 TypeError。只有字符串可以有自己的可枚举属性,而其他所有基本类型都返回一个空数组
    • 该方法底层实现主要分五步骤:1、参数验证 2、可枚举属性检索 3、读取属性值 4、构建结果数组 5、返回数组
    • 而我们提到非对象参数的处理(强制转为对象)都是在第一步参数验证中进行的
    • 只关心对象自身的可枚举属性,不会查看对象原型链中的属性,则是在第二步可枚举属性检索中所做的事情
// 字符串具有索引作为可枚举的自有属性
console.log(Object.values("foo")); // ['f', 'o', 'o']

// 其他基本类型(除了 undefined 和 null)没有自有属性
console.log(Object.values(100)); // []
  • 虽然 Object.values() 是获取对象值的快捷方式,但使用它时也应考虑性能。在处理大型对象或在性能敏感的环境下,频繁调用 Object.values() 可能会导致性能下降,因为每次调用都需要重新遍历对象的属性
    • 这需要在对应的性能场景下考虑,因为对象属性如果不会频繁变化,相同的情况我不需要频繁调用该方法,而是调用返回的变量就行,就不需要考虑遍历多次带来的性能损耗问题
    • 所以性能方面主要考虑两点:1.对象频繁变化 2.大型对象
    • 对象如果频繁变化,可以考虑优化对象的更新机制。如果即使是单次调用,对于属性非常多的大型对象,也是一件耗时的操作,换一种数据结构比如Map或许会更高效

2.2 Object entries

  • 通过 Object.entries 可以获取到一个数组,数组中会存放可枚举属性的键值对数组
    • 这entries是上一章节Map初始值存放的格式,具备相同效果
    • 可以针对对象、数组、字符串进行操作
    • 对于数组与字符串而言,被entries方法所解析,将索引作为key(始终是字符串),内容则被拆解作为value值
const obj = {
  name:"小余",
  age:20
}

console.log(Object.entries(obj));//[ [ 'name', '小余' ], [ 'age', 20 ] ]
//应用对象中的entries
for (const emtry of Object.entries(obj)){
  const [key,value] = emtry
  console.log(key,value);//name 小余  age 20
}

for (const [key, value] of Object.entries(obj)) {
  console.log(`${key} ${value}`); 
}

Object.entries(obj).forEach(([key, value]) => {
  console.log(`${key} ${value}`); 
});

//如果是数组
console.log(Object.entries(["a","b","c","d"]));//[ [ '0', 'a' ], [ '1', 'b' ], [ '2', 'c' ], [ '3', 'd' ] ]

//如果是一个字符串
console.log(Object.entries("abc"));//[ [ '0', 'a' ], [ '1', 'b' ], [ '2', 'c' ] ]

数组与字符串在控制台效果

图23-2 数组与字符串在控制台效果

  • 而我们说到使用entires方法转为的格式与Map方法的初始值是相同的,这其实就可以进行连环操作,将对象结构转为Map结构,而这也是entries的一个应用场景
    • 这时候就可以结合我们之前的思考,当使用对象的性能效率不够高时,我们可以换一个数据结构,而entries就是两个数据结构之间的转化桥梁
const obj = { foo: "bar", baz: 42 };
const map = new Map(Object.entries(obj));
console.log(map); // Map(2) {"foo" => "bar", "baz" => 42}
  • 在 JS 中,Object.keys(), Object.values(), 和 Object.entries() 是用于访问对象属性的成套方法,它们各自返回对象中属性的键、值或键值对的数组,因此该系列结束后,我们进行一个比对总结,如下表


表22-1 访问对象键值对的三种方法总结

方法描述返回值类型示例返回值
Object.keys()返回一个数组,包含对象自身的所有可枚举属性键数组 (Array)['a', 'b', 'c']
Object.values()返回一个数组,包含对象自身的所有可枚举属性值数组 (Array)['Hello', 42, true]
Object.entries()返回一个数组,其元素是与自身可枚举属性键值对对应的数组数组的数组 (Array<Array>)[ ['a', 'Hello'], ['b', 42], ['c', true] ]

2.3 String Padding

  • 字符串填充(String Padding)主要是指两个字符串操作方法:padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的
    • 主要用在某些字符串我们需要对其进行前后的填充,来实现某种格式化效果
    • 该两种方法都是来自String原型上的方法,所以可以在任何字符串后进行.符号调用和连续链式调用
    • 两个方法的命名都有讲究,pad指padding(填充),所以padStart是填充初始位置,padEnd是填充末尾位置
//padStart:位数不够往前填充,第一个值填所需位数,第二个值填当位数不够时的填充内容
const minute = "2".padStart(2,"0")
//padEnd:位数不够往后填充
const second = "1".padEnd(2,"0")
console.log(`${minute}:${second}`);//02:10
//连续链式调用
const minute = "2".padStart(2,"0").padEnd(3,"0")//020


表22-2 字符串前后填充总结

特性/方法padStart()padEnd()
描述在字符串的开始处添加填充字符,直到字符串达到指定的长度在字符串的末尾添加填充字符,直到字符串达到指定的长度
语法str.padStart(targetLength [, padString])str.padEnd(targetLength [, padString])
参数targetLength - 目标总长度targetLength - 目标总长度
padString - (可选) 用于填充的字符串,默认为空格。padString - (可选) 用于填充的字符串,默认为空格。
返回值填充后的字符串填充后的字符串
使用场景1. 数字或时间格式化前导零1. 添加文本或符号以符合文档格式要求
2. 日志和报表中对齐文本输出2. 格式化后的字符串输出对齐
  • 同时需要注意以下几点:
    1. 如果 targetLength 小于原字符串的长度,则返回原字符串
    2. 如果 padString 为空字符串或未指定,则默认使用空格 (' ') 进行填充
    3. 这些方法不会修改原字符串,而是返回一个新的字符串
  • 我们来简单具象一个应用场景:比如需要对身份证、银行卡的前面位数进行隐藏:
const cardNumber = "110102YYYYMMDD888X"//身份证号码
const lastFourNumber = cardNumber.slice(-4)//获取身份证后四位
const finalCardNumber = lastFourNumber.padStart(cardNumber.length,"*")//要求达到身份证位要求的位数,不满足时缺少几位就往开头填入几位*
console.log(finalCardNumber);

2.4 Trailing Commas(不推荐)

  • Trailing:尾部,Trailing Commas:尾部逗号
  • 在ES8中,我们允许在函数定义和调用时多加一个逗号:
    • 但这种方式在平时并不推荐,除非我们明确后面还打算继续添加内容,否则很少使用,会给人一种未填写结束的感觉
function foo(a,b,){//函数定义多加逗号
  console.log(a+b);
}
foo(1,5);//6
foo(1,5,)//6	函数调用多加逗号

2.5 Object Descriptors

  • Object.getOwnPropertyDescriptors :前面章节已经学习过了,这里不再重复(获取一个对象其对应的描述符)
  • Async Function:async、await:放在后续Promise后进行讲解

三、ES9新特性

3.1 新增知识点

  • Async iterators:后续在迭代器中讲解(异步迭代器),抽出来单独学习
  • Object spread operators:前面讲过了,也就是展开运算符
    1. 允许在对象中复制或合并对象属性。它使用三个点 (...) 来表示对象展开,可以把一个对象的所有属性复制到另一个对象中
    2. 如果两个对象有相同的属性名称,则后面的对象中的值会覆盖前面的对象中的值
  • Promise finally:后续在Promise专题中专门讲解,Promise一开始时没有finally,是后面加上的

四、ES10新特性

4.1 flat

  • 该方法是Array原型方法,而flat这一单词可以理解为"降维"
    • flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回
    • 返回新数组,也说明了不改变原数组,属于一种复制方法,返回的是浅拷贝
//flat的使用:
//将一个数组。按照制定的深度遍历,将遍历到的元素和子数组中的元素组成一个新的数组,进行返回
const nums = [10,20,[30,40,50],[60,[70],[80,[90,100]]]]//嵌套了多层的数组

//flat里面填数字几就平坦化几层
const newNums1 = nums.flat(1)
console.log(newNums1);//(8) [10, 20, 30, 40, 50, 60, Array(1), Array(2)]
const newNums2 = nums.flat(2)
console.log(newNums2);//(9) [10, 20, 30, 40, 50, 60, 70, 80, Array(2)]
const newNums3 = nums.flat(3)
console.log(newNums3);//(10) [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
  • 在上面这个案例中,我们一共是四维数组
    • 而我们该方法的精髓叫做"降维",也能够理解为将嵌套的数组结构“拍平”,即将多层嵌套的数组合并为单层数组
    • 此方法的实现涉及到递归遍历数组的每个元素,检查这些元素是否自身也是数组,并根据方法提供的深度参数决定是否继续递归展开
const nums = 
[10, 20, 
  [30, 40, 50], 
  [60, 
    [70], 
    [80, 
      [90, 100]
    ]
  ]
]
  • 这个深度需要进行注意,首先默认值深度是1,但这不意味着默认不变,深度1是在一维数组的基础上叠加的(或者理解为一维数组是深度0),所以深度1所针对的范围是到二维数组
    • 所做的事情就是将复杂数组"拍平",深度则是拍平的效果范围,深度1会将二维数组拍成一维数组,将三维数组拍成二维数组,直接在原有维度基础上去减即可,最多拍成一维数组就不会继续变化了
const arr1 = [1, 2, [3, 4]];
arr1.flat();
// [1, 2, 3, 4]

const arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();
// [1, 2, 3, 4, [5, 6]]

const arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);
// [1, 2, 3, 4, 5, 6]
  • 而数组是分为正常数组和稀疏数组的,flat方法在深度遍历时候会删除数组中的空槽,并且删除空槽效果会作用于数组整体,与深度无关
const arr5 = [1, 2, , 4, 5];
//2-4之间的空槽被删除
console.log(arr5.flat()); // [1, 2, 4, 5]

const array = [1, , 3, ["a", , "c"]];
//1-3,a-c之间的空槽被删除
console.log(array.flat()); // [ 1, 3, "a", "c" ]

const array2 = [1, , 3, ["a", , ["d", , "e"]]];
//1-3,a-e之间的空槽被删除
console.log(array2.flat()); // [ 1, 3, "a", ["d", empty, "e"] ]
console.log(array2.flat(2)); // [ 1, 3, "a", "d", "e"]
  • 通过上面的案例规律,就更容易理解以下flat() 方法的基本工作原理是:
    1. 确定深度flat() 方法接受一个可选的深度参数,默认为 1,指定数组应展开的层数
    2. 递归遍历:递归检查数组的每一个元素,如果元素是数组且当前的递归深度未达到指定的深度,则继续递归展开该元素
    3. 合并元素:将遍历到的非数组元素按顺序合并到新的数组中
  • 这里前两者是很好理解的,主要在于第三点,遍历到的非数组元素
    • 这句话需要结合flat方法返回值的说明一起理解:所有元素与遍历到的子数组中的元素合并
    • 首先这个合并过程依旧是按顺序的,其次非数组元素是不会发生变化的
    • 因此,在非数组对象上调用flat也不是不行。我们曾经说过数组是特殊的对象(键是索引,不显示),因此直接调用数组原型方法并绑定到对象上,使对象生效,从理论上来说是可行的,让我们实践一下
const arrayLike = {
  length: 3,
  0: [1, 2],
  // 嵌套的类数组对象不会被展平
  1: { length: 2, 0: 3, 1: 4 },
  2: 5,
};
console.log(Array.prototype.flat.call(arrayLike));
//// [ 1, 2, { '0': 3, '1': 4, length: 2 }, 5 ]
  • 确实可行,把索引忽略掉,这就是一个另类的数组,因此在索引1的部分是一个实际对象,属于非数组元素,则不会继续"降维",而索引1是数组则会被降维

  • 而且需要注意的是,flat() 方法是通过读取 thislength 属性,然后访问每个整数索引,这就很有意思了,为什么不是直接迭代索引?

    • 主要考虑到几点问题,比如稀疏数组(空内容),且数组索引可以不连续。使用 length 属性和显式的索引访问可以确保所有可能存在的元素都被考虑到,包括那些未直接定义(即稀疏部分)的索引。这种方式可以避免因直接迭代可能存在的索引而跳过未定义的但计数在 length 内的元素

    • 对于稀疏数组,其实际占用的内存可能远小于其 length 属性所暗示的(暗示指:假如length属性为5,我们可能认为有5个元素,相当于暗示我们有5个元素,但稀疏数组可能有3个是空的,实际只有2个元素,小于暗示的5个)。通过迭代 length 并检查每个索引,flat() 方法可以正确处理稀疏数组中的空缺,而不需要为不存在的元素分配额外的内存或执行操作。可以优化内存使用并提高处理效率

    • 尽管 flat() 方法主要用于数组,但由于 JS 的数组实际上是特殊的对象,数组的键有可能不是整数。如果使用通常的迭代方法,如 for...infor...of,可能会意外地访问到数组对象的非整数属性,这并不是 flat() 方法的预期行为。通过索引和 length 属性,flat() 可以确保只处理数组的整数索引

    • 而且ECMAScript 标准中对于内置方法的描述通常会尽量保证它们的行为与底层数据结构的抽象层级相符。对数组的操作通常涉及到显式的索引处理,这样做可以让方法的行为与数组的定义(即一组通过整数索引访问的元素集合)保持一致

  • 因此,length属性是必须的,如果没有length属性,相当于将数组长度置为0,这种做法在以前经常用来做清空数组的操作,数组没有长度,也就取消了索引对应的数据存储空间,数据没地方去就会直接丢失,形成清空数组的效果

const arrayLike = {
  0: [1, 2],
  1: { length: 2, 0: 3, 1: 4 },
  2: 5,
};
console.log(Array.prototype.flat.call(arrayLike));//[]
//相当于以前如下代码
const arr = [1,2,3]
arr.length = 0
console.log(arr);//[]

4.1.1 length 属性的暗示意义

  • 在讲解flat时,我们说明到了length属性的暗示效果,当我们说 length 属性的值“暗示”数组大小时,实际上是指它给出了可能的最大索引值加一,而不是数组实际包含元素的准确指示。容易产生以下的误解:
    • 性能误解:可能认为遍历这样一个数组需要处理 length 数量的元素,实际上需要处理的可能远少于此
    • 内存使用误解:同样,可能会误以为数组由于其大的 length 值而消耗大量内存,实际上其内存使用可能很小

4.2 flatMap

  • flatMap() 方法对数组中的每个元素应用给定的回调函数,然后将结果展开一级,返回一个新数组
    • 相当于map方法和flat方法的结合体:arr.map(...args).flat(),但比分别调用这两个方法稍微更高效一些
    • 而flatMap方法有两个参数,其中第一参数是回调函数,还有三个子参数,元素本身、索引、当前数组。这三个参数是有强联系的,就像我们曾经数组的keys、values和entries三个方法一样,元素本身(value)加索引(key)就等于数组,而元素本身是用得最多的,加上索引只有建立在和元素本身进行关联才有意义,因此在元素本身是回调函数的子参数第一位,索引第二位。这种参数做法在数组方法中非常常见(有意为之的),因此学习所有数组方法时,并不会在这方面产生困难
    • 第二参数是用于绑定this的,这在很多方法的尾部参数都是有的,属于常规做法
flatMap(callbackFn, thisArg)
  • flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组
    • 注意一:flatMap是先进行map(映射)操作,再做flat的操作
    • 注意二:flatMap中的flat相当于深度为1
    • 注意三:callbackFn 不会被源数组中的空槽调用,因为 map() 不会调用,而 flat() 将忽略返回数组中的空槽
//flotMap的使用:
//1.对数组中每一个元素应用一次传入的map对应的函数
//实现将"Hello World"拆分成"Hello"跟"World"的效果
const message = [
  "Hello World",
  "Hello 小余",
  "出来了,一只coderwhy"
]


//自己的方式完成需求:for循环完成
const newInfos = []
for(const item of message){
  const infos = item.split(" ")//从空格处做出切割
  for(const item1 of infos){
    newInfos.push(item1)
  }
}
console.log(newInfos);//['Hello', 'World', 'Hello', '小余', '出来了,一只coderwhy']

//方法2:先map后flat操作
const newMessage = message.map(item => item.split(" "))
console.log(newMessage);//会形成二维数组,数组里面嵌套一层
const finalMessage = newMessage.flat(1)//通过flat进行平坦化
console.log(finalMessage);//['Hello', 'World', 'Hello', '小余', '出来了,一只coderwhy']

//方法3:flatMap,一步到位
const newMessage1 = message.flatMap(item => item.split(" "))
console.log(newMessage1);//['Hello', 'World', 'Hello', '小余', '出来了,一只coderwhy']
  • 但对此我们其实是有一个疑惑的,就是flatMap为什么只能固定深度1,而不去像flat那样去定制呢?多传一个参数不就可以了吗?
    • 当然是没有这么简单的,首先flatMap本身就是复合API的效果(flat+Map),复杂度已经有一定的提升,如果再继续可以动态调整深度,API的复杂度会很高,而flatMap方法默认的展开一层再多数情况已经足够使用了。对于flatMap的考虑,更多是让他成为一个专注于特定任务的工具,方法的行为更容易进行预测
    • 而且真的需要继续叠加深度的话,链式调用就OK了,flatMap(xxx).flat(叠加深度),所以为了让flatMap更万金油一点点而提升极大复杂度是很没必要的事情
    • 并且提供固定深度的展平在内部实现时更容易针对优化。由于 flatMap() 只需要处理一层展平,它可以被优化来更快地执行这一特定任务,而不必处理更复杂的动态深度展平逻辑(迭代)。这可以在处理大数组时提供性能上的优势(迭代需谨慎,小心爆栈)
  • 通过flatMap连续调用或者与flat结合调用,能够实现深度的扁平化,获取自己所需求的数据信息,从而避免for循环的多层调用和代码臃肿
let arr = [
  {
    a:[{asub:1},{asub:2}],
    b:2
  },
  {
    a:[{asub:3}],
    b:2
  }
]

const result = arr.flatMap(item => item.a.flatMap(subItem => subItem));
console.log(result);//[ { asub: 1 }, { asub: 2 }, { asub: 3 } ]
  • 因此flatMap() 设计将 flat 部分的深度固定为 1,主要是为了满足大多数情况下的需求,同时保持方法的简单性和高效性。在取舍上,过于复杂的API会带来很高的心智负担,是最容易被pass的做法,从设计模式角度来说,通常称为单一职责

4.2.1 flat应用场景

  • flatMap与flat应用场景相似,有一些操作是会增加数组层数,而flatMap再进行Map操作时,可以将因操作产生的层数"拍"回原型
    • 当我们使用split方法对字符串进行拆分时,会用数组进行包裹这拆开的多个字符串,而在此基础上,一维数组就会变成二维数组,此时就有可能需要使用flatMap进行拍平操作,会比使用Map再叠加flat更方便一些
const messages = ['Hello World', 'Hello XiaoYu', 'my name is coderwhy']

//普通写法
for(const msg in messages) {
  const msgs = msg.split(' ')
  for(){
    //...
  }
}

//正常操作导致增加数组层数
const wordsMap = messages.map(item => {
  return item.split(' ')
})

console.log(wordsMap);
// [
//   [ 'Hello', 'World' ],
//   [ 'Hello', 'XiaoYu' ],
//   [ 'my', 'name', 'is', 'coderwhy' ]
// ]

//flatMap应用
const words = messages.flatMap(item => {
  return item.split(' ')
})

console.log(words);
// [
//   'Hello', 'World',
//   'Hello', 'XiaoYu',
//   'my',    'name',
//   'is',    'coderwhy'
// ]

4.3 Object fromEntries

  • 在之前,我们可以通过 Object.entries 将一个对象转换成 entries
  • 那么如果我们有一个entries了,如何将其转换成对象呢?
    • ES10提供了 Object.fromEntries来完成转换:将键值对列表转换为一个对象
    • fromEntries方法所需要填入的参数严谨是说是可迭代对象,而数组恰好属于该范围内
const obj = {
  name:'小余',
  age:18,
  height:1.88
}
//obj对象转化成entries
const entries = Object.entries(obj)
console.log(entries);//[ [ 'name', '小余' ], [ 'age', 18 ], [ 'height', 1.88 ] ]

//entries转回对象  普通做法
const newObj = {}
for(const entry of entries) {
  newObj[entry[0]] = entry[1]
}
console.log(newObj);//{ name: '小余', age: 18, height: 1.88 }

//entries转回对象  ES10做法
const info = Object.fromEntries(entries)
console.log(info);//{ name: '小余', age: 18, height: 1.88 }
  • 那么这个方法有什么应用场景呢?
    • 我们在实际项目中会发起网络请求,去请求对应的数据信息
    • 比如在这样的一个地址:juejin.cn/search?quer…
    • ?的后面内容,也就是query=coderwhy&type=1,我们通常称为query请求参数,这是一个用于查询的字符串,通常都是将这些内容传递给服务器,然后服务器传递数据回前端,进行展示界面
    • 但在前端的一些情况下,我们也是要对这样的内容做解析的,比如在Vue路由传递参数时,就可以传递query参数数据,但传递过去后,我们要怎么将这段内容解析为结构清晰的对象数据?
    • 这就需要在使用URLSearchParams的基础上,使用fromEntries将entires数据转为对象数据
//先使用正则匹配或者split截取query参数部分
const searchSting = "?query=coderwhy&type=1"
//转为结构数据
const params = new URLSearchParams(searchSting)
console.log(params);//URLSearchParams { 'query' => 'coderwhy', 'type' => '1' },可以通过get和set获取
console.log(params.get("query"));//coderwhy
console.log(params.get("type"));//1

//上面的操作等价于
for(const item of params){
  console.log(item);//[ 'query', 'coderwhy' ] [ 'type', '1' ]
}
const paramObj = Object.fromEntries(params)
console.log(paramObj);//{ query: 'coderwhy', type: '1' }
  • 而且fromEntires这个方法很像是数组的from方法和对象的Entries方法结合体,这三者都涉及到了数组转换
    • Array.from() 主要用于生成数组
    • Object.entries() 用于从对象生成键值对数组
    • Object.fromEntries() 则将键值对数组转换回对象
  • 所以Object.fromEntries() 可以看作是 Array.from()Object.entries() 的逆向操作的结合,将 Object.entries() 的输出转换回初始对象的结构
    • 而fromEntries方法底层所做的事情也能证明这一点
      1. 首先验证传入的内容是否为可迭代对象
      2. 第二步开始创建新对象,接着遍历迭代器
      3. 在第三步遍历迭代器中有两个阶段:1、键值对格式的验证,确保每个迭代项都是一个具有两个元素的数组(或类似数组的对象)2、设置属性,使用 Object.defineProperty() 或简单的属性赋值将键值对添加到新对象上。这样做不仅添加了属性,还确保了属性的正确描述符设置(例如,确保属性是可枚举的)
  • 在这里,第二步是创建新对象,而我们知道数组的from方法是用来创建数组的,因此我们可以理解为当在对象中,from这个单词代表创建对象的意思
  • 而第三步迭代器的两阶段,则是真正开始逆转entires数据并填入第二步创建的新对象中,所以fromEntries方法名字的由来,就很容易进行理解了

4.4 trimStart trimEnd

  • 去除一个字符串首尾的空格,我们可以通过trim方法,如果单独去除前面或者后面呢?
    • ES10中给我们提供了trimStarttrimEnd,通过名字就很容易理解,分别针对前与后的两种选择,因此我们简单说明即可
const name = "  xiao yu  "
//去除首位空格
console.log(name.trim());//xiao yu
//去除前面空格
console.log(name.trimStart());//xiao yu  
//去除后面空格
console.log(name.trimEnd());//  xiao yu

4.5 ES10 其他知识点

  • Symbol description:已经写过了
  • Optional catch binding:放在后面讲解try cach(捕获异常)的位置

五、ES11新特性

5.1 BigInt

  • 在早期的JavaScript中,我们不能正确的表示过大的数字
    • 大于MAX_SAFE_INTEGER的数值,表示的可能是不正确的
    • 在这个数值极限,是JS可以安全表示的最大整数,超过此范围的整数,计算时可能导致不可预见的结果(比如精度损失),特别是在进行整数比较和位操作的时候
  • 之所以不能大于这个数字,就需要说到JS中的数字类型了,不区分整数和浮点数类型,只使用IEEE 754 双精度浮点数,这种方式所有数字都存储为 64 位浮点数
    • 1位符号位:表示数值的正负,0代表正数,1代表负数
    • 11位指数位:使用偏移量或称为指数偏移(对于双精度浮点数,该偏移量为1023)。实际的指数是存储的指数值减去1023
    • 52位尾数位:存储数字的有效数字位,但实际精度为53位,因为默认会在尾数的最前面加上一个隐含的1(对于非零数值)
  • 我们所说的最大安全整数,是指尾数部分的最大值(不考虑指数影响的情况下,尾数可以表示的最大精度)
    • 数字 2^53 在二进制中表示为 1 后面跟着 53 个 0。这个数正好超过了可以用 53 位精确表示的范围,因为它需要第 54 位来表示,因此需要-1
console.log(Number.MAX_SAFE_INTEGER)//9007199254740991
const num1 = 9007199254740991 + 1
const num2 = 9007199254740991 + 2
console.log(num1,num2);//9007199254740992 9007199254740992
//可以看到num1跟num2的值是一样的,证明了超过了这个限定最大数值就会发生不稳定错误
  • 那么ES11中,引入了新的数据类型BigInt,用于表示大的整数
    • BigInt的表示方法是在数值的后面加上n
    • n 在这里起到标识作用,明确这是一个 BigInt 类型的数字,而不是普通的 Number。主要避免类型混淆,特别是进行数值计算时,BigIntNumber 不能直接进行运算,必须显式转换。且转换需要小心,因为用到BigInt的数值都很大,转为Number数字有可能出现精度丢失的问题,所以建议仅在值可能大于 2^53 时使用 BigInt 类型,并且不在两种类型之间进行相互转换
    • 因此BigInt和Number是两个不同的类型,不能用Math对象中的方法
const bigInt = 9007199254740991n
const num1 = bigInt + 1n
const num2 = bigInt + 2n
//正确显示
console.log(num1,num2);//9007199254740992n 9007199254740993n
  • 在表达BigInt类型时,我们可以直接在数字后面加n来定义BigInt,会自动识别,也可以调用函数 BigInt()(但不包含 new 运算符)并传递一个整数值或字符串值
const theBiggestInt = 9007199254740991n;

const alsoHuge = BigInt(9007199254740991);
// ↪ 9007199254740991n
  • 使用 typeof 测试时, BigInt 对象返回 "bigint"

    • 类型与number不同是我们已经清楚的
    • 那为什么BigInt是对象,在调用时却可以不用new运算符呢?
    • 说BigInt是内置对象这是为了强调它在 JS 环境中的内置特性和功能。这种描述并不是说 BigInt 是通过 new 运算符创建的对象,而是强调它作为 JS 语言的一部分,具有内置的方法和属性

    内置对象Bigint说明

    图23-3 内置对象Bigint说明

    • 所以BigInt 是一种原始类型,它不是通过 new 关键字构造的对象,在使用方式上,和Number(),String()等方式是一样的
typeof 1n === "bigint"; // true
typeof BigInt("1") === "bigint"; // true
//使用角度上,和Number()是一样的
typeof Number("1") === "number"//true
  • 而BigInt之所以能够逃脱这个最大安全数值(正负)的限制,主要在于BigInt 在底层使用的数据结构与 Number 类型完全不同
    • BigInt 不是固定大小的数据类型。它会根据数值的大小动态地分配更多的内存来存储整数的位
    • 例如,一个非常大的 BigInt 值可能会使用比一个较小的 BigInt 值更多的内存,这种动态内存管理的做法做到了让 BigInt 理论上能够存储无限大的数
    • 但理论终归是理论,还是会受到系统内存限制的,但基本上不可能触碰到这个边界
    • 并且由于 BigInt 直接以整数形式存储每一个数位,所以带小数的运算会被取整

Bigint类型注意点

图23-4 Bigint类型注意点

- `Number类型`是固定的内存结构,有些针对该内存形式的优化方法,如Math对象方法(一系列用于数学计算的方法),则不能够作用于BigInt类型上,内存结构的不同,也让两者不能混合运算 - 这种大数字的计算通常用于处理大规模数据、高精度要求的金融计算、加密算法等场景

5.2 Nullish Coalescing Operator

  • 该英文可以翻译为空值 合并 运算,表现为代码形式是??
    • 逻辑或运算符的使用方式是一样的,不同之处在于判断逻辑的不同
    • 当左侧的操作数为 null 或者 undefined 时,返回其右侧操作数,否则返回左侧操作数
    • 逻辑或运算符判断假值,空值合并运算符判断null、undefined
leftExpr ?? rightExpr
  • 我们在21章节学习默认参数时曾经说过null与undefined很特殊,还专门说过假值的和逻辑或||,说这种判断逻辑是有缺陷的
    • 空值合并运算符是为了解决同样的问题,减少漏洞,体现其严谨
    • 最常见的用法在于提供自定义默认值,防止在null或者undefined上调用内容,从而产生报错
    • 设置默认值的场景非常广泛,包括但不限于为以下几种情况提供默认值:默认参数、对象属性、数组元素、配置对象、条件渲染、链式调用、函数返回值、异步
let info = false
info = info || "默认值"
//我们以前判断info内有没有内容通常这样判断,但是这样空字符串,0,false这些将无能为力
console.log(info);//默认值

//??空值合并运算符,更加的严谨
let info1 = false
info = info ?? "默认值"
console.log(info1);//false

5.3 Optional Chaining

  • Optional Chaining是指可选链,也是运算符的一种
    • 是ES11中新增一个特性,主要作用是让我们的代码在进行null和undefined判断时更加清晰和简洁
    • 我们可以预计的是,后续有可能会出现更多的API或者运算符在判断null与undefined上,去下功夫,从而替代曾经判断假值的那一部分API与运算符
  • 可选运算符可以让我们少if判断一次值是否为undefined或null,该用法最主要在于网络请求的数据中
    • 已知的数据传递是非常清晰的,基本上不可能出现不可预测的undefined与null
    • 而网络请求不同,这里面的不可控因素非常多,已经超出我们的掌握之外,有时候网络信号不好,就有可能请求不到数据,而没有数据,JS引擎会赋予undefined的默认值,在undefined上去调用值就会产生报错
    • 有些报错是致命的,会直接导致整个项目无法运行,但不是每一个错误都需要去try catch捕获错误原因的,这时候就可以使用可选链运算符,代码简洁且效果好
const obj = {
  name:"小余",
  friend:{
    name:"coderwhy",
    // running:function(){
    //   console.log("running");
    // }
  }
}
//在使用的时候,并不知道obj里面有没有friend这个,如果没有,obj.friend会返回undefined,在undefined上调用running就会报错
obj.friend.running();

//更严谨的做法,加入判断
if(obj.running || obj.friend.running){
  obj.friend.running()//判断后再决定调不调用,因为报错如果不try catch会导致后续代码无法执行
}

//可选链的用法(?.),注意调用下面这个的时候要把前面的两种写法注释掉
obj?.friend?.running?.()//有就执行,没有就不执行

5.4 Global This

  • 在之前我们希望获取JavaScript环境的全局对象,不同的环境获取的方式是不一样的
    • 比如在浏览器中可以通过this、window来获取
    • 比如在Node中我们需要通过global来获取
  • 在ES11中对获取全局对象进行了统一的规范:globalThis
    • 这是内置对象的一种,能够自动判断当前环境(浏览器 or Node),从而进行切换可使用的全局对象,因此我们可以放心使用,而不用担心运行环境
    • 在很多引擎中, globalThis 被认为是真实的全局对象的引用,但是在浏览器中,由于 iframe 以及跨窗口安全性的考虑,它实际引用的是真实全局对象(不可以被直接访问)的 Proxy 代理,Proxy后续我们会专门讲解,在通常的应用中,很少会涉及到代理与对象本身的区别,但是也需要加以注意
  • globalThis 的具体实现细节可能因 JavaScript 引擎而异,但基本原理是在 JavaScript 引擎的启动或初始化阶段,识别环境并将相应的全局对象赋值给 globalThis
    • 也就是环境检测、赋值操作(赋值对应的全局对象)
    • 在上面两步骤的基础上,再去封装globalThis(从而能够直接使用,无需关心背后的复杂性)和实现兼容性操作(例如不支持 globalThis 的旧环境,可能需要通过 polyfill 来提供支持)
console.log(globalThis)//统一规范
console.log(this)//浏览器
console.log(global)//Node环境

5.5 for..in标准化

  • 在ES11之前,虽然很多浏览器支持for...in来遍历对象类型,但是并没有被ECMA标准化
    • for...in是一个很早就存在的特性,但并没有一个统一的标准
    • 这可能导致在不同JS引擎中,会有一些细微的差别,比如属性遍历的确切顺序
    • 但在ES11后,被明确的标准了,从而在所有地方的表达形式都是相同的
  • for ..in标准化这只是JS走向规范和进步的一个小小体现,我们只要了解就足够
  • 从ES6开始,每年更新的主要目的,都是为了以前打补丁(更加规范),在讲解ES6到目前的ES11中,已经能够有所体现
const obj = {
  name:"小余",
  age:20
}

for(item in obj){//遍历
  console.log(item);
}

5.6 ES11 其他知识点

  • Dynamic Import:后续ES Module模块化中详细写
  • Promise.allSettled:后续讲Promise的时候详细写
  • import meta:后续ES Module模块化中详细写

六、ES12 新特性

6.1 FinalizationRegist

  • FinalizationRegistry对象可以让我们在对象被垃圾回收时请求一个回调
    • FinalizationRegistry 提供了这样的一种方法:当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调。(清理回调有时被称为 finalizer )
    • 可以通过调用register方法,注册任何我们想要清理回调的对象,传入该对象和所含的值
  • 在之前学习垃圾回收时,有说明过,将对象置为null,JS引擎并不会马上回收这块空间,而是直到下一次回收循环才进行回收内存,具体的情况和垃圾回收策略有关
    • 通过new创建FinalizationRegistry对象,然后调用register方法,从而"标记"上obj对象,当obj对象被置为null时,进行观察finalRegistry回调的触发时间,可以得出结论:垃圾回收是有延迟的,而不是立刻回收
    • 在进行测试时,需要从浏览器测试,而不是Node环境,因为Node环境的默认执行不属于"热更新",等不到生效就结束了
let obj = {name:"小余",age:20}

//当被GC(垃圾回收)的时候就会调用这里面的回调函数
const finalRegistry = new FinalizationRegistry(()=>{
  console.log("某一个对象被回收了 ");
})

//调用register(寄存器)进行注册任何我们想要清理回调的对象
finalRegistry.register(obj)//此时不会生效
//置为空,让obj在下轮中被垃圾回收掉
obj = null
//因为DC不是马上就回收的,它会在GPU空闲的时候进行回收,所以在一定间隔之后就会进行回收处理,然后触发我们的回调
  • 在使用该对象时,我们通过register方法,捕获了垃圾回收的时机
    • 通过该时机,可以在回调函数中做出一些对应的操作,但需要注意的是,垃圾回收的时机是不确定的,因此不应该依赖 FinalizationRegistry 来执行关键的程序逻辑。且过度使用 FinalizationRegistry 可能会导致性能问题
    • register方法的参数1是标记需要可能垃圾回收的对象,在这个过程中也会产生对应的引用,在学习WeakSet与WeakMap有说过,一旦目标属于引用状态,不会被回收。因此如果register方法想要实现可捕获垃圾回收的时机,则不能妨碍垃圾回收的运转,只能当作一个"看客",因此register方法的参数1也属于弱引用范畴
  • 除此之外,register方法还有参数2与参数3
    • 其参数2是heldValue(回调值),当捕获时机后,用于传入回调函数的内容
//也可以传入多个的参数来用来当register注册多了之后如何区分
let obj = {name:"小余",age:20}
let info = {name:"coderwhy",age:18}
const finalRegistry = new FinalizationRegistry((value)=>{
  console.log("某一个对象被回收了 :",value);
})
//第二个参数当在触发时会被加入到finalRegistry的回调函数里面
finalRegistry.register(obj,"小余")//某一个对象被回收了 : 小余
finalRegistry.register(info,"coderwhy")//某一个对象被回收了 : coderwhy
obj = null//将引用关联去掉,让obj下一轮被垃圾回收掉
info = null
  • 而参数3则需要与FinalizationRegistry对象的另一个原型方法进行对比使用了
    • register方法的参数3是作为一个名称标记,需要配合unregister方法使用
    • un在英文中通常表示"否定"的含义,在这里则是用于取消register方法的捕获时机效果,也被称为"注销"
  • 使用场景较为稀少,作为一个了解即可,如果想要进一步了解,需要阅读英文版本的MDN文档,中文版本无该对象的翻译
//方法1
register(target, heldValue, unregisterToken)
//方法2
unregister(unregisterToken)

6.2 WeakRefs

  • 在ES6中,我们已经掌握WeakSet与WeakMap,已经足够清晰其Weak弱引用所具备的含义
    • 相对于前两者,WeakRef会更加存粹,Ref是reference(引用)的缩写
    • 该API的主要提供一种持有对象引用的方法,我们测试需要配合FinalizationRegistry对象,从而观察数据的变化
    • WeakRef对象只有一个原型方法deref,用于返回 WeakRef 的目标对象,如果该对象已被垃圾收集,则返回undefined
  • 当new创建WeakRef对象时(必须采用new调用),初始化传入的参数是targetObject(WeakRef 要指向的目标对象 ,也称作 referent)
    • 可以看到直接打印obj1.name是undefined,因为想要获取info对象,需要在WeakRef上使用deref方法获取,再此基础上才能获取对象属性,如obj1.deref().name
let info = {name:"xiaoyu",age:20}
let obj1 = new WeakRef(info)
let obj2 = new WeakRef(info)

const finalRegistry = new FinalizationRegistry((value)=>{
  console.log("回收掉咯",value);
})

finalRegistry.register(info,"info")

//两秒后取消引用
setTimeout(()=>{
  info = null
},2000)

// setTimeout(()=>{
//   const infoRef = obj1.deref()
//   console.log(infoRef.name,infoRef.age);
// },7000)

console.log(obj1.name);//undefined
console.log(obj1.deref().name);//xiaoyu
//几秒后
//回收掉咯 info
  • 这其实在讲解WeakSet有稍微涉及到,当时通过画图说明,一个弱引用进行使用数据,一旦数据被回收,就无法继续调用,很适合用来做临时缓存数据的使用,而不用担心短期使用的数据因该引用而长期持有,占据内存
  • 通过WeakRef使用的数据,都属于弱引用,且必须通过deref方法调用才能使用

6.3 logical assignment operators

  • 在之前,我们学习了一系列的运算符,如:||逻辑或运算符??空值合并运算符&&逻辑与运算符
    • ES12中对一系列逻辑赋值运算符多出另一种使用的可行性:赋值
    • 逻辑或||进阶为逻辑或赋值||=空值合并??进阶为逻辑空赋值??=逻辑与&&进阶为逻辑与赋值&&=
    • 基础的逻辑运算符具备的判定效果为:从左到右,返回符合条件的内容,两者都不符合,则返回后者
    • 当加上赋值功能后,判定条件不变,返回值将赋值于左侧,用于设置默认值是非常不错的选择
//以前学过的赋值运算符,类似下面这种
const foo = "小余" 
// const counter = 100
// counter += 50

//逻辑赋值运算符(ES12新增的)
function bar(message){
  //以前判断传入有没有值,通常这么做
  message = message || "默认值"
  //上面这种方式已经简化了
  message ||= "默认值"
  //这个例子有更好的方式,通过前面用过的案例
  message = message ?? "默认值"//前面的方式对空字符串,0,false,之类的无能为力。这种方式则可以解决
  //那这种方式也能够简化的啦
  message ??= "默认值" 
  console.log(message);
}

//多种方式自行测试
bar("")
bar(false)
bar(0)
bar("小余666")
  • 需要注意其赋值的等价关系,我们以??和??=举例
    • 尽管看起来相似,它们的行为和结果却有重要区别
    • 错误的等价在于不管结果如何,都一定会触发一次赋值,不管 x 是否原本就是 nullundefined,都将表达式的结果赋值回 x
    • 所以会产生两种结果:x=x或者x=y,在这里x=x属于没有必要的行为。避免不必要的赋值可以提高性能,特别是当赋值涉及到复杂数据结构或者有副作用时
  • 而正确等价是"惰性的",只会在x不符合预期的情况下,才会改变为y,只在必要的情况下赋值
    • 在过往,我们有说明过数据的不可变性,那在这里为什么可以放心的覆盖对应的值?
    • 主要在于"不符合预期",数据的不可变性前提在于数据是有存在必要性,各种逻辑赋值运算符所判别的分别是假值或者null与undefined,本身就不属于需要的部分(无效值),这种改变是受控和有目的的,不是随意的或无序的,因此可以在保持程序整体可控性的前提下实行
//正确等价
x ?? (x = y);
//错误等价
x = x ?? y;

6.4 ES12其他知识点

  • Numeric Separator:写过了(数字分割符号,数字太大用_分开)
  • String.replaceAll:字符串全体替换(说是替换,但不改变原数据,而是返回新数据)
const name = "参与共学计划,一起成长进步,一起携手同行"
const newName = name.replace("共学计划","JS共学")//只会修改第一个遇到的
const newName1 = name.replaceAll("一起","共同")//会将全部都进行修改
console.log(newName);//参与JS共学,一起成长进步,一起携手同行
console.log(newName1);//参与共学计划,共同成长进步,共同携手同行

后续预告

  • 在接下来的内容中,我们将详细讲解ProxyReflect的每个捕获器和方法,以及它们如何帮助我们实现更高级的对象操作。不仅如此,我们还会探讨如何在实际项目中应用这些特性,以及它们在现代JavaScript开发中的重要性