ES6快速指南
let/const
-
let 变量声明标识,与var类似,但是***let***声明的变量只在所在的代码块中有效,不存在作用域提升
{ var a = 20; let b = 10; } console.log(a); // 20; console.log(b); // Uncaught ReferenceError: b is not defined
-
同一个名称的变量再同一个作用域内只能用let申明一次, 被var 声明过的变量可以被let二次声明,但是被let声明过的变量不允许再被var 二次申明
var b = 10; var b = 20; let a = 1; let a =2; // Identifier 'a' has already been declared var b = 30; // Identifier 'a' has already been declared
箭头函数
es6 允许 使用 =>
定义函数,他与function声明的函数功能无异,但是他具有一下特点
-
箭头函数内部的this只与函数声明位置的上下文有关,不会不会随着调用者改变
function hello() { setTimeout(() => { console.log("arrow::,this.a); },1000) setTimeout(function (){ console.log("function:",this.a); },1000) } let obj = { a:1,hello }; obj.hello(); // arrow: 1 // function: undefined // 由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向 hello.call({a:100},hello); // arrow: 100 // function: undefined
-
箭头函数的函数体如果只有一行,可以省略{},这一行的执行结果将作为函数的返回值 (与C#中的lamda表达式类似)
let foo = () => 'bar'; foo(); // bar;
-
箭头的函数的参数列表如果只有一个参数可以省略(),如果没有参数就必须带()
let hello = name => console.log("hello,",name,"!"); hello("world"); // hello, world !
-
箭头函数不再有 arguments, super, new.target,caller,callee 隐式参数。
(function() { console.log(...arguments); })("a","b"); // a b (() => { console.log(...arguments); })("a","b"); // Uncaught ReferenceError: arguments is not defined let hello = () => { console.log(hello.caller) }; hello(); // 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
解构语法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构
```javascript
// 数组解构
let [a,b,c] = [1,2,3,4];
console.log(a,b,c); // 1 2 3
// 对象解构
let { foo,name } = {foo:"bar", name:"bug4j"};
console.log(foo, name) // bar bug4j
```
解构语法实际是上可以堪称上是一种模式匹配复制,只要 = 左右的模式解构匹配上了,就可以正确的给变量复制
let [a,[[b]],[c]] = [1,[[2,3,4]],[5],6];
console.log(a,b,c); // 1 2 5
= 左右的模式不要求完全匹配,如果模式没有匹配上,则变量的值为undefined,
let [a, b, c ] = [1,2];
console.log(a,b,c); // 1 2 undefined
对象解构只对属性名称和嵌套关系进行匹配,与顺序无关
… (spread) 运算符
-
对于数组(set),spread 运算符用来将一个数组转为用逗号分隔的参数序列
let arr = [1,2,3,4,5]; let set = new Set(["a","b"]); console.log([...arr,...set]); // 1 2 3 4 5 a b
-
对于key-value对象,可以将目标对象自身的所有可遍历的(enumerable)、分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面
let obj = {foo:"bar"}; let obj1 = {...obj,"hello":"world"}; console.log(obj1); // {foo: "bar", hello: "world"}
-
配合解构语法使用,可以将目标对象在当前解构表达式中没有被读取的可枚举属性拷贝到指定的变量中
let {a,b,...c} = { a:1,b:2,x:3,y:4 }; console.log(c);// {x: 3, y: 4}
-
spread和解构语法一样,只是对目标对象属性进行浅拷贝
let obj = { a:1,b:{c:2,d:3} }; let { b } = obj; let obj1 = { ...obj,d:100}; console.log(obj.b === b); // true console.log(obj.b === obj1.b); // true
-
key-value对象中使用spread运算符时, 后面(右边或者下边)的表达式中出现的属性会覆盖 前面(左边或者上边)表达式中的属性值
let a = {a:1,b:2}; let b = {a:3,c:4}; let obj = { ...a,...b,c:"c" }; console.log(obj); // {a: 3, b: 2, c: "c"}
-
spread 运算符只能处理对象中可枚举的属性
let obj = {a:1,b:2}; Object.defineProperty(obj,"c",{value:"c",enumerable:false}); console.log(obj); // {a: 1, b: 2, c: "c"} console.log({ ...obj }); // {a: 1, b: 2}
对象属性定义简化操作
在定义对象时,如果对象属性名称与赋值变量名称一致时,允许简化:
let App = defineAsyncComponent(() => import App from '@/App.js');
let components = { App };
new Vue({ components }).mount("#app");
// 相当于
let components = { App:App };
new Vue({ components:components }).mount("#app");
可选运算符与可选运算符链和空值运算符
通常情况下,一个对象如果有多层属性嵌套,在读取这个对象内部的一个属性值时,我们都需要判断这个属性的上一层属性是否为null从而避免空指针错误:
let obj = { a:{ b:{ c:1 } } };
let val = obj.a.b.c; // 可能会报错
let val1 = ((obj.a || {}).b || {}).c; // 不会报错
// 或者
val1 = (obj.a && obj.a.b && obj.a.b.c) ? obj.a.b.c : undefined;
// 或者 使用三元运算符 ? :
一旦对象属性层级较深,这种判断就很乱容易出错,es6 提供一个 可选运算符(?.
)来解决这种问题:
let obj = { a:{ b:{ c:1 } } };
let val = obj.a?.b?.c;
// 或者
val = obj.a?.["b"]?.["c"];
可选运算符也可以用于函数的判断调用:
let obj = {};
obj.hello?.(); // undefined
// 如果obj中存在hello,单数hello并不是一个函数,任然会报错
函数的可选参数(参数默认值)和可变参数
-
参数默认值
es6之前如果调用函数时,某个参数没有传值,会给这个参数分配undefined, 想要有默认值只能采用一下方法:
function hello(x,y) { x = x || 1; y = y || 2; }
这种方法有个缺点,如果 x 的值 为 false, 0 或者 “”, x 也会被赋予默认值0
function hello(x) { x = x || 1; console.log(x); } hello(false); // 1 hello(0); // 1 hello(""); // 1
要避免这种问题,实际上只能先判断是否为这些产生干扰的值,
es6 允许为函数参数设置默认值来解决这个问题:
function hello(x,y = 0) { console.log(x + y); } hello(2); // 2
可选参数的位置虽然没有语法限制,但是一般在形参列表的末尾,如果不在末尾,实际上参数是不可以省略的,省略会导致可选参数后面的参数取值不符合预期
-
可变参数
在java中,允许方法将不确定个数的多个同类型参数归并成一个指定类型的数组参数,es6中也允许类似的操作,不同的是,由于javascript 是一种弱类型的语言,可变参数的类型并没有特别规定:
(function(x,y,...rest) { console.log(rest); })(1,2,3,"a",{foo:"bar"},false); // [3, "a", { foo:"bar" }, false]
可选参数后面不能在声明其他参数,否则会报错
字符串新增方法
- includes(), startsWith(), endsWith()
- repeat()
- padStart(),padEnd()
- trimStart(),trimEnd()
- matchAll()
- replaceAll()
字符串模板
字符串模板允许直接申明一个多行字符串,并且可以通过${}
在字符串中插入变量
es6 之前,在js中的写法:
var str = "<div>" +
"<div class='label'>姓名:</div><div class='value'>"+ name +"</div>" +
"<div class='label'>年龄:</div><div class='value'>"+ age +"</div>" +
"</div>"
使用es6模板字符串:
let str = `<div>
<div class='label'>姓名:</div><div class='value'>${ name }</div>
<div class='label'>年龄:</div><div class='value'>${ age + 1 }</div>
</div>`;
模板字符串可以嵌套,${}
中可以书写任何有效的es6表达式:
let adultActivities = ["抽烟","喝酒","烫头"];
let childrenActivities = ["好好学习"]
let str = `你的年龄为:${ age },你是个${age >= 18 ? `成年人, 你可以${adultActivities.join(",")}啦` : `未成年,你可以${childrenActivities.join(",")}`}!`;
数组新增方法
Array.from(): 将类数组解构转换为数组
Array.of(): 将一系列值包装到一个数组中
find() 和 findIndex() : 元素搜素
includes(): 判断数组是否包含元素
flat(),flatMap(): 数组降维打击
旧版本的方法 : some(), every(), filter(), reduce(),shfit(),splice(),pop():
let arr1 = Array.from($("div"));
let arr2 = Array.of(1,"a",2);
let arr3 = [1,2,[3,[4,5,[6,7]],8]].flat(3); // [1, 2, 3, 4, 5, 6, 7, 8]
let n = [1,2,3,4].find(n => n>=3 ) // 3
let index = [0,1,2,3,4].find(n => n>=3 ) // 4
let incls = [0,1,2,3,4].includes(6) // false;
let someRes = [0,1,2,4].some(n => (n % 3 == 0)) // false
let everyRes = [0,1,2,4].some(n => n <= 3) // false
let sum = [1,2,3,4,5].reduce((lastRes,n) => lastRes + n ) // 15
set 与 map
Set
set: 可以看做是自动去重版的数组,set 和数组有很多同名的方法如:some,every,map,forEach,filter,find,reduce,他也有一些独有的方法:
let s = new Set([1,2,3,4]);
s.add(1);
s.addAll([3,4,5]);
console.log(s.size, s); // 5 [ 1,2,3,4,5 ]
let s1 = new Set([3,5,6]);
let interSet = s.intersection(s); // Set: [3]
let diffSet = s1.difference([6,7,8]) // Set: [3,5]
let unionSet = s1.union(s) // Set: [1,2,3,4,5,6]
let d = s.delete(4);
console.log(d,s) // true Set: [2,3,4]
Map
map可以看作是增强版的 object, 普通的object 的 key 只能是string, map 的key 可以是任何js对象,并且,map能保存元素插入的顺序
var m = new Map();
m.set(undefined,"111");
m.set(null,"222");
m.set(NaN,"333");
console.log(m) // Map(3) {undefined => "111", null => "222", NaN => "333"}
map遍历方式有和多种例如 m.keys(), m.entries, m.values, m.forEach, for … of循环等等。
Promise 与 jquery.Deferred
传统的js异步编程过程中,如果要在异步操作执行完后再执行某些操作,必须将这些操作放到回调函数中,在异步操作结束后在调用回调函数,这种方式在异步操作很多的时候,会大大降低代码的可阅读性,可维护性,Promise 与 jquery.Deferred 的目的都是为了解决js中异步回调地狱,让异步链式调用逻辑变得更清晰。
promise最早是由社区提出的,后来才被es委员会纳入es标准,因此,早期有很多实现方案, jquery.Deferred就是其中一种。
Promise
Promise 的特点
promise对象有一下几个特点:
- 对象的状态不受外界影响。promise对象有三种状态,只有异步操作的执行结果能触发这两种转换。
- 一旦状态改变了,就不会再变,任何时候都可以得到这个结果。pending, fullfilled, rejected,三种状态之间要么从pending转换为fullfilled,要么从pending 转换为 rejected, 不存在其他的状态转换。如果改变已经发生了,你再对
Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。 - Promise 一旦创建就会立即执行,没有办法取消,
- 如果不设置错误的回调,Promise内部的错误无法体现给调用者。
- 当Promise处于pending状态时,无法获取到内部更多的执行进度等细节。
用法:
基础使用
Promise 构造函数接收一个函数作为异步操作的执行者,这个函数有两个形参 resolve函数,reject 函数,异步操作内部可以在合适的时机调用resolve将promise的状态转换为fullfilled状态,或者调用reject函数将promise的状态转换为 rejected。 调用resolve或者reject函数时传递的参数将作为promise成功或失败的回调方法的参数传递给开发者。开发者可以通过.then方法指定promise 执行成功(resolved)和失败(rejected)的回调
function proTest() {
let p = new Promise(function(resolve,reject) {
setTimeout(() => {
resolve("resolved");
},1000)
});
console.log("created");
return p;
}
let p = proTest();
p.then(res => {
console.log(res);
})
// created
// resolved
setTimeout(() => {
p.then(res => {
console.log(`still get the correct result: ${ res }`);
})
},2000)
// still get the correct result: resolved
单个值传递
如果resolved回调中没有返回任何值,then方法调用后会自动创建一个Promise并返回,因此可以像下面那样进行链式调用, 但是后面的then中并拿不到前面执行的结果
proTest().then(res => {
console.log(res); // undefined
})
多个值传递
promise.then 链式调用过程中,then回调函数(resolve,reject)中返回的任何值都会被封装成一个promise对象传递给链式调用的下一个then方法指定的回调中
proTest().then(res => {
console.log(res);
return "after resolved"
}).then(res1 => {
console.log(res1);
})
// resolved
// after resolved
如果要给下一个then传递多个值,可以将结果封装成一个对象或者数组再返回:
proTest().then(res => {
console.log(res);
return [res,"after resolved"];
}).then(([res,res1]) => {
console.log(`second then resolved: ${res}`);
console.log(res1);
})
// created
// resolved
// second then resolved: resolved
// after resolved
控制Promise状态
这样直接返回值的方式,得到的Promise会立即变为resolved状态,如需要手动改变下一promise的状态,可以在resolved中直接抛出异常,或者手动构造一个Promise,将这个promise的状态变为rejected再返回, 然后在下一个promise的reject回调中就可以捕获到错误信息
function getUserByUserName(username) {
return new Promise((resolve,reject) => {
let users = [{
"username":"John",
"password":"222"
},{
"username":"Marry",
"password":"111"
}];
let user = users.find(user => user.username === username);
resolve(user);
});
}
function login(username,password) {
return getUserByUserName(username).then(user => {
if(!!!user) throw new Error(`user: ${username} not found !`);
if(user.password === password) {
return `user: ${username} login success`;
} else {
return new Promise((resolve,reject) => {
reject(`user ${username} wrong password !`);
})
}
})
}
login("Kate","333").then(res => { console.log(res); },error => { console.log(error)});
// Error: user: Kate not found !
login("John","111").then(res => { console.log(res); },error => { console.log(error)});
// user:John wrong password !
login("Marry","111").then(res => { console.log(res); },error => { console.log(error)});
// user:Marry login success
Promise.resolve() 与 Promise.reject()
有时候我们只是需要返回一个resolved状态的promise或者一个rejected状态的promise以及一些确定的信息,而并不需要载做额外的异步操作,这种通过new Promise(…)的方式就显得有点啰嗦了,Promise对象为我们提供了两个静态方法 Promise.resolve 和 Promise.reject为我们解决这种问题。
Promise.resolve(args) 的参数可以是任意值,
- 如果参数是一个Promise,Promise.resolve 会立即返回这个Promise
- 如果参数是一个类Promise(thenable对象),Promise.resolve会把这个对象转换成一个Promise并返回,并将对象的then方法中传递的值作为这个转换后的Promise的resolve回调的参数。
- 如果参数是一个普通对象,Promise.resolve 会将对象封装成一个fullfilled状态的Promise,被传入的对象就是这个Promise的resolve回调的参数。
- 如果是一个其他对象,Promise.resolve返回一个fullfilled状态的Promise, 并将传入的对象作为这个Promise的resolve回调的参数。
Promise.reject(args) 的参数同样可以是任意值,相比Promise.resolve来说,Promise.reject就单纯很多了。Promise.reject方法调用后会立即返回一个rejected状态的Promise, 传递的参数会直接作为这个promise的reject回调的参数。
function login(username,password) {
return getUserByUserName(username).then(user => {
if(!!!user) throw new Error(`user: ${username} not found !`);
if(user.password === password) {
return Promise.resolve(`user: ${username} login success`);
} else {
return Promise.reject(`user: ${username} wrong password !`);
}
})
}
login("Kate","333").then(res => { console.log(res); },error => { console.error(error)});
// Error: user: Kate not found !
login("John","111").then(res => { console.log(res); },error => { console.error(error)});
// user: John wrong password !
login("Marry","111").then(res => { console.log(res); },error => { console.error(error)});
// user: Marry login success
Promise.catch() 与 Promise.finally()
上面这种then方法中同时指定resolved和rejected回调多少显得有点不直观,不符合promise链式调用的风格,promise还提供一个catch方法用来指定rejected的回调:
login("Kate","333").then(res => { console.log(res); })
.catch(error => { console.error(error) });
有时候无论异步操作成功与否我们都需要做一些事情,比如释放数据库连接,记录页面操作等,这种和promise的状态无关的操作可以放在.filnally回调中执行:
login("Kate","333").then(res => {
console.log(res);
}).catch(error => {
console.error(error);
}).finally(() => {
console.log("disconnect from server ...");
})
// Error: user: Kate not found !
// disconnect from server ...
// finally 总会在 resolved回调和rejected回调后面执行
多个Promise的状态监测
Promise.all(), Promise.race(),Promise.allSettled(),Promise.any(), 这四个方法都是对多个promise的之心结果做不同的操作
Promise.all(iterable<Promise>)
: 当所有promise的状态变为fullfilled状态时,方法返回的promise才会变为fullfilled状态 ,并且传入的Promise的resolve的结果会封装成一个数据传递下去,只要有一个promise状态变为rejected, 方法返回的Promise的状态就会变为rejectedPromise.allSettled(iterable<Promise>) :
Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled
或rejected
后的promise,并带有一个对象数组,每个对象表示对应的promise结果Promise.race(iterable<Promise>)
:Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise拒绝,返回的 promise就会拒绝。Promise.any(iterable<Promise>)
: 只要传入的promise中有一个状态变为fullfilled, 返回的Promise的状态就会变为fullfilled。 只有所有传入的Promise的状态都是rejected时,返回的Promise的状态才会变为rejected.
注意: Promise.allSettled和 Promise.any 目前还处于es2020提案的stage-4阶段,这意味着他们的语法可能会变动,并且部分浏览器任然不支持, 开发中还是要尽量避免使用。
Promise.try
参考链接:https://github.com/tc39/proposal-promise-try
Promise.try提案 目前任然处于stage-1 状态,不建议使用。
Fetch Api: Promise的应用
fetch 是 es6 提供的一个异步网络请求接口;
es6 之前,要发起异步请求最常用的方法是使用XmlHttpRequest对象,fetch 相对于 XHR来说,支持诸如 cookie, 跨域请求等等很多新特性, 并且fetch 可以支持Promise 链式编程,避免了xhr中的回调地狱问题。
fetch 的用法
基础用法:
// 发起一个get请求获取 /users.json
fetch("/users.json").then(resp => resp.json()).then(users => {
console.log(users);
})
复杂的请求:
fetch 还有一个连个参数的重载,第二个参数可以是一个 Request对象,或者是一个Request配置对象。Request详参考https://developer.mozilla.org/zh-CN/docs/Web/API/Request
fetch("/api/user/1",{ method:"DELETE",headers:{},body:{} }).then(resp => resp.json()).then(res => {
console.log(res);
})
fetch api 有一个很大的缺点,只有请求过程中发生的网络错误会是promise的状态变为rejected, 其他的诸如 http 404, 500 等等非20x状态的请求也会被认为只一个成功的请求从而使promise的状态变为fullfilled状态。所以使用时,一般会在返回值检查一下response.status 或者 respnse.ok() 来判请求断是成功:
fetch("/api/user/1",{ method:"DELETE",headers:{},body:{} }).then(resp => {
if(resp.status >= 200 && resp.status <= 299) { // 或者 if(resp.ok)
return resp.json();
} else {
return Promise.reject(resp.responseText);
}
}).then(res => {
console.log(res);
})
另外fetch也无法像xhr那样监听文件上传进度。fetch详情参考 https://developer.mozilla.org/zh-cn/docs/web/api/fetch_api
JQuery.Deffered
JQuery.Deffered 是jquery 1.5.0 中提出的一种异步编程模型的实现。由于JQuery.Deffered提出的时间比较早,使用方法与标准的Promise有很大区别
基础用法
JQuery.Deffered最简单的用法:
function defTest() {
var def = $.Deferred();
setTimeout(() => {
def.resolve("your time is up !");
},1000);
return def;
}
defTest().then(res => {
console.log(res);
});
// your time is up !
也可以在创建Deffered对象的时候传入要执行的异步操作:
function defTest() {
var def = $.Deferred(() => {
setTimeout(() => {
def.resolve("your time is up !");
},1000);
});
return def;
}
defTest().then(res => {
console.log(res);
});
// your time is up !
上面这种方式有一个弊端,就是 deferred 最终会被返回暴露出去,这样一来调用者就可以自己随意调用def.resolve或者def.reject方法改变deferred对象的状态,为了解决这个问题,Deffered对象为我们提供了.promise()方法来返回代表当前Defferred对象:
function defTest() {
var def = $.Deferred(() => {
setTimeout(() => {
def.resolve("your time is up !");
},1000);
});
return def.promise();
}
defTest().then(res => {
console.log(res);
});
// your time is up !
Deffered 的链式编程
Deffered也可以像Promise一样进行链式调用,所以上面的登录方法也可以改写成下面的形式:
function getUserByUserName(username) {
let def = $.Deferred();
let users = [{
"username":"John",
"password":"222"
},{
"username":"Marry",
"password":"111"
}];
let user = users.find(user => user.username === username);
def.resolve(user);
return def.promise();
}
function login(username,password) {
return getUserByUserName(username).then(user => {
let def = $.Deferred();
if(!!!user) throw new Error(`user: ${username} not found !`);
if(user.password === password) {
def.resolve(`user: ${username} login success`);
} else {
def.reject(`user: ${username} wrong password !`);
}
return def.promise();
})
}
login("Kate","333").then(res => { console.log(res); },error => { console.log(error)});
// Error: user: Kate not found !
login("John","111").then(res => { console.log(res); },error => { console.log(error)});
// user: John wrong password !
login("Marry","111").then(res => { console.log(res); },error => { console.log(error)});
// user: Marry login success
Deffered 和 Promise 都遵循相似的规范,实际上大多数情况下是可以混合使用的。
$.ajax: Deffered 的完美应用
jquery 的 ajax 是 jquery.Deffered 的一个很典型的应用例子,基于这一点,我们可以这样写ajax代码:
function getUserByUserName(userName) {
return $.ajax({
url:"/users.json",
}).then((users = []) => {
let user = users.find(u => u.username === userName);
let def = $.Deferred();
if(user) {
def.resolve(user);
} else {
def.reject(`user:${userName} Not Found !`);
}
return def.promise();
});
}
function login(username,password) {
return getUserByUserName(username).then(user => {
let def = $.Deferred();
user.password === password ? def.resolve(`user: ${username} login success !`) : def.reject(`password for user: ${username} is not correct !`);
return def.promise();
})
}
login("Kate","333").then(res => {
console.log(res);
}).catch(error => {
console.log(error);
});
// user:Kate Not Found !
login("John","111").then(res => {
console.log(res);
}).catch(error => {
console.log(error);
});
// password for user: John is not correct !
login("Marry","111").then(res => {
console.log(res);
}).catch(error => {
console.log(error);
});
// user: Marry login success !
jquery.Deffered 也有一些独有的api, 可以参考 https://jquery.cuishifeng.cn/deferred.resolveWith.html .
async / await
基本用法
async
函数返回一个 Promise 对象,可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
function getUserByName(name) {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve({userName:name,password:"111"});
},3000)
});
}
async function asyncTest() {
let user = await getUserByName("John");
console.log("before");
console.log(user);
}
let res = asyncTest();
console.log(`result type of asyncTest is: ${ res.constructor.name }`);
// result type of asyncTest is: Promise
// before
// { userName:"John", password:"111" }
上面的asyncTest 函数执行完后,你会发现无论执行多少次,before总是会先被打印出来,而实际调试你就会发现,执行到await这一行的时候,js引擎会等到getUserByName返回的Promise状态变成fullfilled或者rejected时,尝试获取resolve回调传递过来的值作为await表达式的值返回。这说明await 关键字吧原本异步的方法getUserByName编程了同步操作了。
注意: await 表达式所在的函数必须用 async 修饰,否则会报错
async返回值
async 关键字会把同步方法转换成一个返回 Promise对象的异步方法,方法的返回值会自动的作为 这个Promise对象的then方法回调函数的参数。
async function sayHello(name) {
return `hello, ${name}`;
}
let res = sayHello("Stark");
console.log(res.constructor?.name);
// Promise
res.then(msg => {
console.log(msg);
});
// hello, Stark
async 内部的异常处理
async
函数内部抛出错误,会导致返回的 Promise 对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到。
async 返回的Promise的状态
async
函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
await命令
通常 await 命令后面是一个promise对象(或者类Promise的thenabled对象),await 表达式的返回值就是这个Promise的结果。 如果await 后面不是一个Promise对象(或者类Promise的thenabled对象),就会直接将他作为表达式的值返回。
async function test() {
let a = await 123;
// 等同于
let a = 123;
}
await 后面如果是一个thenable对象,则会当做Promise处理。
await 的异常处理
如果await 后面的表达式报错,或者后面的Promise状态变成了resject , 则当前的async函数会中断执行,并且这个错误(reject状态)会暴露到async函数所返回的Promise内部,使async函数返回的Promise的状态立刻变成reject状态。
async function test() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
如果不想await 的reject状态改变真个async函数返回Promise的状态,可以使用try … catch 来捕获 await表达式的异常状态,或者直接使用await后面的promise对象的catch回调捕获处理这个异常
async function test() {
await Promise.reject('出错了').catch(e => { console.log(e) });
await Promise.resolve('hello world'); // 会继续执行
}
proxy
Proxy 用于拦截对象操作,并修改操作的默认行为。可以利用Proxy对对象功能进行增强。
proxy 基础
let options = {
getPrototypeOf(target){ console.log("Object.getPrototypeOf 方法的拦截器。") },
setPrototypeOf(target, value){ console.log("Object.setPrototypeOf 方法的拦截器。") },
isExtensible(target){ console.log("Object.isExtensible 方法的拦截器。") },
preventExtensions(target){ console.log("Object.preventExtensions 方法的拦截器。") },
getOwnPropertyDescriptor(target, propKey){ console.log("Object.getOwnPropertyDescriptor 方法的拦截器。") },
defineProperty(target, propKey, descriptor){ console.log("Object.getOwnPropertyDescriptor 方法的拦截器。") },
has(target, propKey){ console.log("in操作符的拦截器。") // 原对象不可配置或者禁止扩展,has()拦截会报错, has拦截无法判断属性属于自身属性还是来自原型链上的属性
get(target, propKey, receiver){ console.log("属性取值拦截器。") },
set(target, propKey, value, receiver){ console.log("属性赋值的拦截器。") },
deleteProperty(target, propKey) { console.log("delete操作符的拦截器。") }, // 如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。
ownKeys(target){ console.log("Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的拦截器。") },
apply(target, context, args) { console.log("函数调用的拦截器。") }, // 注意,不能拦截对象成员函数调用
construct(target, args, newTarget) { console.log("new操作符的拦截器。") },
}
var obj = new Proxy({},options);
obj.foo = "bar"; // 属性赋值的拦截器。
let foo = obj.foo // 属性取值拦截器。
obj.func = function() { }; // 属性赋值的拦截器。
obj.func(); // 报错,因为属性复制已经被拦截,并且没有做任何操作,所以obj.func 实际是没有值的
delete obj.func // delete操作符的拦截器。
需要特别注意的是,一旦设置了某个操作的拦截器,代理对象的这个操作的默认行为就会被完全覆盖掉。另外,即便是对象上没有操作的属性,该操作的拦截器还是会执行;
proxy语法
上面列出的拦截器方法形参的含义分别为:
- target : 原始对象
- propKey: 当前操作的属性的key
- receiver: 当前的代理对象
- value: setXXX 方法传递的目标值
- descriptor: 对象属性的描述配置
- context:函数调用是的上下文(this)
- args: 函数调用或者构造器调用是的参数列表
- newTarget: 创造实例对象时,
new
命令作用的构造函数, 可以理解为当前代理对象的构造函数
let options = {
set(target,propKey,value,receiver){
target[propKey] = value;
},
};
let obj = new Proxy({},options);
使用Proxy拦截操作时需要注意,如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。
-
例子: 利用proxy实现私有成员属性:
function checkPrivateField(key) { if(key && key.startsWith("_")) throw new Error("direct access to private member is not allowed !"); } let person = new Proxy({setAge(value) { this._age = value }},{ set:function(target, propKey, value, receiver) { checkPrivateField(propKey); target[propKey] = value; }, get:function(target, propKey, receiver) { checkPrivateField(propKey); return target[propKey]; }, })
-
使用apply进行函数参数校验
function add(...num) { return num.reduce((total = 0,item) => total + item ); } var Add = new Proxy(add,{ apply(target,context,args) { args.forEach(item => { if(typeof item !== 'number') { throw new Error(`wrong type of argument: ${item}, type: ${typeof item}, number only`) } }); return target.bind(context)(...args); } })
class 与 继承
用法与java,C#类似,目前部分浏览器还不支持class,通常在node环境中用的比较多。es6 class 申明时,没有public private 等关键字,但是有 constructor, static ,extends等关键字用于申明构造器,静态方法以及继承关系。
基本使用
es6 class 实际上可以算是一种语法糖,他的功能es5 的 function 基本都能实现, 例如一下是一段es5代码,模拟Dog类继承了Animal类:
function Animal(color, count) {
this.color = "";
this.legCount = count;
this.run = function() {
console.log(`run with ${this.legCount} legs !`);
}
}
function Dog(color,count) {
var parent = new Animal(color,count);
Object.setPrototypeOf(this,parent);
}
let dog = new Dog("black",4);
dog.run(); // run with 4 legs !
用 es6 写:
class Animal {
constructor(color,count) {
this.color = "";
this.legCount = count;
}
run() {
console.log(`run with ${this.legCount} legs !`);
}
}
class Dog extends Animal {
constructor(color,count) {
super(color, count);
}
}
let dog = new Dog("black",4);
dog.run(); // run with 4 legs !
- es6 中,类的所有方法实际上都是定义在构造函数的prototype上的,上面Animal的写法相当于:
class Animal {
constructor(color,count) {
this.color = "";
this.legCount = count;
}
run() {
console.log(`run with ${this.legCount} legs !`);
}
}
Object.setPrototypeOf(Animal,{ constructor(color,count) {
this.color = "";
this.legCount = count;
},run() {
console.log(`run with ${this.legCount} legs !`);
}
})
- 与es5不同的时,es6中类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
function Animal() {
this.run = function() {}
}
console.log(Object.keys(new Animal())); // ["run"]
class Animal {
run() {}
}
console.log(Object.keys(new Animal())); // []
setter 和 getter
与 ES5 一样,在“类”的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class Animal {
constructor(color,count) {
this.color = color;
this.legCount = count;
}
run() {
console.log(`run with ${this.legCount} legs !`);
}
set color(value) { this.color = value; }
get color() { return this.color }
}
let animal = new Animal("black", 4);
console.log(animal.color); // black
animal.count = 100;
console.log(animal.count); // 100
- class 内部自动开启严格模式,不需要声明
- class 不存在作用域提升,声明语句必须在使用语句之前
静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。和大多数强类型语言一样,通过在方法前面加上static
关键字来声明静态方法,静态方法无法被实例继承,只能通过类名调用:
class Animal {
static run() {
console.log("run where ?");
}
}
Animal.run(); // run where ?
var a = new Animal();
a.run(); // Uncaught TypeError: a.run is not a function
constructor
class 内部允许声明名称constructor的方法作为类的构造函数,如果没有申明,系统会自动提供一个没有参数的constructor方法,并且 constructor方法不能重载,
class Animal {
constructor() {}
constructor(a,v) {} // Uncaught SyntaxError: A class may only have one constructor
}
实例的属性
实例属性除了定义在constructor()
方法里面的this
上面, 也可以通过“=”直接定义在类的顶层
class Animal {
color = "red";
}
console.log(new Animal().color); //red
或者也可以通过setter, getter 定义在类内部最顶层
class Animal {
constructor() {
this.color = "red";
this._name = "dog";
}
get name() { return this._name }
set name(value) {
this._name = value;
}
}
继承
Class 可以通过extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
ES5 的继承,实质是先创造子类的实例对象this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。
如果子类没有定义constructor
方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor
方法。
另一个需要注意的地方是,在子类的构造函数中,只有调用super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super
方法才能调用父类实例。
- es6中,子类继承父类后,父类class对象会被放到子类class对象的原型对象上
class Animal {
}
class Dog extends Animal {}
console.log(Animal.isPrototypeOf(Dog)); // true
console.log(Dog.__proto__ == Animal) // true
由于静态方法和静态属性都是定义在类对象上的,因此,他们都可以被继承。
-
super
es6中super既可以当作对象使用,也可以当作函数使用
当作函数使用时,代表父类的构造函数,es6规定,如果子类自定义了构造函数,则子类构造函数中必须在调用this或者super对象之前调用一次super函数来初始化父类。否则会在运行时报错
class Dog extends Animal { constructor() { this.a = "1"; } } new Dog(); //ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
super作为对象调用时,在普通实例方法中,代指父类的原型对象,在静态方法中,指向父类。
类的prototype和__proto__
模块化 import 与 export
模块化概述
es6之前,ECMA标准中是根本没有模块化概念的,但是社区提出了好几种模块化方案,主要有CommonJS 和 AMD ,其中commonJS主要用于服务段,例如nodejs采用的就是cjs. AMD主要用于浏览器端,代表实现有requirejs 。
es6 中实现了一种简单的模块化方案,只需要通过export命令暴露出模块,然后通过import导入需要的模块:
// App.js
export class App {
consutctor(options) {
}
$mount(selector) {
let containers = document.querySelectorAll(selector);
containers.forEach(item => {
item.innerHTML = "<h1>hello ES6 !</h1>";
})
}
}
<body>
<div id="app">
</div>
</body>
<script type="module">
import App from 'App.js';
new App({}).$mount("#app");
</script>
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";
。
在es6中模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
export 与 import
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export
关键字输出该变量:
var foo = "bar";
var name = "John";
export foo;
export name;
多个export可以合并成一下形式:
export { foo,name }
export 也可以输出函数或者class。 写法与变量一样。
使用import导入模块的格式为 import { 模块名称 } from '文件路径'
:
import { foo } from '...';
如果没有指定输出名字,export 默认暴露给外部的变量名称与变量申明时的名称一致,也可以通过as关键字修改对外暴露的名称,或者也可以在import导入时使用 as 重命名
export { foo as bar , name }
import { bar as BAR } from '...'
export 可以出现在模块顶级作用域的任何位置
// test.js
var name = "John";
export foo;
var name = "John";
export name;
function hello() {}
import 命令 可以出现在模块任何位置,但是,import存在变量作用域提升,使用import导入的模块的会自动提升到当前作用域顶部.
new App({}).$mount("#app");
import { App } from 'app.js';
同一个模块被import引用多次,只执行一次。
import 可以导入模块暴露的指定的对象,也可以通过*
一次性导入全部暴露的对象
import * as obj from 'test.js';
console.log(obj.name); // "John";
// obj 不允许修改
default命令
上面所有export 都指定了暴露的对象的名称,import 是 也必须指定要导入的名称。export default 命令暴露的对象会自动转成一个匿名对象, 并没有固定的名称,在使用import导入时可以不适用as就能任意指定新的名称
// app.js
export default function Hello() {
console.log("hello")
}
// index.js
import World from '@/app.js';
new World(); // hello
但是要注意,一个文件中只能有一个 export default 暴露的对象。
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
但需要注意的是,写成一行以后,foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
export 与 import 混合写法
浏览器中使用es6模块
es6的模块化实现是的浏览器中可以不依赖第三方引擎直接使用es6模块,完美的实现了大型应用中组件按需加载的诉求。同时也提高了JavaScript代码的语言能力。
浏览器中使用es6模块化,用法与原来在浏览器中引入js文件一样,只不过需要指定script标签的type属性为 “module” 即可。
<script src="./app.js" type="module"/>
<script type="module">
import { App } from 'app.js';
</script>
注意:
-
所有module类型的脚本都会被自动标记为defer,因此,多个模块化脚本不保证按照顺序执行。
-
module script标签的顶层作用域的this对象不在是window, 而是undefined。
-
module script内部是一个独立的作用域,除非export暴露属性或者在全局对象添加属性,否则其他地方无法读取到内部的任何值。
迭代器(iterator) , 异步迭代器(async iterator) 与 for … of
iterator 概述
在java , C++,C#等大多数后端编程语言中,对于集合类型的数据如 Map,Set,List,Dictionary 都一个统一的遍历接口,即遍历器接口Iterator。javascript 在 es6以前可以当作集合类的数据解构只有Array, Object, es6中,又引入了 Map, Set, 大大扩充了js的集合数据类型。为了给这些集合类型数据提供一个同意的遍历方式,es6提供了一个iterator接口。
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令
for...of
循环,Iterator 接口主要供for...of
消费。
调用对象的iterator函数之后,返回一个具有这个对象所有key-value(entry) 和一个next方法的对象,每次调用next方法都会返回一个{ value:object,done:boolean }对象,value表示当前遍历的属性的值。done表示遍历是否结束,next内部自动指向下一个要被遍历的数据,知道遍历完成或者调用者手动退出。
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator
属性,或者说,一个数据结构只要具有Symbol.iterator
属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator
属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator
,它是一个表达式,返回Symbol
对象的iterator
属性,这是一个预定义好的、类型为 Symbol 的特殊值。所以我们可以通过一下方式调用iterator函数:
var s = new Set([1,2,4,5,6]);
let it = s[Symbol.iterator]();
var entry;
while((entry = it.next(),!entry.done)) {
console.log(entry.value);
}
// 1 2 4 5 6
for … of
像上面那样使用iterator接口其实是一件很麻烦的事情,es6 提供了 一种扩展for循环:for value of Collection
来简化迭代器的使用,for of 循环每次遍历的value就是迭代器当前指向的属性值, 并且他会自动判断遍历是否完成, 在for of 内部也可以使用 continue, break 等关键字;
let s = new Set([1,2,4,5,6]);
for(let value of s) {
if(value == 2) continue;
console.log(value);
}
// 1 4 5 6
自定义迭代器
es6允许用户自定义任何对象的默认迭代器,只要自定义迭代器部署在对象的Symbol.iterator 属性上,任何需要使用该对象的迭代器接口的地方都会默认的取调用这个自定义的迭代器, 一个自定义迭代器应该具备一下特性:
- 迭代器必须是一个函数
- 迭代器函数返回一个至少包含next方法的对象,这个方法调用后返回 一个 包含value 和done 属性的对象,value为迭代器当前遍历的值,done表示遍历是否结束。
- 迭代器一旦创建,他的执行结果不会受到后续该迭代器所属对象的后续操作的影响,迭代器内部也不应该再修改所属对象。
- 同一对象的状态调用迭代多次的结果应该一致。
- 执行操作不可逆
例如我们可以通过重写string的迭代器的方法,实现倒序打印字符:
let str = "hello ECMAScript 2016 !";
str.__proto__[Symbol.iterator] = function () {
let arr = str.split("");
let currentIndex = arr.length;
function next() {
currentIndex -- ;
return { value: arr[currentIndex], done: currentIndex< 0 };
}
return { next }
}
console.log(Array.from(str).join("")); //! 6102 tpircSAMCE olleh
和 for … of 一样,Array.from 默认调用的是也入参的迭代器接口,实际上,es6为对象提供的默认方法中,所有接收参数类型可以是数组,类数组解构的方法内部都是使用迭代器来处理这个参数的。另外,数组,set,string 的解构赋值操作也是调用的迭代器接口.
迭代器的return函数:
return函数类似于 Promise.finally 和 try … catch …finally , 用于在迭代操作提前终止(break操作或者异常)的时候做一些清理工作,return 函数要求返回一个包含done属性的对象,例如:
let str = "hello ECMAScript 2016 !";
str.__proto__[Symbol.iterator] = function () {
let arr = str.split("");
let currentIndex = arr.length;
function next() {
currentIndex -- ;
return { value: arr[currentIndex], done: currentIndex< 0};
}
function return() {
console.log(currentIndex > 0 ? "迭代异常终止或者用户手动终止!" : "迭代正常结束 !");
return { done:true };
}
return { next, return } }
}
for(let c of str) {
if(c == "d") break; // 迭代异常终止或者用户手动终止!
throw new Error(""); // 迭代异常终止或者用户手动终止!
}
原生可遍历数据
es6中有些系统数据接口默认就部署了iterator接口,因此,不需要额外的处理就可以使用for of 循环。
原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
对象Object默认没有iterator接口
let objectIterator = function() {
let keys = Object.keys(this).sort();
let values = keys.map(key => this[key]);
let currentIndex = -1;
function next() {
currentIndex++;
return { value:{key:keys[currentIndex],value:values[currentIndex]},done:currentIndex > (keys.length - 1) }
}
return { next:next.bind(this) }
}
Object.prototype[Symbol.iterator] = objectIterator;
let object = { x:1,y:2,a:4,b:5,w:"7" };
for(var entry of object) {
console.log(entry.value);
}
实际上,除非你确实有业务上的需求需要给object添加遍历器,否则上面这种做法很鸡肋,应为es6 提供了Map集合完全可以满足上面的需求:
var m = new Map({ x:1,y:2,a:4,b:5,w:"7" });
for(var value of m) {
console.log(m);
}
装饰器Decorator
装饰器可理解为java中的注解。
Reflect
reflect并没有给javascript提供新的功能,他的功能都能够通过一些旧的操作符或者函数实现。reflect 的出现主要有一下目的:
- 将原来Object上一些明显属于语言内部特性的方法(Object.defineProperty, Object.apply ,Object.setPrototypeOf …)放到Reflect上.
- 将原有的一些语言特新的操作符(delete , in …)放到Reflect上,让Object的操作都编程易于理解的函数调用行为。
- 修改某些
Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。 Reflect
对象的方法与Proxy
对象的方法一一对应。不管Proxy内部对默认方法做了什么操作,都能够在Reflect
上找到对应的默认方法。
方法
Reflect
对象一共有 13 个静态方法。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
上面这些方法的作用,大部分与Object
对象的同名方法的作用都是相同的,而且它与Proxy
对象的方法是一一对应的。
generator函数
基本使用
generator函数是一种特殊形式的函数,允许使用yield关键字将函数自动的分阶段暂停,同时分阶段的返回不同的值。
generator函数的形式:
function* test() {
...
}
function * test() {
...
}
function *test() {
...
}
generator函数执行后会立即返回一个包含一个next方法的异步执行器,通过调用这个next方法就可以一步一步的执行generator函数,并得到 {value, done}形式的不同阶段的返回值,其中value是generator函数中通过yield 返回的值,done 表示函数是否执行完毕。
function* test() {
yield "hello";
yield "world";
yield "foo";
return "bar";
}
let g = test();
g.next(); // { value: "hello", done: false }
g.next(); // { value: "world", done: false }
g.next(); // { value: "foo", done: false }
g.next(); // { value: "bar", done: true }
generator函数并不会立即执行函数体内容。必须调用next方法才能触发执行。函数中的yield表示一个暂停标识,当函数内部执行碰到yield的语句时,就会执行并返回当前的yield表达式的值,然后暂停,进入等待状态直到下一次调用next:
function* test() {
console.log("running...")
yield "hello";
return "bar";
}
let g = test();
// 这一步没有执行任何内部的代码
let { value } = g.next();
// running...
console.log(value);// hello
done属性:done标识generator函数是否执行完,这个值有系统内部维护,一般情况下,当函数遇到return或者函数执行结束了,done值就是true。
generator函数执行结束后,next方法可以继续调用,但是结果永远是{ value: undefined, done: false }
generator函数可以用for循环读取:
function* test() {
yield "hello";
yield "world";
yield "foo";
return "bar";
}
let g = test();
for(let {value, done} = g.next();!done; {value, done} = g.next()) {
console.log(value);
}
for(let val of g) {
console.log(val);
}
next方法的参数
next方法允许传入一个参数,这个参数将会作为next方法所执行的yield表达式的返回值。
每次执行generator函数都会返回一个遍历器对象,这导致执行过程中,很难改变函数内部状态,而next方法的参数,就使得状态更新变得容易许多:
function* test() {
yield "hello";
let willBreak = yield "world";
if(willBreak) {
return 'broken';
}
yield "foo";
return "bar";
}
let g = test();
g.next(); // { value: 'hello', done:false }
g.next(true); // { value: 'world', done:false }
g.next(); // { value: 'broken', done:true }
g.next(); // { value: undefined, done:true }