十一. ES6 ~ ES12

92 阅读25分钟

十一. ES6 ~ ES12

11.1. 字面量增强的写法

ES6中对 对象字面量 进行了增强,称之为 Enhanced object literals(增强对象字面量),字面量的增强主要包括下面几部分:

  • 属性的简写:Property Shorthand
  • 方法的简写:Method Shorthand
  • 计算属性名:Computed Property Names
 let name = "why"
 let age = 18
 let obj = {
     // 属性的简写:Property Shorthand
     name,
     age,
     // 方法的简写:Method Shorthand
     foo(){
         console.log("foo");
     },
     // 计算属性名:Computed Property Names
     [name+123]:"计算属性名"
 }
 console.log(obj);

11.2. 解构Destructuring

数组的解构

 let names = ["张三", "李四", "王五"]
 // 对数字解构
 let [item1, item2, item3] = names
 // 解构后面的元素
 let [, , item4] = names
 // 解构出一个元素,后面的元素放到一个新数组
 let [item5, ...newNames] = names
 // 结构的默认值(没有默认值则是undefined)
 let [item6, item7, item8, item9 = "赵六"] = names

对象的解构

 let obj = {
     name: "why",
     age: 18,
     height: 1.88
 }
 // 任意顺序,按key去解构
 var { height, name, age } = obj // 1.88 why 18
 // 默认值
 var { age, address = "成都市" } = obj // 18 成都市
 // 重命名
 var { name: newName } = obj // why
 ​
 // 应用场景列举:
 function foo({name,age}){
     console.log(name,age); // why 18
 }
 foo(obj)

11.3. let_ const_ var

11.3.1. let/const基本使用

let关键字:

  • 从直观的角度来说,let和var是没有太大的区别的,都是用于声明一个变量

const关键字:

  • const关键字是constant的单词的缩写,表示常量、衡量的意思;

  • 它表示保存的数据一旦被赋值,就不能被修改;

    • 如果赋值的是引用类型(内存地址 ),那么可以通过引用找到对应的对象,修改对象的内容;

注意: 另外let、const不允许重复声明变量

11.3.2. 作用域提升_ 暂时性死区

var声明的变量是会进行作用域提升的,但let/const没有(暂时性死区)

是不是意味着foo变量只有在代码执行阶段才会创建的呢?

  • 事实上并不是这样的,我们可以看一下ECMA262对let和const的描述;
  • 这些变量会被创建在包含他们的词法环境被实例化时,但是是不可以访问它们的,直到词法绑定被求值;

image-20220304203652205

作用域提升: 在声明变量的作用域中,如果这个变量可以在声明之前被访问,那么我们可以称之为作用域提升;

 //  let/const是没有作用域提升的,foo解析时是被创建了,但是不能访问
 console.log(foo); // Reference(引用)Error: Cannot access 'foo' before initialization(初始化)
 let foo="foo"

暂时性死区:使用let、const声明的变量,在声明之前,变量都是不可以访问的;

  • let声明的变量是有块级作用域,因此不能访问到全局的foo,由于let不会有提升,所以后面的foo也不能访问到
 // 例一
 var foo = "foo"
 if (true) {
     console.log(foo);
     let foo = "abc"
     }
 // 例二
 function bar(){
     console.log(foo);
     let foo="abc"
     }
 bar()

11.3.3. Window对象添加属性

在全局通过var来声明一个变量,事实上会在window上添加一个属性:

但是let、const是不会给window上添加任何属性的。

11.3.4. 块级作用域

ES5的var没有块级作用域,ES5里只有 全局作用域 和 函数作用域

 {
   var foo = "foo"
 }
 console.log(foo); // foo

ES6中新增了块级作用域,并且通过let、const、function、class声明的标识符是具备块级作用域的限制的

  • 但我们发现函数拥有块级作用域,但是外面依然是可以访问的
  • 这是因为引擎会对函数的声明进行特殊的处理,大部分浏览器为了兼容以前的代码,让function是没有块级作用域的
 {   // 除了demo()可以访问,其他的都不能访问
     let foo = "aaa" 
     function demo() { }
     class Person {}
 }

11.3.5. if_ switch_ for块级代码

 if (true) {
     var foo = "foo" // 外部能访问
     let bar = "bar" // 外部不能访问
     }
 ​
 let color = "red"
 switch (color) {
     case "red":
         var foo = "foo"// 外部能访问
         let bar = "bar"// 外部不能访问
         }
 // 外部能访问i;但如果是let声明的i,外部则不能访问
 for(var i=0;i<10;i++){
    // i= 10
 }

