前言
本文将介绍ES2016以及之后版本中常用的知识点以及该知识点与之前版本用法的对比,更多的了解知识点背后的原理,从而更加深刻的理解ES6+到底好在哪里。
1、let和const
let,const都是用来声明变量,用来替代老语法的var关键字,与var不同的是,它们会创建一个块级作用域(一般一个花括号内是一个新的作用域)
特点:
- 不存在变量提升
- 不允许重复声明
- 暂时性的死区——只要块级作用域内用let,const声明的变量,那这个变量就绑定到这个区域了,不受外部的影响
- 块级作用域
- let和var全局声明时,var可以通过window的属性访问而let不能。
- const声明的变量必须赋值,即不能只声明不初始化
- const声明的变量不能改变,如果声明的变量是引用类型(数组或对象),则不能改变它的内存地址
场景案例:
场景1:经典面试题
// 使用var
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
} // => 5 5 5 5 5
// 使用let
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
});
} // => 0 1 2 3 4
知识点:
- for循环:设置循环变量的部分(即圆括号部分)是一个父作用域,而循环体内部(即中括号部分)是一个单独的子作用域,所以可以在循环体内部访问到i的值。
- 变量i:使用var声明的变量i,在预编译阶段会有变量提升,会提升到当前作用域的顶部并初始化赋值为undefined,var i=0在for循环中只执行一次;使用let声明的变量i,不存在变量提升,只有代码运行到let i语句的时候才开始初始化并赋值,在for循环中当前的i只在本轮循环中有用,可以理解为每次循环的i其实都是一个新的变量,JS内部引擎会记住上次循环结果赋值给下个循环。
- setTimeout:会将一个匿名的回调函数推入异步队列,并且回调函数还具有全局性,在非严格模式下this会指向window。这里就会有这样一个问题:setTimeout里的函数是何时进入任务队列?,请看下面的代码:
setTimeout(function () {
console.log('ab')
}, 5 * 1000) //这里改为0ms
for (let i = 0; i <= 5000000; i++) {
if (i === 5000000) {
console.log(i)
}
}
说明:setTimeout里的函数在setTimeout执行的时候,就开始计时,计时完成(这里就是5s之后)才进入任务队列,当主执行栈执行完就开始执行任务队列。
场景2:const使用
const a; //Uncaught SyntaxError: Missing initializer in const declaration
const a=1;
a=2; //Uncaught TypeError: Assignment to constant variable.
const obj = {
a: 1
}
obj.a = 2
obj = { b: 1 } //Uncaught TypeError: Assignment to constant variable.
let a = 1
console.log(window.a)//undefined
var b=1;
console.log(window.b)//1
2、解构赋值
解构赋值主要就是 数组的解构赋值和 对象的解构赋值,至于其他数据类型的解构赋值都与这两种有关系,对象的解构赋值的内部机制,是先找到 同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
注意点:
- 数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
- 只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
- 对象的解构赋值可以取到继承的属性。
对象的解构赋值
场景1:内部机制
let {
name: nameOne,
test: [{
name: nameTwo
}]
} = {
name: 'wx',
test: [{
name: 'test'
}]
}
//简化版
let { name, test } = {
name: 'wx',
test: [{
name: 'test'
}]
}
说明:这里等号左边真正声明的变量其实是nameOne
和nameTwo
,然后根据它们所对应的位置找到等号右边对应的值,从而进行赋值。
场景2:对象的方法
// 例一
let { sin, cos } = Math;
// 例二
const { log } = console;
log('hello') // hello
//vuex中action中的方法
//不使用对象解构:
const actions = {
getMainTaskList(ctx) {
ctx.commit('setMainTask')
},
}
//使用对象解构:
const actions = {
getMainTaskList({ commit, state, dispatch }) {
commit('setMainTask', { mainTaskData: JSON.parse(res) })
},
}
场景3:嵌套解构的对象
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
// 以前的写法:
let x = obj.p[0]
let y = obj.p[1].y
// 解构写法:
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
数组的解构赋值
// 老式写法
const local = 'wx-18'
const splitLocale = locale.split("-");
const language = splitLocale[0];
const country = splitLocale[1];
// 解构赋值写法
const [language, country] = locale.split('-');
其他类型的解构赋值
//字符串
const [a, b, c, d, e] = 'hello';
//数值和布尔值
let {toString: s} = 123;
s === Number.prototype.toString // true
console.log(s);//function toString()[查看详细的解释](https://segmentfault.com/q/1010000005647566)
//undefined和null
注意:由于`undefined`和`null`无法转为对象,所以对它们进行解构赋值,都会报错。
3、扩展运算符
扩展运算符主要是两大类:数组和对象的扩展运算符。使用三个点点(...),后面跟含有 iterator接口的数据结构
数组的扩展运算符
场景1:复制数组
// ES5的复制数组-复制了指针(浅)
const a1 = [1, 2];
const a2 = a1;
a2[0] = 2;
// ES5的复制数组-深拷贝
const a1 = [1, 2];
const a2 = a1.concat();//a1.slice()
a2[0] = 2;
// ES6复制数组-深拷贝
const a1 = [1,2]
const a2 = [...a1] //const [...a2] = a1
a2[0] = 2;
场景2:合并数组
//ES5合并数组
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = arr1.concat(arr2) //["a", "b", "c"]
// ES6合并数组
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = [...arr1,...arr2] //["a", "b", "c"]
场景3:与解构赋值结合使用
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first) //1
console.log(rest) //[2, 3, 4, 5]
//将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
const [...rest,last] = [1, 2, 3, 4, 5];//Uncaught SyntaxError: Rest element must be last element
场景4:类对象转数组
任何定义了遍历器( Iterator)接口的对象,都可以用扩展运算符转为真正的数组。
// 例1:
let nodeList = document.querySelectorAll('div');//类数组对象,并且部署了iterator接口
let arr = [...nodeList];
// 例2:
let arrayLike = {
'0': 'a',
'1': 'b',
length: 2
};
// Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
let arr = [...arrayLike];
//当然可以通过`Array.from(arrayLike)`将它转为真正的数组
let arr = Array.from(arrayLike)
对象的扩展运算符
let obj1 = {
a:1,
b:1,
}
let obj2 = {
...obj1,
c:1
}
console.log(obj2);//{a: 1, b: 1, c: 1}
//vuex中
computed: {
...mapGetters('raplaceBattery', {
runningTask: 'runningMainTask'
})
},
methods:{
...mapActions('raplaceBattery', [
'getMainTaskList'
]),
}
4、数组和对象的遍历方式
数组和对象的遍历方式有很多种,常用的for循环,for...in循环以及ES6提出的for...of循环,这些方式有各自的使用场景以及优缺点。
数组的遍历方式
以下遍历的数组都为:let arr = [1,2,3,4]
方式1:for
//写法比较麻烦,但是常用
for (let i = 0; i < arr.length; i++) {
console.log('for循环:',arr[i]);
}
//使用break跳出循环,return会报错
for (let i = 0; i < arr.length; i++) {
if(i === 2) break
console.log('for循环:',arr[i]);//1,2
}
方式2:forEach
arr.forEach(ele => {
console.log('forEach循环:',ele);
});
//缺点:无法跳出循环,break直接回报错,return只是跳出本次循环
arr.forEach(ele => {
if(ele === 2) return;
console.log('forEach循环:',ele);//1,2,4
});
方式3:map
返回新的数组,不改变原数组
let mapArr = arr.map((item,index,arr)=>{
if(index === 2) return;//只是跳出本次循环,
return item+1
})
console.log(arr);//[1, 2, 3, 4]
console.log(mapArr);//[2, 3, undefined, 5]
//map有一个选填的返回函数里面的this的参数
let mapArr = arr.map(function (e) {
return e + this; // 此处的 this为10
}, 10);
console.log(mapArr);//[11, 12, 13, 14]
这里为什么要用回调函数而不用箭头函数?
答:因为箭头函数里的this指向和函数里的this指向不同。请了解箭头函数的this指向(后续)
方式4:filter,find,findIndex
filter:返回新数组,不改变原数组,主要用于过滤
find:返回 第一个判断条件的 元素,不是数组
findIndex:返回 第一个判断条件的 元素的索引
let filterArr = arr.filter((item,index,arr)=>{
return item > 1
})
console.log(filterArr);//[2, 3, 4]
//find
let findArr = arr.find((value,index,arr)=>{
return value > 2
})
console.log(findArr);//3
//findIndex
let findIndexEle = arr.findIndex((val,i,arr)=>{
return val > 2
})
console.log(findIndexEle);// 2
方式5:every和some(返回布尔值)
every:只要有一个不符合,返回false,全符合,返回true
some:只要有一个符合,返回true,全不符合,返回false
let everyBool = arr.every((val, i, arr) => {
return val < 3
})
console.log(everyBool);//false
let someBool = arr.some((val,i,arr)=>{
return val > 2
})
console.log(someBool);//true
方式6:keys(),values(),entries()
ES6提供的新方法用于遍历数组,返回每一个索引组成的遍历器对象
let keysArr = arr.keys()
console.log(keysArr);//Array Iterator {}
//这种方式不可以哦
arr.keys().forEach(ele => {
console.log(ele);//Uncaught TypeError: arr.keys(...).forEach is not a function
});
//for...of可以
for (const index of arr.keys()) {
console.log(index);//0,1,2,3
}
for (const val of arr.values()) {
console.log(val);//1,2,3,4
}
for (const [index,ele] of arr.entries()) {
console.log(index,ele);
}
//0 1
//1 2
//2 3
//3 4
方法7:for...of循环
遍历所有部署了iterator接口的数据结构的方法,如:数组,Set,Map,类数组对象,如 arguments 对象、DOM NodeList 对象,Generator 对象,字符串
场景1:用于set和map结构
遍历set结构返回的是 一个值,遍历map结构返回的是一个数组
遍历的顺序是各个成员添加的顺序
let setArr = new Set([1,2,3,3])
let mapObj = new Map().set('b',1).set('a',2)
for (const item of setArr) {
console.log(item);//1,2,3
}
for (const item of mapObj) {
console.log(item);//item是数组,别搞错了
}
//["b", 1]
//["a", 2]
for (const [item, index] of mapObj) {
console.log(item, index);
}
//b 1
//a 2
场景2:类数组对象(必须部署了Iterator接口)
//字符串
const str = 'wx'
for (const item of str) {
console.log(item);//w,x
}
//DOM NodeList对象
let ps = document.querySelectorAll('div')
for (const p of ps) {
console.log(p);
}
//arguments对象
function writeArgs(){
for (const item of arguments) {
console.log(item);
}
}
writeArgs('1',2)//1,2
//rest的方式
function writeArgs(...rest){
for (const item of rest) {
console.log(item);
}
}
writeArgs('1',2)//1,2
//没有部署的类数组对象,通过Array.from转为数组
let likeArr = {length:2,0:'1',1:'2'}
for (const item of Array.from(likeArr)) {
console.log(item);//1,2
}
场景3:普通对象
普通对象不能使用for...of遍历,因为没有部署Iterator接口,一般使用for...in进行键名遍历
let obj = {
a:1,
b:2,
c:3
}
for (const item of obj) {
console.log(item);//Uncaught TypeError: obj is not iterable
//你非要使用for...of呢?你调皮了
方式1:
for (const item of Object.keys(obj)) {
console.log(item);//a,b,c
}
方式2:使用Generator函数进行包装
function* packObj(obj){
for (const key of Object.keys(obj)) {
yield [key,obj[key]]
}
}
for (const [key,value] of packObj(obj)) {
console.log(key,value);
}
}
//a 1
//b 2
//c 3
对象的遍历方式
以下遍历的对象都为:
Object.prototype.name= 'wx'//给原型加了name属性
let obj = {
a:1,
b:2,
c:3,
[Symbol('mySymbol')]: 4,
}
//Object.getOwnPropertyDescriptor(obj,'enumTemp').enumerable = false //这种方式无效
Object.defineProperty(obj, 'enumTemp', {
value: 5,
enumerable: false
})//添加一个不可枚举属性enumTemp
1. for...in
特点:遍历对象 自身的和 继承的可枚举属性(不含Symbol属性), 不建议使用
for (const key in obj) {
console.log(key);//a,b,c,name
}
2. Object.keys(obj),Object.entries(obj)
特点:只包括对象自身的可枚举属性--(不含Symbol属性,不含继承的), 推荐使用
Object.keys(obj).forEach(item=>{
console.log(item);//a,b,c
})
for (const [item,index] of Object.entries(obj)) {
console.log(item,index);
}
//a 1
//b 2
//c 3
3. Object.getOwnPropertyNames(obj)
特点:包括对象自身所有的属性(包括不可枚举的)--(不含Symbol属性,不含继承的)
console.log(Object.getOwnPropertyNames(obj));
//["a", "b", "c", "enumTemp"]
4. Object.getOwnPropertySymbols(obj)
特点:返回数组包括自身所有symbol属性的键名
console.log(Object.getOwnPropertySymbols(obj));//[Symbol(mySymbol)]
let sym = Object.getOwnPropertySymbols(obj).map(item=>obj[item])
console.log(sym);//[4]
5、Proxy、Reflect和Object.defineProperty
它们都是为了操作 对象而设计的API,并且主要用于 监听数据变化以及 响应式能力
1. Object.defineProperty
具体更详细的了解请查看:Object.defineProperty
//基本形式
let obj = {};
Object.defineProperty(obj, "num", {
value: 1,
writable: true,
enumerable: true,
configurable: true
});
console.log(obj.num) //1
//get,set形式
let obj = {};
let value = 2;
Object.defineProperty(obj, 'num', {
get: function () {
console.log('执行了 get 操作')
return value
},
set: function (newVal) {
console.log('执行了 set操作');
value = newVal
},
enumerable: true,
configurable: true
})
console.log(obj.num); // 执行了 get 操作 //2
2. Proxy
Tips:vue3.0使用Proxy代替Object.defineProperty实现数据响应式。
它是一个 “拦截器”,访问一个对象之前,都必须先经过它,对比defineProperty只有get,set两种处理拦截操作,而Proxy有13种,极大增强了处理能力,并且也消除了Object.defineProperty存在的一些局限问题
它为何优秀呢?
- 它有13种操作拦截对象的方法,如:ownKeys(target),apply(target, object, args),defineProperty(target, propKey, propDesc)。。。
- 消除了Object.defineProperty存在的一些局限问题:对属性的添加、删除动作的监测,对数组基于下标的修改,对于 .length修改的监测,对 Map、Set、WeakMap 和 WeakSet 的支持。(尤大Vue3.0说的)
//1、基础用法
let proxy = new Proxy({}, {
get(target, key, receiver) {
console.log('get 操作');
console.log(receiver);//Proxy {name: "wx"}
return target[key]
},
set(target, key, val, receiver) {
console.log('set 操作');
console.log(receiver);//Proxy {}
target[key] = val
}
})
proxy.name = 'wx' //set 操作
console.log(proxy.name); //get 操作 //wx
//2、has用法,拦截propKey in proxy的操作,返回一个布尔值
let handler = {
has(target, key) {
if (key[0] === '_') {
return false
}
return key in target
}
}
let target = { prop: 'foo', _prop: 'foo' };
let proxy = new Proxy(target, handler)
console.log('_prop' in proxy)//false
//3、deleteProperty方法拦截delete操作,返回布尔值
let target = { prop: 'foo', _prop: 'foo' };
let proxy = new Proxy(target, {
deleteProperty(target, key) {
if (key[0] === '_') {
return false
}
delete target[key]
return true
}
})
console.log(delete proxy['_prop']) //false
console.log(target) //{prop: "foo", _prop: "foo"}//这里并没有删除‘_prop’属性
//4、apply方法拦截函数的调用,call和apply操作,它有三个参数,分别是:目标对象,目标对象的上下文(this)和目标对象的参数数组
let target = () => 'target function'
let proxy = new Proxy(target, {
apply(target, ctx, args) {
return 'apply proxy'
}
})
console.log(proxy()) //apply proxy
3. Reflect
它和Proxy一样,也是用来处理对象,经常与proxy搭配使用,用来保证这些方法 原生行为的正常执行,所以它是服务于proxy的。
//1、基本用法
let obj = {
'a': 1,
'b': 2
}
let proxy = new Proxy(obj, {
get(target, key) {
console.log('get', target, key);
return Reflect.get(target, key);
},
deleteProperty(target, key) {
console.log('delete' + key);
return Reflect.deleteProperty(target, key);
},
has(target, key) {
console.log('has' + key);
return Reflect.has(target, key);
}
});
console.log(proxy.b);
//get {a: 1, b: 2} b
//2 //原生行为
//2、has用法
let myObject = {
foo: 1,
};
// 旧写法
console.log('foo' in myObject);// true
// 新写法
console.log(Reflect.has(myObject, 'foo'));// true
//3、deleteProperty用法
let myObject = {
foo: 1,
b:2
};
// 旧写法
console.log(delete myObject['foo'])//true
console.log(myObject);//{b: 2}
// 新写法
console.log(Reflect.deleteProperty(myObject,'b'));//true
console.log(myObject);//{}
6、Promise 对象
Promise是解决异步编程提出的新的解决方案,它相对于之前的 回调函数方案在很多方面做了改进,更加的合理和强大。
理解Promise之前建议先了解 异步, 事件循环以及 回调函数
- 回调函数
回调函数是之前用来解决异步编程常用的方式,大概的流程是:前端发出一个请求,进入浏览器的http请求线程,等收到响应后,将该回调函数推入异步队列,等处理完主线程的任务后会逐个读取异步队列中的回调并开始执行。
1、第三方库信任问题及错误处理
ajax('http://localhost:3000',()=>{
console.log('执行回调');
})
上面这个例子是最常见的一个回调例子,但是它有什么问题呢?试想一下,如果这个请求在网络环境不好的情况下,进行了超时重试的操作,那么这个回调函数就会执行多次,另外一种情况,要是这个请求失败了,那么它失败的错误信息怎么拿到,这些都是回调函数可能存在的问题,这明显不是我们希望看到的结果。
2、回调地狱
//引用《你不知道的JavaScript(中卷)》
listen("click", function handler(evt) {
setTimeout(function request() {
ajax("http://some.url.1", function response(text) {
if (text == "hello") {
handler();
} else if (text == "world") {
request();
}
});
}, 500);
});
总结:
- 多重嵌套,导致回调地狱
- 代码逻辑复杂之后,很难理清代码结构
- 第三方库信任问题及错误处理不透明
- Promise
它是一个构造函数,用来生成promise实例对象,它有两个参数(是函数),一般推荐命名为:resolve和reject
特点:
(1)、它有三种状态:pending
(进行中)、fulfilled
(已成功)、rejected
(已失败)
(2)、它的状态改变只有两种可能:pending
到fulfilled
或者pending
到rejected
(3)、promise新建后会立即执行
// 基础用法
let p = new Promise((resolve, reject) => {
if (true) {
return resolve('1') //resolve(new Promise())
// 后面写的代码没用了
} else {
reject('error')
}
})
p.then((val) => {
console.log('fulfilled:', val);
}).catch(e => { //推荐使用catch进行错误处理,可以检测到promise内部发生的错误
console.log('catch', e);
}).finally(()=>{...})
//对比两种方式
// bad
request(url, function (err, res, body) {
if (err) handleError(err);
fs.writeFile('1.txt', body, function (err) {
request(url2, function (err, res, body) {
if (err) handleError(err)
})
})
});
// good
request(url)
.then(function (result) {
return writeFileAsynv('1.txt', result)
})
.then(function (result) {
return request(url2)
})
.catch(function (e) {
handleError(e)
});
1. Promise.all([...])
将多个Promise(p1,p2,p3)实例包装成一个新的promise实例,这个新实例的状态由p1,p2,p3共同决定。
何时用它?当多个任务之间没有必然联系,它们的顺序并不重要,但是必须都要完成才能执行后面的操作
const p1 = request("http://some.url.1/");
const p2 = request("http://some.url.2/");
Promise.all([p1, p2]).then(([p1Result, p2Result]) => {
// 这里p1和p2全部响应之后
return request("http://some.url.3/")
}).then((result) => {
console.log(result);
}).catch(e => {
console.log(e);
})
2. Promise.race([...])
它与Promise.all([...])类似,只是它只关心谁先第一个完成Promise协议,一旦有一个完成,它就完成。即各个promise之间(p1,p2,p3...)存在“竞争”关系
const p = Promise.race([
request('/fetch-request'),
new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('request timeout'))
}), 5 * 1000
})
]);
p.then(console.log)
.catch(console.error);
上面的代码中,5秒之内无法从request
中返回结果,p的状态就会变为reject
,从而执行catch
3. Promise.resolve()和Promise.reject()
有时候需要将现有的对象 转化为promise对象进行处理,如deferred
或者thenable
对象
注意:立即resolve()的promise对象,是在“ 本轮事件循环”结束时执行,而不是等到下一轮才执行
//deferred转为promise
const deferToPromise = Promise.resolve($.ajax('/a.json'))
//thenable转promise
let thenable = {
then(resolve, reject) {
resolve(11)
}
};
let p1 = Promise.resolve(thenable);
p1.then((val) => {
console.log(val);//11
});
//纯对象转promise
let obj = {
a:1,
b:2
}
const p = Promise.resolve(obj)
p.then((v)=>{
console.log(v);//{a: 1, b: 2}
}).finally(()=>{
console.log('aaa');
})
7、async...await函数
它被称为异步编程的终极解决方案,它进一步 优化了promise的写法,用 同步的方式书写异步代码,并且能够更优雅的实现异步代码的顺序执行。
它返回一个promise对象。
任何一个await
语句后面的 Promise 对象变为reject
状态,那么整个async
函数都会中断执行吗?。
// 基本用法
async function request(){
const data = await requestApi()
// const data1 = await requestApiOther()
return data
}
request().then((val)=>{
console.log(val);
}).catch((e)=>{
console.log(e);
})
重点关注:
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world');
}
上面的这种情况,后面的await
不会执行,但是你就想让后面的也执行呢?
async function f() {
// 方式:1:
try {
await Promise.reject('出错了');
} catch (e) {
console.log(e)
}
// 方式2:
// await Promise.reject('出错了').catch(e => {
// console.log(e);
// })
return await Promise.resolve('hello world');
}
f().then((v) => {
console.log(v);
}).catch(e => {
console.log(e);
})
推荐使用:
1、将await
使用try...catch
包裹
async function f(){
try {
await someRequest()
} catch (error) {
console.log(error);
}
}
2、多个await
后面的异步操作如果不存在依赖关系,最好同时触发
async function f(){
try {
await Promise.all([getFoo(), getBar()]);
} catch (error) {
}
}
f().then(([foo,bar])=>{
// handle
}).catch(e=>{
console.log(e);
})
3、多个await
后面的异步操作如果存在依赖关系,请参照以下写法
function fetchData() {
return Promise.resolve('1')
}
function fetchMoreData(val) {
return new Promise((resolve, reject) => {
resolve(val * 2)
})
}
function fetchMoreData2(val) {
return new Promise((resolve, reject) => {
resolve(val * 3)
})
}
// good
function fetch() {
return fetchData()
.then(val1 => {
return fetchMoreData(val1)
})
.then(val2 => {
return fetchMoreData2(val2)
})
}
fetch().then(val => {
console.log(val) //6
})
// better
async function fetch() {
const value1 = await fetchData()
const value2 = await fetchMoreData(value1)
return fetchMoreData2(value2)
};
建议在项目中多使用ES7的async...await
写法,它简洁,优雅,可维护性高。
未完待续。。。
参考资料链接: