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类似。
总结:const和let不存在变量提升,存在暂时性死区,即只有声明之后才能使用变量;存在块级作用域,不得重复声明。
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的时候接受一个函数作为参数,这个函数有两个参数,分别为resolve和reject,前者是将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(微任务),
宏任务包括:、setInterval、setImmediate、I/O、UI rendering;
微任务包括:process.nextTick, Promise, Object.observe(已废弃), MutationObserver(html5新特性)
setTimeout/Promise等称为任务源,而进入任务队列的是他们制定的具体执行任务;来自不同任务源的任务会进入到不同的任务队列,其中setTimeout与setInterval是同源的;宏任务可以理解成每次执行栈执行的代码就是一个宏任务。
事件运行机制:
(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)的含义是指某个任务在主线程最早可得的空闲时间执行。它在“任务队列”的尾部添加一个事件,因此要等到同步任务和“任务队列”现有的时间处理完才会得到执行。
按照事件循环机制分析以上代码运行流程
- 首先,事件循环从宏任务(
macrotask)队列开始,这个时候,宏任务队列中,只有一个script(整体代码)任务;当遇到任务源(task source)时,则会先分发任务到对应的任务队列中去。 - 然后我们看到首先定义了两个
async函数,接着往下看,然后遇到了console语句,直接输出script start。输出之后,script 任务继续往下执行,遇到setTimeout,其作为一个宏任务源,则会先将其任务分发到对应的队列中。 - script 任务继续往下执行,执行了
async1()函数,前面讲过async函数中在await之前的代码是立即执行的,所以会立即输出async1 start。 遇到了await时,会将await后面的表达式执行一遍,所以就紧接着输出async2,然后将await后面的代码也就是console.log('async1 end')加入到microtask中的Promise队列中,接着跳出async1函数来执行后面的代码。 - script任务继续往下执行,遇到Promise实例。由于Promise中的函数是立即执行的,而后续的
.then则会被分发到 microtask 的Promise队列中去。所以会先输出promise1,然后执行resolve,将promise2分配到对应队列。 - script任务继续往下执行,最后只有一句输出了
script end,至此,全局任务就执行完毕了。 根据上述,每次执行完一个宏任务之后,会去检查是否存在 Microtasks;如果有,则执行 Microtasks 直至清空 Microtask Queue。 因而在script任务执行完毕之后,开始查找清空微任务队列。此时,微任务中,Promise队列有的两个任务async1 end和promise2,因此按先后顺序输出async1 end,promise2。当所有的 Microtasks 执行完毕之后,表示第一轮的循环就结束了。 - 第二轮循环依旧从宏任务队列开始。此时宏任务中只有一个
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属性。
可迭代对象有Array、Set、Map,手动添加迭代器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。
\