11.3.6. 块级作用域的应用

 <button>按钮1</button>
 <button>按钮2</button>
 <button>按钮3</button>
 ​
 const btns = document.getElementsByTagName('button')
 for (var i = 0; i < btns.length; i++) {
     btns[i ].onclick = function () {
         // 按钮点击时,会去上层作用于找ivar没有块级作用域,所以就找到了全局的i
         // 如果是let,上层作用域就是{}了,每个块的i不会相互干扰
         console.log(`第${i}个按钮被点击了`);
     }
 }

11.3.7. var、let、const的选择

优先推荐使用const,这样可以保证数据的安全性不会被随意的篡改;

当我们明确知道一个变量后续会需要被重新赋值时,这个时候再使用let;

注:var所表现出来的特殊性:比如作用域提升、window全局对象、没有块级作用域等都是一些

历史遗留问题;

11.4. 模板字符串

模板字符串:使用``代替ES6之前的"+变量+"

 const name="why"
 console.log(`我的名字是${name}`); // 我的名字是why
 ​
 let num=10
 console.log(`double num is ${num*2}`); // double num is 20
 ​
 let age=18
 function doubleAge(){
     return age*2
 }
 console.log(`double age is ${doubleAge()}`); // double age is 36

标签模板字符串:可以用``来调用函数,并传入参数

 // m: 模板字符串中的整个字符串,只是被切成了多块,放到了一个数组中
 // n: 第一个${}
 // n: 第二个${}
 function foo(m,n,x){
     console.log(m,n,x); // ['hello', 'wor', 'ld'] 'why' 18
 }
 const name="why"
 const age= 18
 foo`hello${name}wor${age}ld`

11.5. 函数的默认参数

 // 函数的默认参数
 function foo(m = 1, n = 2) {
     console.log(m,n); // 1 2
 }
 foo()
 ​
 // 对象参数的默认值和解构
 function bar({ name, age } = { name: "why", age: 18 }){
     console.log(name,age); // zhangsan 20
 }
 bar( { name:"zhangsan", age:20 } )
 ​
 // 另一种写法
 function info({ name = "lisi", age = 25} = { }) {
     console.log(name,age); // lisi 25
 }
 info()

注意:1. 建议默认值我们通常会将其放到最后,如果非要放到前面,则只能传入undefined,参数就会自动取默认值,但多此一举了

  1. 默认值会改变函数的length的个数,默认值以及后面的参数都不计算在length之内了。
 function bar(name, age = 18, address, height) {
     console.log(bar.length); // 1
 }
 bar()

11.6. 剩余参数

ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:

  • 如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组
 function foo(m, n, ...args) {
     console.log(m, n); // 1 2
     console.log(args); // [3, 4, 5]
 }
 foo(1, 2, 3, 4, 5)

那么剩余参数和arguments有什么区别呢?

  • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
  • arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
  • arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供 并且希望以此来替代arguments的;

注意: 剩余参数必须放到最后一个位置,否则会报错。

11.7. 展开语法

展开语法(Spread syntax):

  • 可以在函数调用/数组构造时,将数组表达式或者string在语法层面展开;
  • 还可以在构造字面量对象时, 将对象表达式按key-value的方式展开;

展开语法的场景:

  • 在函数调用时使用;
  • 在数组构造时使用;
  • 在构建对象字面量时,也可以使用展开运算符,这个是在ES2018(ES9)中添加的新特性;
 const names = ["a", "b", "c", "d"]
 const name = "why"
 const info = { name: "coder" , age: 18 }
 ​
 // 1.函数调用时
 function foo(x, y, z) {
     console.log(x, y, z);
 }
 foo(...names) // a b c
 foo(...name) // w h y 注:对字符串使用展开语法,会把每个字符串进行展开
 ​
 // 2.构造数组时
 const newNames = [...names, ...name] // ['a', 'b', 'c', 'd', 'w', 'h', 'y']
 ​
 // 3.ES2018(ES9)添加的新特性,展开运算只能用于可迭代对象,对象的展开不是用迭代器实现的
 // {0: 'a', 1: 'b', 2: 'c', 3: 'd', name: 'coder', age: 18, address: '成都市'}
 const obj = {...info, address: "成都市" , ...names} 

补充:展开运算符其实是一种浅拷贝,将info的内容复制给了obj

 const info = {name:"张三",friends:{name:"李四"}}
 const obj={...info, name: "王五"}
 obj.friends.name="村霸上线了"
 console.log(info.friends.name); // 村霸上线了

image-20220305020807918.png

11.8. 箭头函数

