ES6相关

118 阅读14分钟

ES6相关

1.说说var/let/const之间的区别

var声明的对象是全局变量,且该变量存在变量提升,即会提前声明;var可以对变量多次声明,后面覆盖前面;在函数中使用var是局部变量,函数中不使用var,则为全局变量

var a = 20
function change(){
   a = 30
}
change()
console.log(a) // 30 

let声明的变量只在所在的代码块内有效,不存在变量提升,也不会提前声明;在相同作用域中let不可对变量重复声明;函数内部重新声明参数也不行。

function func(arg) {
  let arg;
}
func()
// Uncaught SyntaxError: Identifier 'arg' has already been declared

const声明一个只读常量,不能改变,必须初始化一个值;const只读的原理是保证变量指向的那个内存地址,而对于引用类型指向的是一个内存地址,保存的仅仅为一个指针,不能确保改变量的结构不变;其他情况和let类似。

总结:constlet不存在变量提升,存在暂时性死区,即只有声明之后才能使用变量;存在块级作用域,不得重复声明。

2.数组新增了哪些扩展?

将一个数组转为用逗号分隔的参数序列

console.log(...[1, 2, 3])
// 1 2 3

主要用于函数调用的时候,将一个数组变为参数序列

function push(array, ...items) {
  array.push(...items);
}
​
function add(x, y) {
  return x + y;
}
​
const numbers = [4, 38];
add(...numbers) // 42

可以将某些数据结构转为数组

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

能够更简单实现数组复制,注意:扩展运算符实现的是浅拷贝,和assign方法类似,和一个空数组合并,对象的扩展运算符同理

const a1 = [1, 2];
const [...a2] = a1;
// [1,2]
//上述代码等价于
const a2 = Object.assign({}, a1)

数组合并

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]

扩展运算符可以与解构赋值结合起来,用于生成数组

const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]
​
const [first, ...rest] = [];
first // undefined
rest  // []
​
const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []

可以将字符串转为真正的数组

[...'hello']
// [ "h", "e", "l", "l", "o" ]

定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组---重点

let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
​
let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);
​
let arr = [...map.keys()]; // [1, 2, 3]

如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错

const obj = {a: 1, b: 2};
let arr = [...obj]; // TypeError: Cannot spread non-iterable object

解决办法:定义迭代器属性

const obj = {
    a: 1,
    b: 2
}
​
obj[Symbol.iterator] = function() {
    let index = 0,
        self = this,
        keys = Object.keys(this)
    
    return {
        next(){
            if (index < keys.length) {
                return { 
                    value: self[keys[index++]], 
                    done: false
                }
            } else {
                return { 
                    value: undefined,
                    done: true
                }
            }
        }
    }
}
console.log([...obj]) // [1, 2] 输出一个数组for (let value of obj) {
    console.log(value) // 1, 2  输出两个参数
}

3.你是怎么理解ES6新增Set、Map两种数据结构的?

Set是一种叫做集合的数据结构

Map是一种叫做字典的数据结构

Set类似与数组,由一堆无序的、不重复的值构成的数据结构;也有增删改查的方法:

  • add()
  • delete()
  • has()
  • clear()

add()添加某个值,返回Set机构本身。

delete()删除并返回一个Boolean值,表示删除是否成功。

has()返回一个布尔值。

clear()清除,无返回值。

Set实现遍历的方法:

  • keys():返回键名的遍历器
  • values():返回键值的遍历器
  • entries():返回键值对的遍历器
let set = new Set(['red', 'green', 'blue']);
​
for (let item of set.keys()) {
  console.log(item);
}
// red
// green
// blue
  • forEach():使用回调函数遍历每个成员,forEach没有返回值。
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9

扩展运算符和Set 结构相结合实现数组或字符串去重。

let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)]; // [3, 5, 2]

总结:Set集合是一种ES6之后的新的数据结构,类似数组,由一堆无序、没有重复的数据构成。同样也具有增删改查、遍历等方法。特点有可以和扩展运算符结合实现数组或字符串去重。

Map是有键值对的有序列表,键值都可以是任何类型。和object类型基本一致,能实现他的所有实现。

