[TOC] #### 1. ES6 - 对象字面量增强 --- 对象字面量的增强写法主要体现在以下几个方面,以提高代码的可读性、可维护性和功能性: + 属性简写:当属性名和变量名相同时,可以省略属性名,直接写变量名 + 方法简写:可以省略 function 关键字,直接写方法名 + 计算属性名:使用表达式的结果作为对象属性的键名 当前有多个变量,想要把它们放入到对象中,并且在对象中定义一个方法,在 ES6 之前是这样写的: ```javascript const name = 'liang' const age = 20 const object = { name: name, age: age, foo: function () { } } ``` 属性简写和方法简写的使用示例: ```javascript const name = 'liang' const age = 20 const object = { // property shorthand (属性简写) name, age, // method shorthand (方法简写) foo() { } } ``` 可以使用箭头函数作为方法,这样可以更简洁地定义函数,但是要注意箭头函数中的 `this` 指向 ```javascript const object = { foo() { console.log(this) // { foo: ƒ, bar: ƒ } }, bar: () => { console.log(this) // Window { } } } ``` 计算属性名:`[]` 是计算属性名的语法,使用表达式的结果作为对象属性的键名 ```javascript const name = 'liang' const object = { // ES6 计算属性名,等价于 liang456 // 它告诉 JavaScript 引擎,属性名不是直接写死的标识符或字符串,而是要计算 [] 内部表达式的结果 [name + 456]() { console.log('ES6 的计算属性写法'); } } object[name + '123'] = function () { console.log('ES6 之前的计算属性写法'); } object.liang123() object.liang456() ``` #### 2. ES6 - 对象和数组的解构 --- ES6 解构赋值是一种强大的语法特性,它允许你从数组或对象中提取数据,并将其赋值给变量 这种语法使得从复杂的数据结构中提取数据变得更加简洁和直观,这个语法非常重要,请移步:[解构赋值用法详解](https://www.itqaq.com/index/277.html) ```javascript // 现有一个数组或对象,要将里面的多个值取出来 const arr = ['html', 'css', 'js'] // 在 ES6 之前,是这样获取的 const item1 = arr[0] const item2 = arr[1] const item3 = arr[2] // ES6 新增解构赋值,可以这样获取 const [value1, value2, value3] = arr // 对象的解构赋值 const { name, age } = { name: 'liang', age: 20 } ``` #### 3. ES6 - let / const 的基本使用 --- 在 ES5 中声明变量都是使用的 `var` 关键字,从 ES6 开始新增了两个关键字可以声明变量:`let`、`const` ```javascript // variable 变量 var a = 1 let b = 2 // constant 常量 const c = 3 ``` 通过 `let`、`const` 声明的变量不可以重复定义 ```javascript var a = 1 var a = 1 let b = 2 // let b = 2 // Uncaught SyntaxError: Identifier 'b' has already been declared const c = 3 // const c = 3 // Uncaught SyntaxError: Identifier 'c' has already been declared ``` `const` 本质上是传递的值不可以修改,如果传递的是引用类型,可以通过引用找到对象,去修改对象内部的属性 ```javascript // const 本质上是传递的值不可以修改 // 但是,如果传递的是一个引用类型(内存地址),可以通过引用找到对应的对象,去修改对象内部的属性 const obj = { name: 'liang' } obj.name = 'hello' ``` #### 4. ES6 - let / const 的作用域提升 --- let、const 和 var 的另一个重要区别是作用域提升: + var 声明的变量是会进行作用域提升的,在声明之前使用该变量不会报错,只是没有值 + 使用 let、const 声明的变量,在声明之前使用该变量会报错 ```javascript console.log(a) // undefined var a = 1 console.log(b) // Uncaught ReferenceError: Cannot access 'b' before initialization let b = 1 ``` 那么是不是意味着 `let`、`const` 声明的变量只有在代码执行阶段才会创建呢 ?(很多人以为是没有被创建的) + 事实上并不是这样的,可以看一下 ECMA262 对 let 和 const 的描述: + 这些变量会被创建在包含它们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值 对作用域提升的理解: + 在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么可以称之为作用域提升 + 在这里,它虽然被创建出来了,但是不能被访问,不能称之为作用域提升 暂时性死区: + 它表达的意思是在一个代码中,使用 let/const 声明的变量,在声明之前变量都是不可访问的 + 我们将这种现象称之为暂时性死区 #### 5. ES6 - let / const 和 window 的关系 --- Window 对象添加属性: + 全局通过 var 声明的变量,会被添加到 window 对象上,作为 window 对象的属性 + let、const 是不会给 window 上添加任何属性的 ```javascript var name = 'liang' console.log(window.name) // liang let title = 'vue' console.log(window.title) // undefined ``` #### 6. ES6 - 新增的块级作用域 --- 在 ES5 中只有两个东西会形成作用域:全局作用域、函数作用域 ```javascript // 全局作用域 var name = 'liang' // 函数作用域 function foo() { var bar = 'function' } ``` 块代码的结构: ```javascript // {} 块代码(block code) { var a = 1 } // 这里的 {} 不是块代码,这是对象字面量 var obj = { name: 'liang' } ``` 在 ES6 中新增了块级作用域,并且通过 let、const、function、class 声明的标识符是具备块级作用域限制的 我们会发现函数拥有块级作用域,但是在外面依然是可以访问的 + 这是因为引擎会对函数的声明进行特殊的处理,允许像 var 那样进行提升 ```javascript { var a = 1 let foo = 'liang' function demo() { console.log('demo') } } console.log(a) // 1(对 var 声明的变量是无效的,所以这里也能正常输出) // console.log(foo) // Uncaught ReferenceError: foo is not defined // 不同的浏览器有不同的实现(大部分浏览器为了兼容以前的代码,让 function 没有块级作用域) demo() // 此时可以正常调用(如果使用只支持 ES6 的浏览器,是无法访问的) ``` `if`、`switch`、`for` 语句都是块级作用域 ```javascript // if 语句的代码就是块级作用域 if (true) { let foo = 'foo' } console.log(foo) // Uncaught ReferenceError: foo is not defined // switch 语句的代码也是块级作用域 let color = 'red' switch (color) { case 'red': var bar = 'bar' let bgColor = '#000' } console.log(bar) console.log(bgColor) // Uncaught ReferenceError: bgColor is not defined // for 语句的代码也是块级作用域(这个场景用的最多,强烈推荐使用 let) for (var i = 0; i < 5; i++) { } console.log(i) // 5 for (let j = 0; j < 5; j++) { } console.log(j) // Uncaught ReferenceError: j is not defined ``` 我们来看一下 `for` 循环语句中,起始变量使用 `var` 声明的导致的问题:无论点击哪个按钮输出结果都是一样的 ```html <button>按钮</button> <button>按钮</button> <button>按钮</button> ``` ```javascript const btns = document.getElementsByTagName('button') // 特别注意:这里是 var 声明的起始变量 i for (var i = 0; i < btns.length; i++) { btns[i].onclick = function () { console.log('第' + i + '个按钮被点击') // 第3个按钮被点击 } } ``` 这是因为 var 声明的变量 `i` 是全局作用域,函数中没有变量 `i` 就会往上级作用域找,循环结束后 `i` 就已经变为 3 学习过块级作用域后,只需要将 `i` 的声明从 `var` 改为 `let` 即可,这就是块级作用域给我们带来的好处 ```javascript // 使用 let 声明起始变量 i,使其形成块级作用域 for (let i = 0; i < btns.length; i++) { btns[i].onclick = function () { console.log('第' + i + '个按钮被点击') } } ``` #### 7. ES6 - 模板字符串的使用 --- ES6 模板字符串是一种增强型的字符串表示方式,使用反引号包裹内容,支持在字符串中嵌入变量、表达式以及多行文本,极大的简化了字符串拼接和格式化操作。这是一个非常实用的特性,提升了的字符串处理的灵活性和代码的可读性 ```javascript // ES6 之前拼接字符串和其他标识符 const name = 'liang' const age = 20 const height = 180 console.log('my name is ' + name + ', age is ' + age + ', height is ' + height); ``` 基本语法:模板字符串通过反引号定义,可以包含变量和表达式,使用 `${}` 语法进行插入 ```javascript const string = `my name is ${name}, age is ${age + 5}, height is ${height}` ``` 多行文本:模板字符串天然支持多行文本,无需使用 `\n` 和字符串拼接,可以直接换行书写 ```plaintext const html = `<div> <button>按钮</button> <button>按钮</button> <button>按钮</button> </div>` ``` #### 8. ES6 - 标签模板字符串 --- 标签模板字符串是 ES6 中的一个高级特性,它允许你通过一个函数来处理模板字符串 这个函数接收模板字符串的各个部分作为参数,并且可以根据需要对这些部分进行处理,从而实现自定义的字符串处理逻辑 + 我们自己很少这样编写函数,但是有些框架会让我们这样调用它编写的函数(比如: React 框架) ```javascript // 当使用标签模板字符串调用函数时 // 第一个参数:依然是模版字符串中的整个字符串,只是被切成了多块,放到了一个数组中 // 第二个参数:模板字符串中的第一个 ${} // 第三个参数:模板字符串中的第二个 ${} function foo(m, n, x) { console.log({ m, n, x }); } // 普通调用 // foo('hello', 'world', '!') // 标签模板字符串调用 const name = 'liang' const age = 18 foo`hello${name}world${age}!` ``` #### 9. ES6 - 函数参数的默认值 --- 在 ES6 中,函数参数的默认值功能极大的简化了函数定义和调用过程,相比 ES5 时代的写法更加直观和安全 ```javascript // ES5 及之前给参数默认值,写起来很麻烦,并且代码的阅读性比较差,还有 bug(比如:传入一个 0) function foo(m) { // 没有传递参数 m 默认是 undefined m = m || '默认值' } foo() // ES6 可以给函数的参数设置默认值 function foo(m = '默认值') { // ... } // 对象参数和默认值以及解构 function foo({ name, age, gender = '男' } = { name: 'liang', age: 20 }) { // ... } ``` 函数的 length 属性值为其参数数量,对于有参数默认值的函数,从有默认值的参数开始,后续参数不做数量统计 ```javascript // 函数的 length 属性值为其参数数量 function foo(a, b) { } console.log(foo.length) // 2 // 对于有参数默认值的函数,从有默认值的参数开始,后续参数不做数量统计 function bar(a, b, c = 3) { } console.log(bar.length) // 2 // 实际上有默认值的参数一般都是放到最后,下面这种写法不推荐 function info(a, b = 2, c) { } console.log(info.length) // 1 (有默认值的参数后续其他参数不做数量统计) ``` #### 10. ES6 - 函数的剩余参数 --- ES6 中的剩余参数(Rest Parameter)是一种强大特性,可以将不定数量的参数放入到一个数组 ```javascript // 如果最后一个参数是以 ... 为前缀的,那么它会将剩余参数放到该参数中,并且作为一个数组 // 并且剩余参数只能放在最后一个参数(放在前面会报错) function foo(a, b, ...args) { console.log(a) // 1 console.log(b) // 2 console.log(args) // [3, 4, 5] } foo(1, 2, 3, 4, 5) ``` 那么,剩余参数和 arguments 有什么区别呢 ? + 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参 + arguments 对象并不是一个真正的数组,而剩余参数是一个真正的数组,可以进行数组的所有操作 + arguments 是早期 ECMAScript 中为了方便去获取所有的参数提供的一个数据结构,而剩余参数是 ES6 中提供的,并且希望以此来替代 arguments 的 #### 11. ES6 - 箭头函数的补充 --- 箭头函数是没有显式原型的,所以不能作为构造函数,无法通过 new 来创建对象 ```javascript const foo = () => { } console.log(foo.__proto__) // undefined new foo(); // Uncaught TypeError: foo is not a constructor ``` #### 12. ES6 - 展开语法的使用 --- 展开语法(Spread Syntax)最初在 ES6 中引入,主要用于数组操作,到了 ES9,展开语法被扩展到了对象处理 展开语法允许我们将数组或对象的元素展开为独立的元素,它使用三个 `...` 表示: + 可以在函数调用、数组构造时,将数组表达式或 string 在语法层面展开 + 还可以在构造字面量对象时,将对象表达式按 key-value 的方式展开 展开语法的场景: + 在函数调用时使用 + 在数组构造时使用 + 在构建对象字面量时,也可以使用展开运算符,这个是在 ES9 中添加的新特性 ```javascript // 1. 在函数调用时使用展开语法 function foo(a, b, c) { console.log({ a, b, c }) } foo(...['html', 'css', 'js']) // foo(...'html') // 也可以展开一个字符串 // 2. 构造数组时 const name = 'liang' const hobby = ['html', 'css'] const newArr = [...name, ...hobby] console.log(newArr) // 3. 构建对象字面量 ES9(ES2018) const info = { name: 'liang', age: 20 } const object = { title: 'profile', ...info, gender: 1 } console.log(object) ``` 展开运算符进行的是一个浅拷贝 ```javascript const info = { name: 'liang', profile: { age: 18 } } const user = { ...info } user.profile.age = 20 console.log(info.profile.age) // 20 ``` #### 13. ES6 - 二进制和八进制连接符 --- ```javascript const num1 = 100 // 十进制 // b -> binary const num2 = 0b100 // 二进制 // o -> octonary const num3 = 0o100 // 八进制 // x -> hex const num4 = 0x100 // 十六进制 console.log(num1) // 100 console.log(num2) // 4 console.log(num3) // 64 console.log(num4) // 256 // ES12(ES2011)对于大的数组,可以使用连接符 _ // 这种写法常用于提高大数字的可读性,划线 _ 作为数字分隔符,方便识别位数 const num = 10_000_000_000 console.log(num) // 10000000000 ``` #### 14. ES6 - Symbol 数据类型的使用 --- `Symbol` 是 ES6 中新增的一个基本数据类型,翻译为:符号 那么,Symbol 有什么用,为什么需要 Symbol 呢 ? + 在 ES6 之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突 + 比如: 原来有一个对象,想在其中添加一个属性和值,但是在我们不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖它内部的某个属性 ```javascript // ES6 之前,对象的属性名 key 都是字符串形式 var obj = { name: 'liang' } // 如果给 obj.name 赋值,会覆盖原来的值 obj.name = 'hello' console.log(obj.name) // hello ``` `Symbol` 就是为了解决上面的问题,用来生成一个独一无二的值 + Symbol 值是通过 Symbol() 函数来生成的,生成后可以作为属性名 + 也就是在 ES6 中,对象的属性名可以使用字符串,也可以使用 Symbol 值 Symbol 即使多次创建值,它们也是不同的:Symbol 函数执行后,每次创建出来的值都是独一无二的 我们也可以在创建 Symbol 值的时候传入一个描述 description:这个是 ES10(ES2019)新增的特性 ```javascript // Symbol() 可以生成一个独一无二的值,可以让它作为对象的 key const s1 = Symbol() const s2 = Symbol() console.log(s1 === s2) // false // 创建 Symbol 值的时候传入一个描述 const s3 = Symbol('aaa') console.log(s3.description) // aaa ``` Symbol 值作为对象的属性 key 的多种写法: ```javascript const s1 = Symbol() const s2 = Symbol() const s3 = Symbol() const s4 = Symbol() // 1. 在定义对象字面量时使用 const obj = { [s1]: '111', [s2]: '222', } // 2. 新增属性 obj[s3] = '333' // 3. Object.defineProperty 方式 Object.defineProperty(obj, s4, { enumerable: true, configurable: true, writable: true, value: '444', }) // 注意:不能通过.语法获取(obj.s4 是获取不到的,字符串 key 才能这样用) // 获取属性值的正确写法: 对象[Symblod值]() console.log(obj[s1]) // 111 ``` 使用 Symbol 作为 key,在遍历或 Object.keys 等中是获取不到这些 Symbol 值的,有专门的方法去获取 ```javascript const s1 = Symbol() const s2 = Symbol() const obj = { [s1]: '111', [s2]: '222' } // 使用 Symbol 作为 key,无法获取不到这些 Symbol 值 console.log(Object.keys(obj)) // [] console.log(Object.getOwnPropertyNames(obj)) // [] // 但是,可以使用 Object.getOwnPropertySymbols() 来获取所有 Symbol 的 key const symbolKeys = Object.getOwnPropertySymbols(obj) for (const item of symbolKeys) { console.log(obj[item]); } ``` 前面我们说过,Symbol() 返回的值都是独一无二的,那么有没有办法让它返回相同的值呢 ? `Symbol.for(key)` 是 ES6 中的一个静态方法,用于创建或查找全局注册表中的 Symbol + 如果全局注册表中已经存在一个与给定键 key 关联的 symbol, 则返回该 symbol + 如果不存在,会创建一个新的 symbol,并将其与该键关联后放入全局注册表中 ```javascript // Symbol.for(key) const s1 = Symbol.for('aaa') const s2 = Symbol.for('aaa') console.log(s1 === s2) // true // Symbol.keyFor() 用于获取全局注册表中 Symbol 对应的键名 const key = Symbol.keyFor(s1) console.log(key) // aaa ``` #### 15. ES6 - 数据结构 Set(元素不可重复) --- 在 ES6 之前,存储数据的结构主要有两种:数组、对象 + 在 ES6 中新增了另外两种数据结构:Set、Map,以及它们的另外形式:WeakSet、WeakMap Set 是一个新增的数据结构,可以用来保存数据,类似于数组,但是和数组的区别是 Set 的元素不能重复 + 创建 Set 我们需要通过 Set 构造函数(暂时没有字面量创建的方式) + 因为 Set 中存放的元素是不会重复的,那么它有一个很常用功能,就是数组去重 ```javascript // 通过构造函数创建 Set 结构 const set = new Set() // set.add 向 Set 结构中添加元素 set.add(10) set.add(20) set.add(30) set.add(30) // 不会被重复添加,因为结构中已有该元素 // 添加对象时,要注意,下面这样可以被重复添加,因为它们引用地址是不同的 set.add({}) set.add({}) console.log(set); ``` 因为 Set 结构具有元素不会重复的特性,所以我们可以用于 `数组去重` ```javascript // 数组去重 const arr = [32, 10, 25, 10, 32, 15] // 传统方式 const newArr = [] arr.forEach(item => { if (newArr.indexOf(item) === -1) newArr.push(item); }) console.log(newArr) // 利用 set 的特性去重 const arrSet = new Set(arr) // const newArr2 = Array.from(arrSet) const newArr3 = [...arrSet] ``` Set 的常见属性和方法: ```javascript const set = new Set([32, 10, 25, 10, 32, 15]) console.log(set) // Set(4) { 32, 10, 25, 15 } // size 属性:获取元素数量 console.log(set.size) // 4 // 添加和删除元素 set.add(18) set.delete(10) // 判断元素是否在结构中 console.log(set.has(25)) // true // 清空 set 中的元素 // set.clear() console.log(set) // Set(4) { 32, 25, 15, 18 } ``` 对 Set 结构进行遍历: ```javascript const set = new Set([32, 10, 25, 10, 32, 15]) console.log(set) // Set(4) { 32, 10, 25, 15 } set.forEach(item => { console.log(item) }) console.log('- - - - - -'); for (const item of set) { console.log(item) } ``` #### 16. ES6 - 数据结构 WeakSet(存放对象) --- 和 Set 类似的另外一个数据结构称之为 WeakSet,也是内部元素不能重复的数据结构 那么和 Set 有什么区别呢 ? + WeakSet 中只能存放对象类型,不能存放基本数据类型 + WeakSet 对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么 GC 可以对该对象进行回收 WeakSet 常见的方法: + add(value):添加某个元素,返回 WeakSet 对象本身 + delete(value):从 WeakSet 中删除和这个值相等的元素,返回 boolean 类型 + has(value):判断 WeakSet 中是否存在某个元素,返回 boolean 类型 ```javascript const weakSet = new WeakSet() // 区别一:只能存放对象类型,无法添加基本类型数据 // weakSet.add(10) // Uncaught TypeError: Invalid value used in weak set // 区别二:WeakSet 对数据是一个弱引用,而 Set 对数据是一个强引用 let obj = { name: 'liang' } weakSet.add(obj) ``` 对强引用和弱引用的理解: ```javascript // 因为 Set 是强引用,所以当 user 修改为 null 时,user 不会被 GC 回收 let user = { name: 'liang' } const set = new Set(user) user = null // 因为 WeakSet 是弱引用,所以当 profile 修改为 null 时,profile 就被 GC 回收掉了 let profile = { name: 'liang' } const weakSet = new WeakSet(profile) profile = null ``` #### 17. ES6 - 数据结构 Map(支持对象属性 key) --- `Map` 是一种新的数据结构,用于存储键值对,它与传统对象(Object)相比,具有更灵活的键类型和更好的性能特性 `Map` 用于存储映射关系,之前我们可以使用对象来存储映射关系,它们有什么区别呢 ? + 事实上对象存储映射关系只能用字符串(ES6 新增了 Symbol)作为属性名(key) + 某些情况下,可能希望使用其他数据类型作为 key。比如: 对象,这个时候会自动将对象转为字符串来作为 key ```javascript // 传统的对象属性名 key,只能是字符串或Symbol值 const user = { name: 'liang' } const profile = { age: 20 } const object = { [user]: 'aaa', [profile]: 'bbb' } // [] 是计算属性名的写法,将表达式的结果作为属性名 // 因为会将属性名转为字符串,所以都变为了 [object Object],后者覆盖前面,然后输出以下结果 console.log(object) // { [object Object]: 'bbb' } // Map 就是允许对象类型作为 key 的 const map = new Map() map.set(user, 'aaa') map.set(profile, 'bbb') map.set('name', 'liang') // 也可以传普通字符串 console.log(map) ``` 可以通过构造函数来创建一个空的 Map,也可以传入一个包含键值对数组的参数来初始化 Map ```javascript // 创建一个空的 Map let map = new Map() // 创建一个带有初始值的 Map let map2 = new Map([['name', 'liang'], ['music', '天空之外']]) ``` `Map` 常见属性和方法: + size 属性:获取 Map 中键值对的数量 + set() 方法:添加或更新键值对,返回整个 Map 对象,支持链式调用 + get() 方法:根据键获取对应的值 + has() 方法:检测 Map 中是否存在指定的键 + delete() 方法:删除指定键对应的键值对 + clear() 方法:清空键值对 ```javascript const map = new Map([['name', 'liang'], ['music', '天空之外']]) console.log(map.size) // 2 // map.set() 添加或更新键值对 map.set('age', 20).set('height', 190) // map.get() 根据键获取对应的值 console.log(map.get('name')) // liang // map.has() 检测是否存在指定的键 console.log(map.has('age')) // true // map.delete() 删除指定键对应的键值对 map.delete('height') // map.clear() 清空键值对 map.clear() console.log(map) ``` 遍历 Map 也有多种方式: ```javascript const map = new Map([['name', 'liang'], ['music', '天空之外']]) map.forEach((value, key) => { console.log(key, value) }) for (const [key, value] of map) { console.log(key, value) } ``` #### 18. ES6 - 数据结构 WeakMap(key只能是对象) --- 和 Map 相似的另外一个数据结构称之为 WeakMap,也是以键值对的形式存在的 WeakMap 和 Map 有什么区别呢 ? + WeakMap 的 key 只能使用对象,不接受其他的类型作为 key + WeakMap 的 key 对对象的引用是弱引用,如果没有其他引用引用这个对象,那么 GC 可以回收该对象 ```javascript // Map 是强引用,所以当 obj 修改为 null 时,obj 不会被 GC 回收 let obj = { name: 'liang' } const map = new Map() map.set(obj, 'aaa') obj = null // WeakMap 是弱引用,所以当 obj2 修改为 null 时,obj2 就被 GC 回收掉了 let obj2 = { name: 'liang' } const map2 = new WeakMap() map2.set(obj2, 'bbb') obj2 = null ``` #### 19. ES7 - Array.includes 方法和指数运算符 --- 之前判断数组中是否存在某个元素,通常是判断索引值,ES7 新增了 `includes` 方法,可以直接判断数据是否在数组中 ```javascript const arr = ['html', 'css', 'js'] // 判断索引值 if (arr.indexOf('css') !== -1) { console.log('在数组中') } // 支持传入第二个参数指定从哪个位置判断 arr.includes('css', 1) if (arr.includes('css')) { console.log('在数组中') } ``` 那么 `indexOf` 和 `includes` 有没有什么区别呢 ?当然是有的 ```javascript const arr = ['html', 'css', NaN] // indexOf 无法正确判断 NaN console.log(arr.indexOf(NaN)) // -1 // includes 可以正确判断 NaN console.log(arr.includes(NaN)) // true ``` ES7 新增的指数运算符: ```javascript // 2 的 5 次方(之前指数运算的写法) console.log(Math.pow(2, 5)) // ES7 增加了一个运算符 ** console.log(2 ** 5) ``` #### 20. ES8 - Object.values、Object.entries 方法 --- 之前我们可以通过 Object.keys() 获取一个对象的所有 key,在 ES8 中提供了 Object.values() 来获取所有的 value 值 ```javascript // 常见用法 const user = { name: 'liang', age: 20, height: 180 } console.log(Object.keys(user)) // ['name', 'age', 'height'] console.log(Object.values(user)) // ['liang', 20, 180] // 用于数组和字符串(很少用) console.log(Object.values(['html', 'css', 'js'])) // ['html', 'css', 'js'] (原数组本身) console.log(Object.values('hello')) // ['h', 'e', 'l', 'l', 'o'] ``` 通过 `Object.entries()` 可以获取到一个数组,数组中会存放可以枚举属性的键值对数组 ```javascript // 一帮情况下,使用它是为了让我们方便进行遍历操作的 const user = { name: 'liang', age: 20, height: 180 } console.log(Object.entries(user)) console.log(Object.entries('html')) console.log(Object.entries(['html', 'css', 'js'])) ``` #### 21. ES8 - String.padStart、String.padEnd 方法 --- 在 ES8 中,引入了两个字符串方法:padStart、padEnd,用于在字符串的开头或结尾填充指定字符,使其达到指定长度 语法格式: + targetLength:目标字符串长度 + padString:用于填充的字符串,默认为空格 + 返回值为填充后的字符串,不会修改原字符串 ```plaintext string.padStart(targetLength[, padString]) string.padEnd(targetLength[, padString]) ``` 使用示例: ```javascript const string = 'hello' console.log(string) // 目标长度8,默认填充空格 console.log(string.padStart(8)) // 目标长度8,填充指定符号 console.log(string.padStart(8, '*')) // 从结尾填充,目标长度8,填充指定符号 console.log(string.padEnd(8, '*')) ``` 那么,有哪些场景可以用到字符串填充呢 ? ```javascript // 时间格式化 function formatTime(time) { const date = new Date(time); const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}` } // 传入一个11位的手机号,将中间四位修改为 '*' function formatMobile(mobile) { return mobile.slice(0, 3).padEnd(7, '*') + mobile.slice(-4) } ``` 尾逗号(Trailing Commas)是指在数组、对象字面量或函数参数列表的最后一个元素后添加的额外逗号 这一特性自 ES5 起已被标准正式支持,并在 ES2017(ES8)中扩展至函数参数 尽管看似冗余,但它显著提升了代码的可维护性,尤其是在多行结构中增删项时,可以避免因遗漏逗号而引发的语法错误 ```javascript // ES8 才开始支持函数接收参数时或调用时最后面还有逗号 function foo(m, n,) { console.log({ m, n }); } foo(111, 222,) ``` #### 22. ES10 - Array.flat、Array.flatMap 方法 --- `flat()` 和 `flatMap()` 是两个用于处理数组扁平化的常用方法,它们各有不同的用途和特点 + flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历的子数组中的元素合并为一个新数组返回 + flatMap() 方法首先使用映射函数映射每个元素,然后将结果压缩为一个新数组 + flatMap 是先进行 map 操作,再做 flat 的操作 + flatMap 中的 flat 相当于深度为 1 语法格式: ```javascript // depth 是可选参数,表示要展开的嵌套层级,默认为 1。如果传入 Infinity,则会完全展开所有层级的嵌套数据 const newArray = array.flat([depth = 1]) ``` 使用示例: ```javascript const nums = [1, [2], [[3]], [[[4]]]] console.log(nums) console.log(nums.flat(1)) // [1, 2, Array(1), Array(1)] console.log(nums.flat(2)) // [1, 2, 3, Array(1)] console.log(nums.flat(Infinity)) // [1, 2, 3, 4] ``` `flatMap()` 方法的使用方法,通过以下代码示例学习: ```javascript // 需求:将数组元素中的每个单词抽离出来 const slogan = ['what are you doing', 'how old are you'] // 刚学习完 flat() 方法,你可能会这么做 const words = slogan.map(item => item.split(' ')).flat() console.log(words) // ['what', 'are', 'you', 'doing', 'how', 'old', 'are', 'you'] // 现在告诉你,可以使用 flatMap() 简化代码,它会先执行 map() ,然后再进行 flat() const words2 = slogan.flatMap(item => item.split(' ')) console.log(words2) // ['what', 'are', 'you', 'doing', 'how', 'old', 'are', 'you'] ``` #### 23. ES10 - Object.fromEntries、String.trimStart 方法 --- `Object.fromEntries()` 方法用于将键值对列表转换为一个普通对象 ```javascript const user = { name: 'liang', age: 20 } console.log(user) // { name: 'liang', age: 20 } // 如何根据 entries 结果,再将数据处理成对象 const entries = Object.entries(user) console.log(entries) // [ Array(2), Array(2) ] // 传统方式是这样做的 const newObj = {} for (const [key, value] of entries) { newObj[key] = value } console.log(newObj) // { name: 'liang', age: 20 } // 还可以通过 Object.fromEntries() 方法实现 console.log(Object.fromEntries(entries)) // { name: 'liang', age: 20 } ``` `Object.fromEntries()` 方法的应用场景,配置 `URLSearchParams()` 处理查询字符串 ```javascript const queryStrig = 'name=liang&age=20&height=180' const queryParams = new URLSearchParams(queryStrig) // 它是一个可迭代的对象 for (const item of queryParams) { console.log(item) } // 将它转为一个普通对象 const params = Object.fromEntries(queryParams) console.log(params) // { name: 'liang', age: '20', height: '180' } ``` 清除字符串中的空格 ```javascript const string = ' hello world ' // 清除前后两边的空格(ES5) console.log({ trim: string.trim() }) // 清除前面的空格(ES10) console.log({ trimStart: string.trimStart() }) // 清除后面的空格(ES10) console.log({ trimEnd: string.trimEnd() }) ``` #### 24. ES11 - 大整数类型 Bigint --- 在早期的 JavaScript 中,我们不能正确的表示过大的数字 + 大于 `Number.MAX_SAFE_INTEGER` 的数值,表示的值可能是不正确的 ```javascript // ES11 之前 max_safe_integer const maxInt = Number.MAX_SAFE_INTEGER console.log(maxInt) // 9007199254740991 // 可以发现 +1 和 +2 的值竟然是相同的 console.log(maxInt + 1) // 9007199254740992 console.log(maxInt + 2) // 9007199254740992 // ES11 之后:Bigint( 后面加一个 n ) const bigInt = 900719925474099100n console.log(bigInt) // 900719925474099100n // 直接进行运算是会报错的,需要在 10 后面加个 n // Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions // console.log(bigInt + 10) console.log(bigInt + 10n) // 900719925474099110n // 如果是一个变量,可以先使用 BigInt() 转为 bigint,再进行运算 const num = 20 console.log(bigInt + BigInt(20)) // 900719925474099120n ``` #### 25. ES11 - 空值合并运算符 ?? --- 空值合并运算符(Nullish Coalescing Operator),当左侧操作数为 `null/undefined` 时,返回右侧操作数 + 只有 `null` 和 `undefined` 被视为空值,其他假值不会触发 fallback(右侧的后备方案) ```javascript // 之前获取值时,设置默认值大多使用逻辑或实现,这种是存在弊端的(比如:对于 0、false) let foo = 0 console.log(foo || 'hello') // hello // 空值合并运算符 let bar = 0 console.log(bar ?? 'hello') // 0 ``` #### 26. ES11 - 可选链操作符 ?. --- ES11 进入了一个非常实用的特性:可选链操作符(Optional Chaining Operator) 它的核心作用是:安全的访问嵌套对象的属性或方法,避免因为中间某一层为 `null` 或 `undefined` 而抛出错误 ```javascript const profile = { name: 'liang' } // 以下代码会抛出错误,这是因为 profile.info 是 undefined,又访问了 undefined 的 age 属性 // Uncaught TypeError: Cannot read properties of undefined (reading 'age') console.log(profile.info.age) // 使用可选链操作符,属性不存在也不会报错 console.log(profile.info?.age) ``` 当对接后端接口时,接口中的字段值可能出现意料之外的情况,导致前端代码报错,我们应该怎么避免这种问题 ```javascript // 比如:当前查询某个列表,data 应该始终为一个数组,但是后端接口不规范 // 后端提供的接口,当没有数据时,data 给的是一个 null,导致前端代码报错 // 我对接Java 后端接口时,这个问题出现很多次,而且对方还不想修改,说是改起来比较麻烦/框架问题 const res = { code: 200, msg: 'success', data: null } // 报错原因:res.data 为 null,调用了 null.forEach 就报错了 // Uncaught TypeError: Cannot read properties of null (reading 'forEach') // res.data.forEach(item => { }) // 使用 ES11 的可选链,可以很简单的处理掉这个问题 res.data?.forEach(item => { }) ``` #### 27. ES11 - globalThis 和 for...in 标准化 --- 在 ES11 之前,我们想要获取当前环境的全局对象,不同的环境获取方式是不一样的 + 比如:在浏览器中可以通过 this、window 来获取 + 在 node 中我们需要通过 global 来获取 `globalThis` 是 ES11 引入的一个标准化的全局对象引用,用于在任何 JavaScript 环境中统一访问全局对象 ```javascript // 浏览器环境(使用浏览器打开) console.log(this) console.log(window) // node 环境(终端运行命令: “node 文件名” 可查看) console.log(global) // ES11(在任何环境中都可以访问全局对象) console.log(globalThis) ``` ES11 对 `for...in` 语句进行了标准化规范 ```javascript const user = { name: 'liang', age: 20 } // ES11 之前,ECMA 对 for...in 没有制定标准,部分浏览器中 item 指向的可能是 value,而不是 key // 在 ES11 中,ECMA 对 for...in 进行标准化规范,至此 item 统一指向了 key for (const item in user) { console.log(item) } ``` #### 28. ES12 - FinalizationRegistry 执行清理回调 --- FinalizationRegistry 是 ES12 引入的一个高级特性,用于在对象被垃圾回收(GC)后执行清理回调。 FinalizationRegistry 对象可以让你在对象被垃圾回收时请求一个回调,它提供了这样的一种方法: + 当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调(finalizer) + 你可以通过调用 register 方法,注册任何你想要清理回调的对象,传入该对象和所含的值 ```javascript // 创建一个目标对象 let user = { name: 'liang' } // ES12:FinalizationRegistry 类,参数是一个回调函数,对象被垃圾回收时执行 const finalRegistry = new FinalizationRegistry(() => { // 刷新页面并不会立即执行 // 因为 GC 垃圾回收机制是不定时来检测一下有没有垃圾,多等一会就行 console.log('注册到 finalRegistry 的某个对象被销毁'); }) // 将对象注册到 finalRegistry 上 finalRegistry.register(user) // 当 user 不再被任何强引用持有时,未来某刻回调可能被调用(我测试是 30秒左右) user = null // 移除强引用,使其可被 GC ``` 可以注册多个对象到注册表,并且可以传入第二个参数(用于在回调中传递额外信息) ```javascript // 目标对象 let user = { name: 'liang' } let info = { name: 'james' } // 通过打印时间,可发现目标对象被移除强引用之后的30秒左右执行的清理回调 console.log(new Date().getTime() / 1000) const finalRegistry = new FinalizationRegistry(value => { console.log(new Date().getTime() / 1000) console.log('注册到 finalRegistry 的某个对象被销毁 ' + value) }) // 注册多个对象到注册表,并且可以传入第二个参数 finalRegistry.register(user, 'aaa') finalRegistry.register(info, 'bbb') // 移除强引用,使其可被 GC user = null info = null ``` #### 29. ES12 - WeakRef 创建对象的弱引用 --- `WeakRef` 是 ES12 引入的一个特性,用于创建对对象的弱引用,它就是提供弱引用能力的内置类 它允许你引用一个对象,但不会阻止该对象被垃圾回收(GC) + 强引用(Strong Reference):常规的对象引用。只要存在强引用,对象就不会被垃圾回收 + 弱引用(Weak Reference):不会阻止对象被回收。当对象仅被弱引用持有时,它可能在任意时间被 GC 回收 ```javascript // 因为 user 的引用复制给了 info,所以当 user 设置为 null,也不会触发清理回调(引用还存在) let user = { name: 'liang' } let info = user // 强引用 console.log(new Date().getTime() / 100) const finalRegistry = new FinalizationRegistry(value => { console.log('注册到 finalRegistry 的某个对象被销毁 ' + value) }) finalRegistry.register(user, 'aaa') user = null ``` 那么,我们如何将 info 变为 user 的弱引用呢 ? ```javascript let user = { name: 'liang' } // 1. 强引用 // let info = user // 2. 可以改为使用 WeakSet,使 info 变为弱引用,这种方式能实现 // let info = new WeakSet() // info.add(user) // 3. ES12 推出 WeakRef,让我们更方便的创建弱引用 let info = new WeakRef(user) console.log(info.deref()) // 获取到对象数据 const finalRegistry = new FinalizationRegistry(value => { console.log('注册到 finalRegistry 的某个对象被销毁 ' + value) }) finalRegistry.register(user, 'aaa') user = null ``` #### 30. ES12 - 逻辑赋值运算符、String.replaceAll() --- JavaScript 中的三种逻辑赋值运算符是 ES12 引入的语法糖,用于在满足特定条件时对变量或属性进行条件性赋值 ```javascript // 逻辑或赋值运算 ||= let message = '' message ||= 'default' // 相当于 message = message || 'default' // 逻辑与赋值运算 &&= let a = 1 let b = 2 a &&= b // 相当于 a = a && b(当 a 有值,取 b 的值返回) // 空值合并赋值 ??= let c = '' let d = 2 c ??= d // c = c ?? d ``` `String.prototype.replaceAll()` 是 ES12 引入的一个字符串方法,用于将字符串中所有匹配的子串替换为新值 + 它解决了长期以来 `String.prototype.replace()` 默认只替换第一个匹配项的问题 为什么需要 `replaceAll()` ? 在它出现之前,要全局替换字符串,通常需要: ```javascript // 方法1:使用正则表达式 + 全局标志 g str.replace(/old/g, 'new') // 方法2:手动处理(不推荐) str.split('old').join('new') ``` `replaceAll()` 提供了更简单、直观、安全的全局替换方式,语法格式: + searchValue:可以是字符串或者正则表达式 + replaceValue:新的字符串 ```javascript str.replaceAll(searchValue, replaceValue) ``` 使用示例: ```javascript const string = 'good idea good' // 第一个参数:使用字符串 console.log(string.replaceAll('good', 'aaa')) // 第一个参数:正则表达式。必须带有全局标志 g , 否则会抛出错误 console.log(string.replaceAll(/good/g, 'bbb')) ``` #### 31. ES13 - Array/String.at()、Object.hasOwn() --- `Array.prototype.at()` 和 `String.prototype.at()` 是 ES13 引入的新方法,支持负数索引取值 + 它解决了长期以来 JavaScript 无法直接用负索引取值的问题 ```javascript // index:整数,可以是正数(从 0 开始),也可以是负数(从 -1 开始,表示最后一个) // 如果超出索引范围,返回 undefined,不会报错 array.at(index) string.at(index) ``` 在 ES13 之前,要访问数组最后一个元素,通常这样写: ```javascript const arr = ['html', 'css', 'js'] arr[arr.length - 1] // js ``` 从 ES13 开始,可以使用 `at()` 直接获取指定负索引位置的元素,同样它也支持正索引 ```javascript const arr = ['html', 'css', 'js', 'vue'] console.log(arr.at(0)) // vue(第一个) console.log(arr.at(2)) // js(索引为2) console.log(arr.at(-1)) // vue(最后一个) ``` ES13 中 Object 新增了一个静态方法(类方法):`Object.hasOwn(obj, key)` ```javascript const user = { name: 'liang', // 2. 在原型上添加属性也可以写在这里 __proto__: { height: 180 } } // 1. 在原型上添加属性 Object.prototype.age = 20 console.log(user.name) // liang console.log(user.age) // 20 console.log(user.height) // 180 // ES13 之前 // 判断属性是否在对象自身上,在自身上返回 true(在原型上会返回 false) console.log(user.hasOwnProperty('name')) // true console.log(user.hasOwnProperty('age')) // false // ES13 推出 Object.hasOwn(obj, key) 用于替代 Object.prototype.hasOwnProperty(key) console.log(Object.hasOwn(user, 'name')) // true console.log(Object.hasOwn(user, 'age')) // false ``` 那么 `Object.hasOwn(obj, key)` 和 `Object.prototype.hasOwnProperty(key)` 有什么区别呢 ? + 因为 hasOwnProperty 是一个实例方法,那么这个方法可能会 被“污染”或覆盖 + hasOwn 对无原型对象的数据也支持,而 hasOwnProperty() 不行 ```javascript // 区别1: 因为 hasOwnProperty 是一个实例方法,那么这个方法可能会 被“污染”或覆盖 const user = { name: 'liang', // 重写该方法,那么原型上的 hasOwnProperty 就被覆盖了 hasOwnProperty() { return 'aaa' } } console.log(user.hasOwnProperty('name')) // aaa(显然是不对的) // 区别2: hasOwn 对无原型对象也支持,而 hasOwnProperty() 不行 // Object.create() 用于创建一个新对象,并指定其原型 const info = Object.create(null) info.name = 'liang' // 此时不能使用 hasOwnProperty() // Uncaught TypeError: info.hasOwnProperty is not a function // console.log(info.hasOwnProperty('name')) console.log(Object.hasOwn(info, 'name')) // true ``` #### 32. ES13 - 类中新增的成员(私有化成员) --- 从 ES13 开始,正式支持在类中定义真正的私有化属性(private) ```javascript class Person { // 2. 对象属性:public 公共的 // 在类的内部和外部都可以访问(并且所有类的实例都能访问到这个值) height = 180 //(ES13 之前)对象属性:private 私有的,只是开发者之间的约定,在外面其实还是能访问的 _intro = '个人介绍' //(ES13 开始)对象属性:private 私有的 #info = 'real private info' constructor(name, age) { // 1. 对象中的属性:在 constructor 通过 this 设置 this.name = name this.age = age } running() { console.log(this.#info) // 类内部可读取 this.#showName() // 类内部可调用 } // 私有方法 #showName() { console.log('name: ' + this.name); } } const p = new Person('liang', 20) console.log(p._intro) // 可以访问(不是真的私有属性) p.running() const p2 = new Person('james', 25) console.log(p.height) // 180 console.log(p2.height) // 180 ``` 类的静态属性、静态方法使用示例: ```javascript class Person { // 静态的公共属性(默认情况是 public) static name = 'liang' // 静态的私有属性 private(使用 # 表示) static #age = 20 // 静态方法 static showName() { console.log(this.#age); } // 静态代码块(在类初始化时立即执行的代码块) static { console.log('hello world'); } } console.log(Person.name) Person.showName() ```