箭头函数是没有显式原型的,所以不能作为构造函数,使用new来创建对象;也没有this 和 arguments

 let foo=()=>{}
 console.log(foo.prototype); // undefined
 new foo() // TypeError: foo is not a constructor

11.9. 数值的表示

 const num1=100
 // binary 二进制
 const num2=0b100
 // octal 八进制
 const num3=0o100
 // hexadecimal 十六进制
 const num4=0x100
 // 数字分隔符:数字过长时,大的数值可用_连接符分割,ES2021更新的(ES12)
 const num5= 10_000_000_000
 console.log(num1,num2,num3,num4,num5); // 100 4 64 256 10000000000

11.10. Symbol

基本使用

Symbol是ES6中新增的一个基本数据类型,翻译为符号。

那么为什么需要Symbol呢?

  • 在ES6之前,对象的属性名都是字符串形式,那么很容易造成属性名的冲突;
  • 比如原来有一个对象,我们希望在其中添加一个新的属性和值,但是我们在不确定它原来内部有什么内容的情况下,很容易造成冲突,从而覆盖掉它内部的某个属性;

Symbol就是为了解决上面的问题,用来生成一个独一无二的值

  • Symbol值是通过Symbol函数来生成的,生成后可以作为属性名;
  • 也就是在ES6中,对象的属性名可以使用字符串,也可以使用Symbol值;

Symbol即使多次创建值,它们也是不同的: Symbol函数执行后每次创建出来的值都是独一无二的;

我们也可以在创建Symbol值的时候传入一个描述description:这个是ES2019(ES10)新增的特性;

注意: Symbol不能通过 .语法 去获取(obj1.s1),js会去找作为字符串的s1的key。只能用obj1[s1]

 const obj={
     name:"why",
     age:18,
     friends:{name:"李四"}
 }
 // 不管定义属性时,key有没加"",在底层解析时,都会给所有的key加上""
 console.log(Object.keys(obj)); // ['name', 'age', 'friends']
 ​
 const s1 = Symbol()
 const s2 = Symbol()
 const s3 = Symbol("asdf") // ES2019(ES10)中,Symbol还有一个描述(description)
 console.log(s3.description); // asdf
 ​
 // Symbol值作为key
 // 1. 在定义对象字面量时使用
 const obj1 = {
     [s1]: "aaa",
     [s2]: "bbb",
 }
 // 2.新增属性
 obj1[s3] = "ccc"
 // 3.Object.defineProperty方式
 const s4 = Symbol()
 Object.defineProperty(obj1, s4, {
     enumerable: true,
     configurable: true,
     writable: true,
     value: "ddd"
 })
 console.log(obj1[s1], obj1[s2], obj1[s3], obj1[s4] ); // aaa bbb ccc ddd

遍历Symbol值的key

  • 使用Symbol作为key的属性名,在遍历、Object.keys等中是获取不到这些Symbol值
  • 需要使用Object.getOwnPropertySymbols来获取所有Symbol的key
 const s1 = Symbol('s1')
 const s2 = Symbol('s2')
 const obj1 = {
     [s1]: "aaa",
     [s2]: "bbb",
     name:"why"
 }
 ​
 console.log(Object.keys(obj1)); // ['name']
 console.log(Object.getOwnPropertyNames(obj1)); // ['name']
 console.log(Object.getOwnPropertySymbols(obj1)); // [Symbol(s1), Symbol(s2)]
 for (const symbolKey of Object.getOwnPropertySymbols(obj1)){
     console.log(obj1[symbolKey]); // aaa bbb
 }

相同值的Symbol

Symbol的目的是为了创建一个独一无二的值,如果我们现在就是想创建相同的Symbol应该怎么来做?

  • 我们可以使用Symbol.for方法来做到这一点;
  • 并且我们可以通过Symbol.keyFor方法来获取对应的key;
 // 1.Symbol.for(key) : 创建一个Symbol值; 创建Symbol值时,会先去找之前有没有相同的key('s1'),如果有,就把之前的Symbol值返回
 const s1 = Symbol.for('s1')
 const s2 = Symbol.for('s1')
 console.log(s1 === s2); // true
 ​
 // 2.Symbol.keyFor(symbol) : 获取对应的key;
 console.log(Symbol.keyFor(s1)); // s1

11.11. Set_ WeakSet

11.11.1 认识set和基本API使用

在ES6之前,我们存储数据的结构主要有两种:数组、对象。

  • 在ES6中新增了另外两种数据结构:Set(集合类型)、Map(存储映射关系),以及它们的另外形式WeakSet、WeakMap。

  • 使用 new 关键字和 Set 构造函数可以创建一个空集合:

  • 如果想在创建的同时初始化实例,则可以给 Set 构造函数传入一个可迭代对象,其中需要包含插入

    到新集合实例中的元素