增删改查等方法:

  • size属性,返回Map结构的成员总数。
  • set(),设置键名key对应的键值为value,然后返回整个 Map 结构。
  • get(),传入键,返回值
  • has()
  • delete()
  • clear()

遍历同理。

选择Object还是Map? (主要区别在于内存和性能上的区别)

  • 相同大小的内存空间,Map可以存更多的键值对。
  • 插入键值对时,Map的性能会更好
  • 键值对较少时用Map搜索性能更好,反之Object更好
  • 涉及大量删除操作时Map的delete()方法性能更好。

WeakMap数据结构,为弱映射的集合类型,这里的weak描述的是js垃圾回收程序对待弱映射中‘键’的方式。即该类型的键只能是Object或者继承自Object的类型

实现真正的私有变量?

WeakMap不会妨碍垃圾回收,所以适合保持关联元数据?

WeakSet,这里的weak描述的是js垃圾回收程序对待弱映射中‘值’的方式。即该类型的值只能是Object或者继承自Object的类型

4.你是怎么理解ES6中 Promise的?使用场景?

promise是异步编程的一种解决方案,解决回调地狱的问题,使代码更加合理、阅读性更强。

Promise是一个构造函数,new一个promise的时候接受一个函数作为参数,这个函数有两个参数,分别为resolvereject,前者是将promise对象的状态从未完成变为成功,后者为从未完成变为失败。

Promise实例具有以下几种方法:

  • then()
  • catch()
  • finally()

then是实例状态发生改变时的回调函数,第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数。返回一个新的Promise实例,因此promise可以写成链式结构。

catch()用于指定发生错误时的回调函数。

Promise构造函数也有一系列的方法:

  • all(),Promise.all()方法用于将多个 Promise实例,包装成一个新的 Promise实例
  • 接受一个数组(迭代对象)作为参数,数组成员都应为Promise实例

类方法,多个 Promise 任务同时执行。

如果全部成功执行,则以数组的方式返回所有 Promise 任务的执行结果。 如果有一个 Promise 任务 rejected,则只返回 rejected任务的结果。

  • race() , 同上
  • allSettled(),同
  • resolve(), 将传入参数转为promise对象
  • reject(),传入参数作为后续方法的参数,返回一个新的Promise实例
  • try()

总结: 解决回调地狱问题,使代码更具合理性、可读性。

使用方法:

首先new一个Promise对象

然后调用上一步返回promise对象的then方法,注册回调函数,根据实际情况给回调函数传入参数,如果后续还有步骤,则必须传入参数resolve

最后通过注册catch处理前面所有出现的异常。

Promise 在事件循环中的执行过程是怎样的?

promise在初始化时,传入的函数是同步执行的,会立刻执行,而注册then方法里的回调函数时异步的,需要先注册到任务队列中,继续向下执行同步代码,直到同步代码执行完毕才会promise回调函数,没有回调的话接着执行下一个事件循环。

事件循环就是指主线程从事件队列中读取消息、执行的过程。

事件队列就是一个存储着执行任务的序列,按照顺序挨个执行。

一个线程中,事件循环是唯一的,但是可以有多个事件队列,包括macro-task(宏任务)和micro-task(微任务),

宏任务包括:、setIntervalsetImmediateI/OUI rendering

微任务包括:process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)

setTimeout/Promise等称为任务源,而进入任务队列的是他们制定的具体执行任务;来自不同任务源的任务会进入到不同的任务队列,其中setTimeoutsetInterval是同源的;宏任务可以理解成每次执行栈执行的代码就是一个宏任务。

事件运行机制:

(1)执行一个宏任务(栈中没有的话就从事件队列中获取)

(2)执行过程中如果遇到微任务,就将它添加到微任务的任务队列中;

(3)宏任务执行完毕后,立即执行当前微任务队列的所有微任务;

(4)当前微任务执行完毕,开始检查渲染,然后GUI线程接管渲染;

(5)渲染完毕后,JS线程继续接管,开始下一个宏任务。

而promise的具体操作就处于微任务队列中

代码实例:

async function async1() {           
    console.log("async1 start");  //(2)        
    await  async2();            
    console.log("async1 end");   //(6)    
}        
async  function async2() {          
    console.log( 'async2');   //(3)     
}       
console.log("script start");  //(1)      
setTimeout(function () {            
    console.log("settimeout");  //(8)      
},0);        
async1();        
new Promise(function (resolve) {           
    console.log("promise1");   //(4)         
    resolve();        
}).then(function () {            
    console.log("promise2");    //(7)    
});        
console.log('script end');//(5)

