# ECMAScript 6 简介
ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
# let 和 const 命名
# let 基本用法 - 块级作用域
在 es6 中可以使用 let 声明变量,用法类似于 var
⚠️ let 声明的变量,只在
let
命令所在的代码块内有效
{ | |
let a = 10; | |
var b = 20; | |
} | |
console.log(a); //a is not defined | |
console.log(b); //20 |
# 不存在变量提升
var
命令会发生 变量提升
现象,即变量可以在声明之前使用,值为 undefined
。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。
为了纠正这种现象,let 命令改变了语法行为,它所声明的变量一定在声明后使用,否则报错
//var 的情况 | |
console.log(c);// 输出 undefined | |
var c = 30; | |
//let 的情况 | |
console.log(c);// 报错 ReferenceError | |
let c = 30; |
# 不允许重复声明
let
不允许在相同作用域内,重复声明同一个变量
let c = 10; | |
let c = 30; | |
console.log(c); // 报错 | |
function func(arg) { | |
let arg; // 报错 | |
} |
# 暂时性死区
了解的一个名词,说的就是 let
和 const
命令声明变量的特征。
在代码块内,使用 let
命令声明变量之前,该变量都是不可用的。这在语法上,称为 暂时性死区
(temporal dead zone,简称 TDZ)
# 为什么需要块级作用域?
# 原因一:内层变量可能会覆盖外层变量
function foo(a){ | |
console.log(a); | |
if(1===2){ | |
var a = 'hello 小马哥'; | |
} | |
} | |
var a = 10; | |
foo(a); |
# 原因二:用来计数的循环遍历泄露为全局变量
var arr = [] | |
for(var i = 0; i < 10; i++){ | |
arr[i] = function(){ | |
return i; | |
} | |
} | |
console.log(arr[5]()); |
变量 i
只用来控制循环,但是循环结束后,它并没有消失,用于变量提升,泄露成了全局变量。
解决循环计数问题
// 解决方式一:使用闭包 | |
var arr = [] | |
for(var i = 0; i < 10; i++){ | |
arr[i] = (function(n){ | |
return function(){ | |
return n; | |
} | |
})(i) | |
} | |
// 解决方式二:使用 let 声明 i | |
var arr = [] | |
for(let i = 0; i < 10; i++){ | |
arr[i] = function () { | |
return i; | |
} | |
} |
# const 基本用法 - 声明只读的常量
这意味着, const
一旦声明变量,就必须立即初始化,不能留到以后赋值。对于 const
来说,只声明不赋值,就会报错。
const a = 10; | |
a = 20;// 报错 | |
const b; // 报错 |
# 与 let
命令相同点
- 块级作用域
- 暂时性死区
- 不可重复声明
# let
和 const
使用建议
在默认情况下用 const, 而只有你在知道变量值需要被修改的情况下使用 let
# 模板字符串
传统的 JavaScript 语言,输出模板通常是这样写的
const oBox = document.querySelector('.box'); | |
// 模板字符串 | |
let id = 1,name = '小马哥'; | |
let htmlTel = "<ul><li><p>id:" + id + "</p><p>name:" + name + "</p></li></ul>"; | |
oBox.innerHTML = htmlTel; |
上面的这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题
let htmlTel = `<ul> | |
<li> | |
<p>id:${id}</p> | |
<p>name:${name}</p> | |
</li> | |
</ul>`; |
# 解构赋值
解构赋值是对赋值运算符的一种扩展。它通常针对数组和对象进行操作。
优点:代码书写简洁且易读性高
# 数组解构
在以前,为变量赋值,只能直接指定值
let a = 1; | |
let b = 2; | |
let c = 3; |
ES6 允许我们这样写:
let [a,b,c] = [1,2,3]; |
如果解构不成功,变量的值就等于
undefined
let [foo] = []; | |
let [bar, foo] = [1]; | |
foo`的值都会等于`undefined |
#
# 对象解构
解构可以用于对象
let node = { | |
type:'identifier', | |
name:'foo' | |
} | |
let {type,name} = node; | |
console.log(type,name)//identifier foo |
对象的解构赋值时,可以对属性忽略和使用剩余运算符
let obj = { | |
a:{ | |
name:'张三' | |
}, | |
b:[], | |
c:'hello world' | |
} | |
//可忽略 忽略b,c属性 | |
let {a} = obj; | |
//剩余运算符 使用此法将其它属性展开到一个对象中存储 | |
let {a,...res} = obj; | |
console.log(a,res); |
默认值
let {a,b = 10} = {a:20}; |
# 函数参数解构赋值
直接看例子
function add([x, y]){ | |
return x + y; | |
} | |
add([1, 2]); // 3 |
使用默认值
function addCart(n,num=0){ | |
return n+num; | |
} | |
addCart(10);//10 | |
addCart(10,20); //30 |
# 用途
交换变量的值
let x = 1;
let y = 2;
let [x,y] = [y,x];
上面代码交换变量
x
和y
的值,这样的写法不仅简洁,而且易读,语义非常清晰。从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
// 返回一个数组
function example() {
return [1, 2, 3];
}
let [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
提取 JSON 数据
解构赋值对提取 JSON 对象中的数据,尤其有用
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
// 对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者
console.log(id, status, number);
// 42, "OK", [867, 5309]
函数参数的默认值
输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
const {ajax} = require('xxx')
ajax()
# 函数的扩展
# 带参数默认值的函数
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法
function log(x,y){ | |
y = y || 'world'; | |
console.log(x,y); | |
} | |
log('hello');//hello world | |
log('hello','china') //hello china | |
log('hello','')//hello world |
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') { | |
console.log(x, y); | |
} | |
log('Hello') // Hello World | |
log('Hello', 'China') // Hello China | |
log('Hello', '') // Hello |
ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
默认的表达式可以是一个函数
function getVal(val) { | |
return val + 5; | |
} | |
function add2(a, b = getVal(5)) { | |
return a + b; | |
} | |
console.log(add2(10)); |
小练习
请问下面两种写法有什么区别?
// 写法一 | |
function m1({x = 0, y = 0} = {}) { | |
return [x, y]; | |
} | |
// 写法二 | |
function m2({x, y} = { x: 0, y: 0 }) { | |
return [x, y]; | |
} |
上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。
// 函数没有参数的情况 | |
m1() // [0, 0] | |
m2() // [0, 0] | |
//x 和 y 都有值的情况 | |
m1({x: 3, y: 8}) // [3, 8] | |
m2({x: 3, y: 8}) // [3, 8] | |
//x 有值,y 无值的情况 | |
m1({x: 3}) // [3, 0] | |
m2({x: 3}) // [3, undefined] | |
//x 和 y 都无值的情况 | |
m1({}) // [0, 0]; | |
m2({}) // [undefined, undefined] | |
m1({z: 3}) // [0, 0] | |
m2({z: 3}) // [undefined, undefined] |
# rest 参数
ES6 引入 rest 参数(形式为 ...变量名
),用于获取函数的多余参数,这样就不需要使用 arguments
对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) { | |
let sum = 0; | |
for (var val of values) { | |
sum += val; | |
} | |
return sum; | |
} | |
add(2, 5, 3) // 10 |
上面代码的 add
函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。
# 箭头函数 ***
ES6 允许使用箭头 =>
定义函数
let f = v=>v; | |
// 等同于 | |
let f = function(v){ | |
return v; | |
} | |
// 有一个参数 | |
let add = value => value; | |
// 有两个参数 | |
let add = (value,value2) => value + value2; | |
let add = (value1,value2)=>{ | |
return value1 + value2; | |
} | |
// 无参数 | |
let fn = () => "hello world"; | |
let doThing = () => { | |
} | |
// 如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。 | |
let getId = id => ({id: id,name: 'mjj'}) // 注意 | |
let obj = getId(1); |
# 箭头函数的作用
使表达更加简洁
const isEven = n => n % 2 == 0;
const square = n => n * n;
简化回调函数
// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
# 使用注意点
没有 this 绑定
let PageHandler = {
id:123,
init:function(){
document.addEventListener('click',function(event) {
this.doSomeThings(event.type);
},false);
},
doSomeThings:function(type){
console.log(`事件类型:${type},当前id:${this.id}`);
}
}
PageHandler.init();
// 解决 this 指向问题
let PageHandler = {
id: 123,
init: function () {
// 使用 bind 来改变内部函数 this 的指向
document.addEventListener('click', function (event) {
this.doSomeThings(event.type);
}.bind(this), false);
},
doSomeThings: function (type) {
console.log(`事件类型:${type},当前id:${this.id}`);
}
}
PageHandler.init();
let PageHandler = {
id: 123,
init: function () {
// 箭头函数没有 this 的指向,箭头函数内部的 this 值只能通过查找作用域链来确定
// 如果箭头函数被一个非箭头函数所包括,那么 this 的值与该函数的所属对象相等,否则 则是全局的 window 对象
document.addEventListener('click', (event) => {
console.log(this);
this.doSomeThings(event.type);
}, false);
},
doSomeThings: function (type) {
console.log(`事件类型:${type},当前id:${this.id}`);
}
}
PageHandler.init();
箭头函数中没有 arguments 对象
var getVal = (a,b) => {
console.log(arguments);
return a + b;
}
console.log(getVal(1,2)); //arguments is not defined
箭头函数不能使用 new 关键字来实例化对象
let Person = ()=>{}
let p1 = new Person();// Person is not a constructor
# 对象的扩展
# 属性的简洁表示法
const name = '张三'; | |
const age = 19; | |
const person = { | |
name, // 等同于 name:name | |
age, | |
// 方法也可以简写 | |
sayName() { | |
console.log(this.name); | |
} | |
} | |
person.sayName(); |
这种写法用于函数的返回值,将会非常方便。
function getPoint() { | |
const x = 1; | |
const y = 10; | |
return {x, y}; | |
} | |
getPoint() | |
// {x:1, y:10} |
# 对象扩展运算符
const [a, ...b] = [1, 2, 3]; | |
a // 1 | |
b // [2, 3] |
# 解构赋值
对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; | |
x // 1 | |
y // 2 | |
z // { a: 3, b: 4 } |
解构赋值必须是最后一个参数,否则会报错
let { ...x, y, z } = obj; // 句法错误 let { x, ...y, ...z } = obj; // 句法错误
# 扩展运算符
对象的扩展运算符( ...
)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
let z = { a: 3, b: 4 }; | |
let n = { ...z }; | |
n // { a: 3, b: 4 } |
扩展运算符可以用于合并两个对象。
let ab = { ...a, ...b }; | |
// 等同于 | |
let ab = Object.assign({}, a, b); |
# Promise 对象
异步编程模块在前端开发中,显得越来越重要。从最开始的 XHR 到封装后的 Ajax 都在试图解决异步编程过程中的问题。随着 ES6 新标准的到来,处理异步数据流又有了新的解决方案。在传统的 ajax 请求中,当异步请求之间的数据存在依赖关系的时候,就可能产生不优雅的多层回调,俗称” 回调地域 “(callback hell),这却让人望而生畏,Promise 的出现让我们告别回调地域,写出更优雅的异步代码。
回调地狱带来的负面作用有以下几点:
- 代码臃肿。
- 可读性差。
- 耦合度过高,可维护性差。
- 代码复用性差。
- 容易滋生 bug。
- 只能在回调里处理异常。
在实践过程中,却发现 Promise 并不完美,Async/Await 是近年来 JavaScript 添加的最革命性的的特性之一,Async/Await 提供了一种使得异步代码看起来像同步代码的替代方法。接下来我们介绍这两种处理异步编程的方案。
# 什么是 Promise
Promise 是异步编程的一种解决方案:
从语法上讲,Promise 是一个对象,通过它可以获取异步操作的消息;
从本意上讲,它是承诺,承诺它过一段时间会给你一个结果。
promise 有三种状态:pending (等待态),fulfilled (成功态),rejected (失败态);
状态一旦改变,就不会再变。
创造 promise 实例后,它会立即执行。
看段习以为常的代码:
// Promise 是一个构造函数,自己身上有 all,reject,resolve,race 方法,原型上有 then、catch 等方法 | |
let p = new Promise((resolve,reject)=>{ | |
// 做一些异步操作 | |
setTimeout(()=>{ | |
/* let res = { | |
ok:1, | |
data:{ | |
name:"张三" | |
} | |
} */ | |
let res = { | |
ok:0, | |
error:new Error('有错') | |
} | |
if(res.ok === 1){ | |
resolve(res.data); | |
}else{ | |
reject(res.error.message) | |
} | |
}, 1000) | |
}) |
# Promise 的状态和值
Promise
对象存在以下三种状态
- Pending (进行中)
- Fulfilled (已成功)
- Rejected (已失败)
状态只能由
Pending
变为Fulfilled
或由Pending
变为Rejected
,且状态改变之后不会在发生变化,会一直保持这个状态。
Promise
的值是指状态改变时传递给回调函数的值
上面例子中的参数为 resolve 和 reject,他们都是函数,用他们可以改变 Promise 的状态和传入的 Promise 的值
resolve` 和 `reject
resolve
: 将 Promise 对象的状态从Pending(进行中)
变为Fulfilled(已成功)
reject
: 将 Promise 对象的状态从Pending(进行中)
变为Rejected(已失败)
resolve
和reject
都可以传入任意类型的值作为实参,表示Promise
对象成功(Fulfilled)
和失败(Rejected)
的值
# then 方法
p.then((data)=>{ | |
console.log(data); | |
return data; | |
},(error)=>{ | |
console.log(error) | |
}).then(data=>{ | |
console.log(data); | |
}) |
promise 的 then 方法返回一个 promise 对象,所以可以继续链式调用
上述代码我们可以继续改造,因为上述代码不能传参
function timeout(ms) { | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
resolve('hello world') | |
}, ms); | |
}) | |
} | |
timeout(1000).then((value) => { | |
console.log(value); | |
}) |
# then 方法的规则
then
方法下一次的输入需要上一次的输出- 如果一个 promise 执行完后 返回的还是一个 promise,会把这个 promise 的执行结果,传递给下一次
then
中 - 如果
then
中返回的不是 Promise 对象而是一个普通值,则会将这个结果作为下次 then 的成功的结果 - 如果当前
then
中失败了 会走下一个then
的失败 - 如果返回的是 undefined 不管当前是成功还是失败 都会走下一次的成功
- catch 是错误没有处理的情况下才会走
then
中不写方法则值会穿透,传入下一个then
中
# Promise 封装 XHR 对象
const getJSON = function (url) { | |
return new Promise((resolve, reject) => { | |
const xhr = new XMLHttpRequest(); | |
xhr.open('GET', url); | |
xhr.onreadystatechange = handler; | |
xhr.responseType = 'json'; | |
xhr.setRequestHeader('Accept', 'application/json'); | |
xhr.send(); | |
function handler() { | |
console.log(this.readyState); | |
if (this.readyState !== 4) { | |
return; | |
} | |
if (this.status === 200) { | |
resolve(this.response); | |
} else { | |
reject(new Error(this.statusText)); | |
} | |
} | |
}) | |
} | |
getJSON('https://free-api.heweather.net/s6/weather/now?location=beijing&key=4693ff5ea653469f8bb0c29638035976') | |
.then((res) => { | |
console.log(res); | |
}, function (error) { | |
console.error(error); | |
}) | |
//then 方法的链式调用 | |
getJSON('https://free-api.heweather.net/s6/weather/now?location=beijing&key=4693ff5ea653469f8bb0c29638035976') | |
.then((res)=>{ | |
return res.HeWeather6; | |
}).then((HeWeather6)=>{ | |
console.log(HeWeather6); | |
}) |
# catch 方法
then 里面第二个参数捕捉错误只能捕捉上级的 不能捕捉同级的第一个参数里的错误 所以要用 catch
catch(err=>{})`方法等价于`then(null,err=>{})
getJSON('https://free-api.heweather.net/s6/weather/now?location=beijing&key=4693ff5ea653469f8bb0c29638035976')
.then((json) => {
console.log(json);
}).then(null,err=>{
console.log(err);
})
//等价于
getJSON('https://free-api.heweather.net/s6/weather/now?location=beijing&key=4693ff5ea653469f8bb0c29638035976')
.then((json) => {
console.log(json);
}).catch(err=>{
console.log(err);
})
# resolve()
resolve()
方法将现有对象转换成 Promise 对象,该实例的状态为 fulfilled
let p = Promise.resolve('foo'); | |
// 等价于 new Promise (resolve=>resolve ('foo')); | |
p.then((val)=>{ | |
console.log(val); | |
}) |
# reject()
reject()
方法返回一个新的 Promise 实例,该实例的状态为 rejected
let p2 = Promise.reject(new Error('出错了')); | |
// 等价于 let p2 = new Promise ((resolve,reject)=>reject (new Error (' 出错了))); | |
p2.catch(err => { | |
console.log(err); | |
}) |
# all () 方法
all () 方法提供了并行执行异步操作的能力,并且再所有异步操作执行完后才执行回调
试想一个页面聊天系统,我们需要从两个不同的 URL 分别获得用户的的个人信息和好友列表,这两个任务是可以并行执行的,用 Promise.all 实现如下
let meInfoPro = new Promise( (resolve, reject)=> { | |
setTimeout(resolve, 500, 'P1'); | |
}); | |
let youInfoPro = new Promise( (resolve, reject)=> { | |
setTimeout(resolve, 600, 'P2'); | |
}); | |
// 同时执行 p1 和 p2,并在它们都完成后执行 then: | |
Promise.all([meInfoPro, youInfoPro]).then( (results)=> { | |
console.log(results); // 获得一个 Array: ['P1', 'P2'] | |
}); |
# race () 方法
有些时候,多个异步任务是为了容错。比如,同时向两个 URL 读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用 Promise.race () 实现:
let meInfoPro1 = new Promise( (resolve, reject)=> { | |
setTimeout(resolve, 500, 'P1'); | |
}); | |
let meInfoPro2 = new Promise( (resolve, reject)=> { | |
setTimeout(resolve, 600, 'P2'); | |
}); | |
Promise.race([meInfoPro1, meInfoPro2]).then((result)=> { | |
console.log(result); // P1 | |
}); |
Promise.all 接受一个 promise 对象的数组,待全部完成之后,统一执行 success;
Promise.race 接受一个包含多个 promise 对象的数组,只要有一个完成,就执行 success
举个更具体的例子,加深对 race () 方法的理解
当我们请求某个图片资源,会导致时间过长,给用户反馈
用 race 给某个异步请求设置超时时间,并且在超时后执行相应的操作
function requestImg(imgSrc) { | |
return new Promise((resolve, reject) => { | |
var img = new Image(); | |
img.onload = function () { | |
resolve(img); | |
} | |
img.src = imgSrc; | |
}); | |
} | |
// 延时函数,用于给请求计时 | |
function timeout() { | |
return new Promise((resolve, reject) => { | |
setTimeout(() => { | |
reject('图片请求超时'); | |
}, 5000); | |
}); | |
} | |
Promise.race([requestImg('images/2.png'), timeout()]).then((data) => { | |
console.log(data); | |
}).catch((err) => { | |
console.log(err); | |
}); |
# async 函数
异步操作是 JavaScript 编程的麻烦事,很多人认为 async 函数是异步编程的解决方案
<span style="color:red">async 函数执行返回的结果一定是一个 promise</span>
# Async/await 介绍
- async/await 是写异步代码的新方式,优于回调函数和 Promise。
- async/await 是基于 Promise 实现的,它不能用于普通的回调函数。
- async/await 与 Promise 一样,是非阻塞的。
- async/await 使得异步代码看起来像同步代码,再也没有回调函数。但是改变不了 JS 单线程、异步的本质。(异步代码同步化)
# Async/await 的使用规则
凡是在前面添加了 async 的函数在执行后都会自动返回一个 Promise 对象
async function test() {
}
let result = test()
console.log(result) // 即便代码里 test 函数什么都没返回,我们依然打出了 Promise 对象
await 必须在 async 函数里使用,不能单独使用
async test() {
let result = await Promise.resolve('success')
console.log(result)
}
test()
await 后面需要跟 Promise 对象,不然就没有意义,而且 await 后面的 Promise 对象不必写 then,因为 await 的作用之一就是获取后面 Promise 对象成功状态传递出来的参数。
function fn() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
})
})
}
async test() {
let result = await fn() // 因为 fn 会返回一个 Promise 对象
console.log(result) // 这里会打出 Promise 成功后传递过来的'success'
}
test()
# Async/Await 的用法
- 使用 await,函数必须用 async 标识
- await 后面跟的是一个 Promise 实例,最后返回的是 Promise.resolved 后的值,不是 Promise 的话会被封装为一个 Promise
function loadImg(src) { | |
const promise = new Promise(function (resolve, reject) { | |
const img = document.createElement('img') | |
img.onload = function () { | |
resolve(img) | |
} | |
img.onerror = function () { | |
reject('图片加载失败') | |
} | |
img.src = src | |
}) | |
return promise | |
} | |
const src1 = 'https://hcdn1.luffycity.com/static/frontend/index/banner@2x_1574647618.8112254.png' | |
const src2 = 'https://hcdn2.luffycity.com/media/frontend/index/%E7%94%BB%E6%9D%BF.png' | |
const load = async function () { | |
const result1 = await loadImg(src1) | |
console.log(result1) | |
const result2 = await loadImg(src2) | |
console.log(result2) | |
} | |
load() |
当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。
# async/await 的错误处理
关于错误处理,如规则三所说,await 可以直接获取到后面 Promise 成功状态传递的参数,但是却捕捉不到失败状态。在这里,我们通过给包裹 await 的 async 函数添加 then/catch 方法来解决,因为根据规则一,async 函数本身就会返回一个 Promise 对象。
const load = async function () { | |
try{ | |
const result1 = await loadImg(src1) | |
console.log(result1) | |
const result2 = await loadImg(src2) | |
console.log(result2) | |
}catch(err){ | |
console.log(err); | |
} | |
} | |
load() |
# 为什么 Async/Await 更好?
Async/Await 较 Promise 有诸多好处,以下介绍其中三种优势:
简洁
使用 Async/Await 明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理 Promise 的 resolve 值,也不需要定义多余的 data 变量,还避免了嵌套代码。
中间值
在前端编程中,我们偶尔会遇到这样一个场景:我们需要发送多个请求,而后面请求的发送总是需要依赖上一个请求返回的数据。对于这个问题,我们既可以用的 Promise 的链式调用来解决,也可以用 async/await 来解决,然而后者会更简洁些
const makeRequest = () => { | |
return promise1() | |
.then(value1 => { | |
return promise2(value1) | |
.then(value2 => { | |
return promise3(value1, value2) | |
}) | |
}) | |
} |
使用 async/await 的话,代码会变得异常简单和直观
const makeRequest = async () => { | |
const value1 = await promise1() | |
const value2 = await promise2(value1) | |
return promise3(value1, value2) | |
} |
- 提高可读性
下面示例中,需要获取数据,然后根据返回数据决定是直接返回,还是继续获取更多的数据。
const makeRequest = () => { | |
return getJSON() | |
.then(data => { | |
if (data.needsAnotherRequest) { | |
return makeAnotherRequest(data) | |
.then(moreData => { | |
console.log(moreData) | |
return moreData | |
}) | |
} else { | |
console.log(data) | |
return data | |
} | |
}) | |
} |
代码嵌套(6 层)可读性较差,它们传达的意思只是需要将最终结果传递到最外层的 Promise。使用 async/await 编写可以大大地提高可读性:
const makeRequest = async () => { | |
const data = await getJSON() | |
if (data.needsAnotherRequest) { | |
const moreData = await makeAnotherRequest(data); | |
console.log(moreData) | |
return moreData | |
} else { | |
console.log(data) | |
return data | |
} | |
} |
# Class 的基本用法
# 简介
JavaScript 语言中,生成实例对象的传统方法是通过构造函数
function Person(name,age) { | |
this.name = name; | |
this.age = age; | |
} | |
Person.prototype.sayName = function() { | |
return this.sayName; | |
} | |
let p = new Person('小马哥',18); | |
console.log(p); |
上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过 class
关键字,可以定义类。
基本上,ES6 的 class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的 class
改写,就是下面这样
# new 关键字补充
当使用 new 关键字调用一个构造函数或者类时,会创建一个虚拟上下文,包含 new.target 指向这个构造函数或者类,当构造完毕后返回构造的实例,并且销毁这个虚拟上下文(不管内部是否引用 new.target)。
# async 补充
async 关键字修饰的函数相当于一个语法糖,会将后面跟随的函数进行封装,变为一个返回 Promise 的函数,也就是说:async 修饰的函数的返回值一定是 Promise 对象。
# await 补充
await 关键字修饰的一定是一个 Promise 对象,如果不是 JS 内部会对该值进行封装,封装为一个 Promise 对象,Promise 内部 resolve 的返回值,就是 await 修饰后的 Promise 的返回值,如果 Promise 内部 reject 了,就会抛出异常,这时就可以使用 try catch 进行捕获。
class Person { | |
//constructor 方法 是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法,一个类必须有 constructor 方法,如果没有定义,会被默认添加 | |
constructor(name, age) { | |
this.name = name; | |
this.age = age; | |
} | |
// 等同于 Person.prototype = function sayName (){} | |
sayName(){ | |
return this.name; | |
} | |
} | |
console.log(Person===Person.prototype.constructor) |
类的方法内部如果含有
this
,它默认指向类的实例
# 继承
class Animal { | |
constructor(name, age) { | |
this.name = name; | |
this.age = age; | |
} | |
sayName(){ | |
return this.name; | |
} | |
sayAge(){ | |
return this.age; | |
} | |
} | |
class Dog extends Animal{ | |
constructor(name, age,color) { | |
super(name,age); | |
this.color = color; | |
} | |
// 子类自己的方法 | |
sayColor(){ | |
return `${this.name}是${this.age}岁了,它的颜色是${this.color}`; | |
} | |
// 重写父类的方法 | |
sayName(){ | |
return super.sayName() + this.color; | |
} | |
} | |
let d1 = new Dog('小红',8,'red'); | |
console.log(d1); //=>Dog {name: ' 小红 ', age: 8, color: 'red'} | |
console.log(d1.sayName()); //=>' 小红' | |
console.log(d1.sayColor());//=>' 小红是 8 岁了,它的颜色是 red' | |
console.log(d1.sayName()); //=>' 小红 red' |
# 混入
# Module 模块化
# 概述
历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的 require
、Python 的 import
,甚至就连 CSS 都有 @import
,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
# export 命令
模块功能主要由两个命令构成: export
和 import
。 export
命令用于规定模块的对外接口, import
命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 export
关键字输出该变量
//module/index.js | |
export const name = 'zhangsan '; | |
export const age = 18; | |
export const color = 'red '; | |
export const sayName = function() { | |
console.log(fristName); | |
} | |
// 也可以这样 | |
const name = 'zhangsan '; | |
const age = 18; | |
const color = 'red '; | |
const sayName = function() { | |
console.log(fristName); | |
} | |
export {name,age,color,sayName} |
# import 命令
<span style="color:red"> 在一个文件或模块中,export 可以导出多个,对应的 import 导入加 { }</span>
使用 export
命令定义了模块的对外接口以后,其他 JS 文件就可以通过 import
命令加载这个模块。
//main.js | |
import {name,age,color,sayName,fn} from './modules/index.js'; |
如果想为输入的变量重新取一个名字, import
命令要使用 as
关键字,将输入的变量重命名
import * as obj from './modules/index.js'; | |
console.log(obj); |
# export default 命令
<span style="color:red">export default 仅可以导出一个,对应的 import 导入时候不用加花括号 </span>
使用 export default
命令为模块指定默认输出
//export-default.js | |
export default function(){ | |
console.log('foo'); | |
} | |
// 或者写成 | |
function foo() { | |
console.log('foo'); | |
} | |
export default foo; |
在其它模块加载该模块时, import
命令可以为该匿名函数指定任意名字
//import-default.js | |
import customName from './export-default.js' | |
customNmae();//foo |
如果想在一条 import 语句中,同事输入默认方法和其他接口,可以写成下面这样
import customName,{add} from 'export-default.js' |
对应上面 export
语句如下
//export-default.js | |
export default function(){ | |
console.log('foo'); | |
} | |
export function add(){ | |
console.log('add') | |
} |
export default
也可以用来输出类。
// MyClass.js | |
export default class Person{ ... } | |
// main.js | |
import Person from 'MyClass'; | |
let o = new Person(); |
<span style="color:red">export 与 export default 均可用于导出常量、函数、文件、模块等 </span>