Set是一个新增的数据结构,可以用来保存数据,类似于数组, 但是和数组的区别是元素不能重复

  • 创建Set我们需要通过Set构造函数(暂时没有字面量创建的方式):
  • 我们可以发现Set中存放的元素是不会重复的,那么Set有一个非常常用的功能就是给数组去重。

Set常见的属性:

  • size:返回Set中元素的个数;

Set常用的方法:

  • add(value):添加某个元素,返回Set对象本身;
  • delete(value):从set中删除和这个值相等的元素,返回boolean类型;
  • has(value):判断set中是否存在某个元素,返回boolean类型;
  • clear():清空set中所有的元素,没有返回值;
  • forEach(callback, [, thisArg]):通过forEach遍历set;
  • for of:通过for of遍历。
 // 1.数组去重
 const arr = [10, 30, 30, 40]
 const newArr = [...new Set(arr)] // [10, 30, 40]
 const newArr2 = Array.from(new Set(arr)) // [10, 30, 40]
 // 2.常用方法
 const set = new Set([1, 2, 2])
 console.log(set.size); // 2
 console.log(set.add(3)); // Set(3) {1, 2, 3}
 console.log(set.delete(1)); // true
 console.log(set.has(2)); // true
 set.forEach(item => {
     console.log(item); // 2 3 
 });
 for (const item of set) {
     console.log(item); // 2 3 
 }
 set.clear()
 console.log(set); // Set(0) { }

11.11.2. 认识WeakSet和基本API使用

和Set类似的另外一个数据结构称之为WeakSet,也是内部元素不能重复的数据结构。

那么和Set有什么区别呢?

  • 区别一:WeakSet中只能存放对象类型,不能存放基本数据类型;
  • 区别二:Set是强引用,WeakSet对元素的引用是弱引用,如果没有其他引用对某个对象进行引用,那么GC可以对该对象进行回收;

解释下强引用和弱引用:

  • 强应用(strong reference):强引用指向着该内存,则不会被回收

  • 弱引用(weak reference):GC回收内存时。即使有弱引用指向着该内存,也会被回收

    • 作用:我们可以通过弱引用,使用里面的属性,使用方法我暂时没找到(在别的强引用没有解除对该内存的引用前)

    image-20220305221639427.png

WeakSet常见的方法:

  • add(value):添加某个元素,返回WeakSet对象本身;
  • delete(value):从WeakSet中删除和这个值相等的元素,返回boolean类型;
  • has(value):判断WeakSet中是否存在某个元素,返回boolean类型;

注意:WeakSet不能遍历

  • 因为WeakSet只是对对象的弱引用,如果我们遍历获取到其中的元素,那么有可能造成对象不能正常的销毁。
  • 所以存储到WeakSet中的对象是没办法获取的;

验证:弱引用指向的内存,会被回收

 const finalizationRegistry = new FinalizationRegistry((value) => {
     console.log("注册在finalizationRegistry的对象被销毁了", value);
 })
 let obj = {}
 let info = new WeakSet().add(obj)
 // 将obj注册到finalizationRegistry对象中
 finalizationRegistry.register(obj, "obj对象")
 obj = null // 执行到这里是,obj不会立马被回收,GC是不定时回收

11.1.3. WeakSet的应用场景

应用场景(用的场景少):person的runing方法只允许用person的实例对象调用,不允许通过别的对象来调用

疑惑:为什么这个案例不用Set来保存this?

答:Set是强引用,如果p=null时,由于set对p有引用,所以p不会销毁,需要用set.delete(p)来解除引用,太麻烦了

 const weakset = new WeakSet()
 class Person {
     constructor() {
         weakset.add(this)
     }
     runing() {
         // 判断this是不是person的实例对象
         if (!weakset.has(this)) {
             throw new Error("不能通过非构造方法创建出来的对象调用runing方法")
         }
         console.log(this);
     }
 }
 let p = new Person()
 p.runing()
 // Error: 不能通过非构造方法创建出来的对象调用runing方法
 p.runing.call({ name: "why" })
 ​
 console.log(weakset);
 // node中打印为:WeakSet { <items unknown> }
 // window中打印为:WeakSet {Person}
 // 原因(WeakSet同理): WeakSet不能进行遍历,打印WeakSet时,需要把里面的东西全部拿到(遍历),然后拼接出来

11.12. Map_ WeakMap

11.12.1 认识map和基本API使用