先按顺序执行同步代码 从‘script start‘开始,

执行到setTimeout函数时,将其回调函数加入队列(此队列与promise队列不是同一个队列,执行的优先级低于promise。

然后调用async1()方法,await async2();//执行这一句后,输出async2后,await会让出当前线程,将后面的代码加到任务队列中,然后继续执行test()函数后面的同步代码

继续执行创建promise对象里面的代码属于同步代码,promise的异步性体现在then与catch处,所以promise1被输出,然后将then函数的代码加入队列,继续执行同步代码,输出script end。至此同步代码执行完毕。

开始从队列中调取任务执行,由于刚刚提到过,setTimeout的任务队列优先级低于promise队列,所以首先执行promise队列的第一个任务,因为在async函数中有await表达式,会使async函数暂停执行,等待表达式中的 Promise 解析完成后继续执行 async 函数并返回解决结果。

所以先执行then方法的部分,输出promise2,然后执行async1中await后面的代码,输出async1 end。。最后promise队列中任务执行完毕,再执行setTimeout的任务队列,输出settimeout。

setTimeout(fn,0)的含义是指某个任务在主线程最早可得的空闲时间执行。它在“任务队列”的尾部添加一个事件,因此要等到同步任务和“任务队列”现有的时间处理完才会得到执行。

按照事件循环机制分析以上代码运行流程

  1. 首先,事件循环从宏任务(macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。
  2. 然后我们看到首先定义了两个async函数,接着往下看,然后遇到了 console 语句,直接输出 script start。输出之后,script 任务继续往下执行,遇到 setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中。
  3. script 任务继续往下执行,执行了async1()函数,前面讲过async函数中在await之前的代码是立即执行的,所以会立即输出async1 start。 遇到了await时,会将await后面的表达式执行一遍,所以就紧接着输出async2,然后将await后面的代码也就是console.log('async1 end')加入到microtask中的Promise队列中,接着跳出async1函数来执行后面的代码。
  4. script任务继续往下执行,遇到Promise实例。由于Promise中的函数是立即执行的,而后续的 .then 则会被分发到 microtask 的 Promise 队列中去。所以会先输出 promise1,然后执行 resolve,将 promise2 分配到对应队列。
  5. script任务继续往下执行,最后只有一句输出了 script end,至此,全局任务就执行完毕了。 根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。 因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中, Promise 队列有的两个任务async1 endpromise2,因此按先后顺序输出 async1 end,promise2。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。
  6. 第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个 setTimeout,取出直接输出即可,至此整个流程结束。

Promise 解决的痛点还有其他方法可以解决,

比如setTimeout、事件监听、回调函数、Generator函数,async/await

Promise 的业界实现都有哪些?

  • promise可以支持多个并发的请求,获取并发请求中的数据
  • promise可以解决可读性的问题,异步的嵌套带来的可读性的问题,它是由异步的运行机制引起的,这样的代码读起来会非常吃力

Promise 还存在哪些问题?

  • promise一旦执行,无法中途取消
  • promise的错误无法在外部被捕捉到,只能在内部进行预判处理
  • promise的内如何执行,监测起来很难
  • 于是引入了async/await来处理异步问题

总结:

Promise是异步编程的一种解决方案,用链式结构解决回调地狱的问题,以同步的方式来表达异步的操作,使代码更加合理、阅读性更强。

可以把Promise理解成一个容器,里面保存着一个异步操作的结果。

Promise的实例有三个状态、两个过程、三个主要方法。构造函数同样也有一系列的方法。

5.手写promise.all

下述代码只考虑了传入参数为数组情况,传入参数可以是任意一个iterator类型的参数(Array、Map、Set)或者一个promise实例

function PromiseAll(promises) {
    //promise.all肯定返回一个promise对象
    return new Promise((resolve, reject) => {
        if(!Array.isArray(promises)) {
            throw new TypeError("传入的参数必须为一个数组") // 限制传入类型,也可以用Array.from()转化为数组?
        }
        let result = [] // 存放结果
        let count = 0 // 记录有几个结果完成了,当所有都执行完毕即count=promises.length时返回result
        
        //对数组中每个元素进行遍历
        for (let item of promises) {
            Promise.resolve(item) // 先将数组中的数据转为promise对象
            .then((data) => {
                result[item] = data // 将转换成的promise对象传入一个新的数组
                count ++
                if(count === promises.length) {
                    resolve(result) // 全部转换完成后resolve
                }
            }).catch(err => {
                reject(err)
            })
        }
    })
}
// 验证
const promise1 = Promise.resolve(3)
const promise2 = 12
const promise3 = new Promise(function(resolve, reject) {
    setTimeout(resolve, 100, 'foo')
})
​
Promise.all([promise1, promise2, promise3]).then(values => {
    console.log(values) 
})
// 输出结果[ 3, 12, 'foo' ]

总结:

Promise.all传入参数可以是任意一个iterator类型的参数(Array、Map、Set),一般是一个数组,数组中的元素可以是一个promise实例,也可以是任意基本类型数据,如果不是promise实例会被自动转换为promise实例对象;最终会在then的第一个参数里得到一个数组,存储着所有传入数组里所有元素被resolve后得值,即被转换的promise实例对象。如果有任意一个元素被reject了,则会立即捕获reject的原因,其他的结果也没有用了,状态会转为rejected

6.手动添加迭代器iterator属性。

可迭代对象有ArraySetMap,手动添加迭代器iterator属性,从而可以使用扩展运算符,将不可迭代对象变成可迭代对象,

通过设置Symbol.iterator属性

const obj = {
    a: 1,
    b: 2
}
​
obj[Symbol.iterator] = function() {
    let index = 0,
        self = this,
        keys = Object.keys(this)
    
    return {
        next(){
            if (index < keys.length) {
                return { 
                    value: self[keys[index++]], 
                    done: false
                }
            } else {
                return { 
                    value: undefined,
                    done: true
                }
            }
        }
    }
}
console.log([...obj]) // [1, 2] 输出一个数组for (let value of obj) {
    console.log(value) // 1, 2  输出两个参数
}

7.for of 和 for in

  • for in :遍历方法,可遍历对象和数组
  • for of :遍历方法,只能遍历数组,不能遍历非iterable对象

先看for in

const obj = { age: 22, gender: '男' }
const arr = [1, 2, 3, 4, 5]for(let key in obj) {
 console.log(key) //age gender
}
​
for(let index in arr) {
 console.log(index) //0 1 2 3 4
}
​
​
//for of 只能遍历数组
for(let item of arr) {
    console.log(item) // 1 2 3 4 5
}
​

8.ES8用于获取对象的键值对

const obj = {
 name: '饭',
 age: 22,
 gender: '男'
}
​
const keys = Object.keys(obj)
const values = Object.values(obj)
const entries = Object.entries(obj)
​
console.log(keys) //[ 'name', 'age', 'gender' ]
console.log(values) //['饭', 22, '男']
console.log(entries) //['name': '饭', 'age': 22, 'gender': '男']

得到的是键值对的数组

Object.fromEntries()Object.entries()功能类似,不过前者是把键值对转为对象

console.log(fromEntries) //{'name': '饭', 'age': 22, 'gender': '男'}

9.对于ES6有哪些理解?

  • let和const的引入,使变量声明更加灵活
  • 变量的解构赋值,用于交换变量的值、函数返回多个值、提取json数据等
  • 数组的一些扩展,如如数组的常用的includes,兼容性通用性更好?、扩展运算符主要用于函数调用、.keys等获取数组的键值对?、flat扁平化、find返回符合条件的数组、Array.from()转为真正数组等
  • 对象的一些扩展,Object.is比较两个值是否严格相等、Object.assign()用于对象合并、Object.assign()获取对象的键值对
  • 新数据类型Symbol,防止命名冲突?
  • 新数据结构Set和Map,Set类似数组但是元素不可重复,可以用于去重,Map类似对象,键值对可以是任意类型。
  • 引入Reflect来逐渐替代Object,前者为函数行为操作、后者为命令式行为。
  • 引入Promise。
  • 引入async函数。
  • 引入Class。

\