另外一个新增的数据结构是Map,用于存储映射关系。

那么使用Map和对象来存储映射关系,他们有什么区别呢?

  • 事实上我们对象存储映射关系只能用字符串(ES6新增了Symbol)作为属性名(key);

  • 某些情况下我们可能希望通过其他类型作为key,比如对象,这个时候会自动将对象转成字符串来作为key;

     // 验证:对象中不能使用对象来作为key
     const obj1 = { name: "zhangsan" }
     const obj2 = { name: "lisi" }
     const info = {
         [obj1]: "aaa",
         [obj2]: "bbb"
     }
     // 疑惑: 打印结果为什么只有一个键值对
     // 答:当把obj1作为key时,js会把obj1转成字符串格式,也就是'[Object Object]',obj2也会转成同样的字符串,所以后面的key会覆盖掉前面的key
     console.log(info); // {'[object Object]': 'bbb'}, window环境打印会省略''
    

但是Map允许对象类型作为key

 const obj1 = { name: "zhangsan" }
 const obj2 = { name: "lisi" }
 const map= new Map()
 map.set(obj1,"aaa")
 map.set(obj2,"bbb")
 map.set(1,"ccc")
 console.log(map);
 // Map {
 // { name: 'zhangsan' } => 'aaa',
 // { name: 'lisi' } => 'bbb',
 // 1 => 'ccc'
 // }

创建map时可以传入一个数组,(数组里存储的格式必须是entries(ES8))

 const obj1 = { name: "zhangsan" }
 const obj2 = { name: "lisi" }
 const map= new Map([[obj1,"aaa"],[obj2,"bbb"],[1,"ccc"]])
 console.log(map);
 // Map {
 // { name: 'zhangsan' } => 'aaa',
 // { name: 'lisi' } => 'bbb',
 // 1 => 'ccc'
 // }

Map常见的属性:

  • size:返回Map中元素的个数;

Map常见的方法:

  • set(key, value):在Map中添加key、value,并且返回整个Map对象;
  • get(key):根据key获取Map中的value;
  • has(key):判断是否包括某一个key,返回Boolean类型;
  • delete(key):根据key删除一个键值对,返回Boolean类型;
  • clear():清空所有的元素;
  • forEach(callback, [, thisArg]):通过forEach遍历Map;
  • Map也可以通过for of进行遍历。
 const obj1 = { name: "zhangsan" }
 const obj2 = { name: "lisi" }
 const map = new Map([[obj1, "aaa"], [obj2, "bbb"], [1, "ccc"]])
 ​
 console.log(map.get(obj1)); // aaa
 map.forEach((value, key, map) => {
     console.log(value, key, map);
 })
 for (const item of map) {
     // [{ name: 'zhangsan' }, 'aaa']
     // [{ name: 'lisi' }, 'bbb']
     // [1, 'ccc']
     console.log(item);
 }
 //  for of遍历出来的item是数组,数组里放的是[key,value],可以直接解构
 for (const [key, value] of map) {
     // { name: 'zhangsan' } aaa
     // { name: 'lisi' } bbb
     // 1 ccc
     console.log(key, value);
 }

11.12.2. 认识WeakMap和基本API使用

和Map类型相关的另外一个数据结构称之为WeakMap,也是以键值对的形式存在的。

那么和Map有什么区别呢?

  • 区别一:WeakMap的key只能使用对象,不接受其他的类型作为key;
  • 区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;

WeakMap常见的方法有四个:

  • set(key, value):在Map中添加key、value,并且返回整个Map对象;
  • get(key):根据key获取Map中的value;
  • has(key):判断是否包括某一个key,返回Boolean类型;
  • delete(key):根据key删除一个键值对,返回Boolean类型;

注意:WeakMap也是不能遍历的,因为没有forEach方法,也不支持通过for of的方式进行遍历;

11.12.3. WeakSet的应用场景(vue3响应式原理)

vue3响应式原理:监听对象的改变,并且对应的函数做出对应的响应

  • 比如obj1的name发生改变,对应的obj1NameFn1和obj1NameFn2函数就执行
 const obj1= {
     name: "aaa",
     age:18
 }
 function obj1NameFn1(){
     console.log('obj1NameFn1');
 }
 function obj1NameFn2(){
     console.log('obj1NameFn2');
 }
 function obj1AgeFn1(){
     console.log('obj1AgeFn1');
 }
 function obj1AgeFn2(){
     console.log('obj1AgeFn2');
 }
 const obj2= {
     name: "bbb",
     height: 180,
     address: "成都市"
 }
 function obj2NameFn1(){
     console.log('obj2NameFn1');
 }
 function obj2NameFn2(){
     console.log('obj2NameFn1');
 }
 ​
 // 为什么这里用WeakMap,如果obj1=null,WeakMap里面对obj1的引用也会解除
 const weakmap= new WeakMap()
 // 1.收集依赖关系
 // (1).对obj1收集关系
 const obj1Map= new Map()
 obj1Map.set("name",[obj1NameFn1,obj1NameFn2])
 obj1Map.set("age",[obj1AgeFn1,obj1AgeFn2])
 weakmap.set(obj1,obj1Map)
 // (2).对obj2收集关系
 const obj2Map=new Map()
 obj2Map.set("name",[obj2NameFn1,obj2NameFn2])
 weakmap.set(obj2,obj2Map)
 // 2.假如obj1.name发生了改变
 // Proxy/Object.defineProperty
 obj1.name="张三"
 const fns=weakmap.get(obj1).get('name')
 fns.forEach(element => element());

⚑ 下面是ES7(2016)

11.13. Array Includes

在ES7之前,如果我们想判断一个数组中是否包含某个元素,需要通过 indexOf 获取结果,并且判断是否为 -1。

在ES7中,我们可以通过includes来判断一个数组中是否包含一个指定的元素,返回布尔值

  • includes(item, 0):0表示从索引为0的地方开始检查
  • includes() 方法用于判断字符串是否包含指定的子字符串。
 const arr=[1,2,3,"a",NaN]
 if (arr.indexOf(2)!==-1) {
     console.log("cunzai");
 }
 if (arr.includes(NaN)) {
     console.log("youNaN");
 }

注意:indexOf 不能检查NaN是否存在,includes是可以检查的

11.14. 指数运算符

在ES7之前,计算数字的乘方需要通过 Math.pow 方法来完成。

在ES7中,增加了 ** 运算符,可以对数字来计算乘方。

 // 计算3的四次方
 const results= Math.pow(3,4)
 const results1= 3**4

⚑ 下面是ES8(2017)

11.15. Object.values

之前我们可以通过 Object.keys 获取一个对象所有的key,在ES8中提供了 Object.values 来获取所有的value值

 const obj={ name:"why", age:18 }
 console.log(Object.keys(obj)); // [ 'name', 'age' ]
 console.log(Object.values(obj)); // [ 'why', 18 ]
 // 用的少
 console.log(Object.values(['a','ac','zx'])); // [ 'a', 'ac', 'zx' ]
 console.log(Object.values('abcd')); // [ 'a', 'b', 'c', 'd' ]

11.16. Object entries

通过Object.entries 接收一个对象,返回它们键/值对的数组,

 const obj={ name:"why", age:18 }
 ​
 console.log(Object.entries(obj)); // [ [ 'name', 'why' ], [ 'age', 18 ] ]
 Object.entries(obj).forEach(item => { 
     // name why
     // age 18
     const [key, value]= item
     console.log(item[0], item[1]) 
 })
 // 如果传入一个数组或字符串,会将索引作为key
 // [ [ '0', 'abc' ], [ '1', 'cba' ], [ '2', 'cab' ] ]
 console.log(Object.entries(["abc","cba","cab"]));
 // [ [ '0', 'a' ], [ '1', 'b' ], [ '2', 'c' ] ]
 console.log(Object.entries("abc"));

11.17. String Padding

某些字符串我们需要对其进行前后的填充,来实现某种格式化效果,

ES8中增加了 padStart 和 padEnd 方法,分别是对字符串的首尾进行填充的。

 const sum = 'abcdef'
 // 在sum前面填充*,填充到总长度为7; 在sum后面填充-,填充到总长度为10;
 const nueSum = sum.padStart(7, '*').padEnd(10, '-') // *abcdef---
 // 应用场景:隐藏身份证前面数字
 const card = '513722199605212816'
 const newCard= card.slice(-4).padStart(card.length,'*') // **************2816

11.18. Trailing Commas

Trailing Commas:翻译为结尾的逗号;它允许在函数定义和调用时多加一个逗号:

 function foo(m,n,){ }
 foo(1,2)
 foo(1,2,)

11.19. Object.getOwnPropertyDescriptors

Object.getOwnPropertyDescriptor(对象,属性名);原型里写过了,获取对象属性描述符

⚑ 下面是ES9(2018)

Async iterators:后续迭代器讲解

Object spread operators:前面讲过了(对象的展开语法),注:对象的展开不是用迭代器实现的

Promise finally:后续讲Promise讲解

⚑ 下面是ES10(2019)

Symbol description:已经讲过了

Optional catch binding:后面讲解try cach讲解

11.20. flat flatMap

flat() :降维操作,方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

flatMap() :方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。

  • 注意一:flatMap是先进行map操作,再做flat的操作;flatMap中的flat相当于深度为1
  • 注意二:flat中的flat默认深度为1
 const nums=[10,20,[30,40],[[50,60],[70,80]]]
 const newNums= nums.flat(2) // [ 10, 20, 30, 40, 50, 60, 70, 80 ]
 ​
 // flatMap应用场景: 对数组每个item做完映射后,自动转为一维数组
 const message = ["hello world","helle js", "hello vue"]
 const word= message.flatMap(item=>item.split(" ")) // [ 'hello', 'world', 'helle', 'js', 'hello', 'vue' ]
 const word1= message.map(item=>item.split(" ")) // [ [ 'hello', 'world' ], [ 'helle', 'js' ], [ 'hello', 'vue' ] ]
 // 1.场景:数组扁平化 
 var arr = [[1, 2, 2, 3], [4, 5, 5, 6], [7, 8, 9, 10, [11, 12, 12, 13, [14]]]]
 ​
 // 2.实现代码
 const set = new Set()
 ​
 // 递归调用
 function foo(arr) {
   if (typeof arr === "object") {
     arr.flatMap(item => {
       foo(item)
     })
   } else {
     set.add(arr)
   }
 }
 ​
 const why = arr.flatMap(item => {
   foo(item)
 }
 )
 console.log(set); // Set(14) { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 }
 ​

11.21. Object.formEntries

Object.entries :将一个对象转换成 entries

Object.formEntries:将entries转换成 一个对象

 const entries= [ [ 'name', 'why' ], [ 'age', 18 ] ]
 ​
 // es10之前的做法
 const obj={}
 for (const iterator of entries) {
     obj[iterator[0]] = iterator[1]
 }
 console.log(obj); // { name: 'why', age: 18 }
 // es10
 console.log(Object.fromEntries(entries)); // { name: 'why', age: 18 }

11.22. trimStart和trimEnd

去除一个字符串首尾的空格,我们可以通过trim方法

如果单独去除前面或者后面的空格,ES10中给我们提供了trimStart和trimEnd;

 const message="   hello world   "
 console.log(message.trim()); // 去除首尾的空格
 console.log(message.trimStart()); // 去除头部的空格
 console.log(message.trimEnd()); // 去除尾部的空格

⚑ 下面是ES11(2020)

Dynamic Import:后续ES Module模块化中讲解。

Promise.allSettled:后续讲Promise的时候讲解。

import meta:后续ES Module模块化中讲解。

11.23. 大数字 BigInt

在早期的JavaScript中,最大的安全的int类型的数字是:MAX_SAFE_INTEGER

大于MAX_SAFE_INTEGER的数值,表示的可能是正确,也可能是不正确的,看运气,也就是没有安全保证

 const maxInt = Number.MAX_SAFE_INTEGER
 console.log(maxInt); // 9007199254740991
 console.log(maxInt + 1); // 9007199254740992
 console.log(maxInt + 2); // 9007199254740992

在ES11中,引入了新的数据类型BigInt,用于表示大的整数:

  • BitInt的表示方法是在数值的后面加上n
 const bigInt=9007199254740992n
 console.log(bigInt+10n); // 9007199254741002n
 console.log(bigInt+BigInt(10)); // 9007199254741002n

11.24. ?? Nullish Coalescing Operator

注意:如果node版本太低,es11以上的部分api是不支持的。如??

Nullish Coalescing Operator增加了空值合并操作符:??

应用场景:如果foo有值,则直接返回foo;无值则返回默认值

  • 之前我们通常用的是||,但是有个弊端,''和0的布尔值是false,

    • ES11前正确做法:var newFoo = foo !== null && foo !== void 0 ? foo : "defualt value";
  • ?? :左侧是undefined 或 null才返回右边的值,则可以弥补||的缺点,foo是''和0的话,直接返回foo

 let foo 
 // const newFoo= foo || "defualt value"
 const bar= foo ?? "defualt value"
 console.log(newFoo);

11.25. ?. Optional Chaining

Optional Chaining(可选链):也是ES11中新增一个特性,主要作用是让我们的代码在进行null和undefined判断时更加清晰和简洁

 const info = {}
 // TypeError: Cannot read properties of undefined,等于在执行undefined.name
 console.log(info.friends.name);
 ​
 // 不确定friends有值的情况下,可以用info.friends?.name
 // 如果 ?. 左边部分不存在,直接返回undefined,后面的都不会执行了,
 console.log(info.friends?.name); // undefined

11.26. Global This

在之前我们希望获取JavaScript环境的全局对象,不同的环境获取的方式是不一样的

  • 比如在浏览器中可以通过this、window来获取;
  • 比如在Node中我们需要通过global来获取;

那么在ES11中对获取全局对象进行了统一的规范:globalThis

  • 不管在哪种环境下,globalThis都表示当前环境下的全局对象
 console.log(globalThis);

11.27. for..in标准化

在ES11之前,虽然很多浏览器支持for...in来遍历对象类型,但是并没有被ECMA标准化。

  • 大部分遍历的是对象的key,小部分遍历的是对象的value

在ES11中,对其进行了标准化,for...in是用于遍历对象的key的

⚑ 下面是ES12(2021)

11.28. FinalizationRegistry

FinalizationRegistry类可以监听对象的销毁过程,创建对象时需要传入一个回调函数,当注册在FinalizationRegistry里面的对象被销毁时,会执行这个回调函数

如果注册了多个对象,可以给回调函数传入一个值,这样就可以区分是哪个对象被销毁了

可以通过调用register方法,注册任何你想要清理回调的对象,传入该对象和所含的值;

 const finalizationRegistry = new FinalizationRegistry((value)=>{
     console.log("注册在finalizationRegistry的对象被销毁了",value);
 })
 let obj={}
 let info={}
 // 将obj注册到finalizationRegistry对象中
 finalizationRegistry.register(obj,"obj对象")
 finalizationRegistry.register(info,"info对象")
 ​
 obj=null // 执行到这里是,obj不会立马被回收,GC是不定时回收
 info=null

11.29. WeakRefs

如果我们默认将一个对象赋值给另外一个引用,那么这个引用是一个强引用:

  • 如果我们希望是一个弱引用的话,可以使用WeakRef;
  • 也可以使用WeakSet,这里用WeakRef举例说明
 const finalizationRegistry = new FinalizationRegistry((value) => {
     console.log("注册在finalizationRegistry的对象被销毁了", value);
 })
 let obj = {name:"why"}
 let info = new WeakRef(obj)
 // 将obj注册到finalizationRegistry对象中
 finalizationRegistry.register(obj, "obj对象")
 ​
 obj = null // 执行到这里是,obj不会立马被回收,GC是不定时回收
 setTimeout(()=>{
     console.log(info.deref()?.name); // undefined
 },10000)

11.30. logical assignment operators

logical assignment operators:逻辑赋值操作符

 // 1. ||= 逻辑或赋值运算
 let message= "aaa"
 // 下面代码等同 message = message || "defalut value" 
 message ||= "defalut value" 
 ​
 // 2. &&= 逻辑与赋值运算 (用的少) 
 const obj={
     name:"why",
     foo(){
         console.log("sss");
     }
 }
 obj && obj.foo && obj.foo() // 一般&&用的多
 ​
 // 3. ??= 逻辑空赋值运算
 let foo=""
 foo ??= "defalut value" 
 console.log(foo); // ""

Numeric Separator:数字分隔符,数值的表示 里讲过了;

11.31. String.replaceAll:字符串替换;

replaceAll:会把p里面所有的dog替换为money

 const p = 'The quick y dog. The If dog reacted';
 //  The quick y monkey. The If monkey reacted
 console.log(p.replaceAll('dog', 'monkey'));

十二. Proxy-Reflect

Proxy-Reflect是es6中一个理解的难点,也是vue2-vue3响应式的原理

12.1. ES6之前的监听对象

我们先来看一个需求:有一个对象,我们希望监听这个对象中的属性被设置或获取的过程

  • 通过之前的属性描述符中的访问器属性描述符来做到;

但是这样做有什么缺点呢?

  • 首先,Object.defineProperty设计的初衷,不是为了去监听截止一个对象中所有的属性的。

    • 我们在定义某些属性的时候,初衷其实是定义普通的属性,但是后面我们强行将它变成了数据属性描述符。
  • 其次,如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么Object.defineProperty是无能为力的。

  • 所以我们要知道,存储数据描述符设计的初衷并不是为了去监听一个完整的对象。

 let obj ={
     name:"why",
     age:18
 }
 ​
 Object.keys(obj).forEach(key=>{
     let value= obj[key]
     Object.defineProperty(obj,key,{
         get(){
             console.log(`${key}被访问了`);
             return value
         },
         set(newValue){
             console.log(`${key}被赋值了`);
             value= newValue
         }
     })
 })
 ​
 obj.name="aa"
 console.log(obj.name);
 obj.age=32
 console.log(obj.age);