JES2015-ES6目录
名词释义
ECMAScript
ECMA是欧洲计算机制造者协会的简称;
ECMAScript 代表 JavaScript 这门语言所遵循的规范, 是一种标准规范。
ES2015
- ES2015是ECMAScript 2015的简称;
- 2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了,直到2015年ES6.0才正式出炉。因此,ES6 这个词的原意,就是指 JavaScript 语言在ES5.1后的下一个版本ES6.0;
- 在ES6出来后, 标准委员会决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。(ES2015、ES2016…);
- 所以,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准;
- 高版本ES语法转为 ES5 用 工具babel。
ES6
-
官方名称叫做 ES2015
-
ES6是社区的称呼
-
这篇笔记整理比较常用的ES6语法,详见下方教程链接
学习教程:
https://es6.ruanyifeng.com/
let 和 const
变量的定义规范
1. 尽量使用 let 和 const 定义
- 书写代码的时候尽可能优先使用 const
let 和 var 的对比
- let 认块作用域,var 认函数作用域
- let 不参与变量提升,var 参与变量提升
- let 不能在同一作用域里声明多个同名变量,var 可以(后面覆盖前面)
- var声明变量是属于顶级对象window
const:
-
const定义的是常量, 初始化时, 必须赋值
-
在ES5中, 借助对象原型上的方法定义常量:
Object.defineProperty(window, 'es', { value: '卡卡西', writable: false }) console.log(es); // -> 卡卡西 es = '鸣人' console.log(es); // -> 卡卡西
-
const定义常量的本质: 变量保存的值不能改变
-
基本数据类型, 保存“值”
-
引用数据类型, 保存“引用地址”, 此时引用类型中的值, 依然可以修改,
-
如果想引用类型中的值不能被修改,
Object.freeze()
可以解决 -
Object.freeze()
是用来冻结对象的。冻结对象后就不允许将新属性添加到对象中,并防止删除或更改现有属性。const obj = { name:'ES6', year: 2015 } Object.freeze(obj) console.log(obj) // -> 2015
-
注意:
-
方法仅冻结对象中的成员, obj变量依然可以被修改指向, 因此对象的定义依然需要借助const;
-
如果对象中的成员是多层次, 且是引用类型, 并对其中内容进行修改, freeze方法无法冻结,
只能自己定义递归函数去解决
// 定义冻结递归函数 function myFreeze (obj) { // 先冻结"第一层" Object.freeze(obj) Object.keys(obj).forEach(e => { if (typeof obj[e] == 'function' || typeof obj[e] == 'object') { myFreeze(obj[e]) } }) } const esObj = { extension: ['ES7', 'ES8', 'ES9'], name: 'ES6', year: 2015 } myFreeze(esObj) esObj.extension[0] = '666' console.log(esObj.extension[0]) // -> ES7 无法修改
-
-
-
箭头函数
- 一种新的函数定义方式
- 对于函数表达式的简写方式(匿名函数)
- 语法: () => {}
注意箭头函数-不能随意使用的场景:
- 作为事件的回调函数
- 使用箭头函数定义对象的方法
- 不可以使用arguments对象
- 不能作为构造函数
- 不能定义原型方法
-
只有一个形参且没有默认值,()可省略
注意:如果没有参数,()不能省略
(a) => {} a => {}
-
函数体中只有一句代码,可省略{}
let a = ()=> console.log(666); a() //out 666
-
没有 arguments
-
没有 this 关键字
-
=> 官方: 箭头函数里面的 this 是 上下文环境(context), 外部作用域的 this 就是箭头函数内的 this;
-
其实箭头函数中是不存在this的, 会往上一级作用域去查找this, 可以这样去理解this指向.
箭头函数的this指向原理:
- 将箭头函数的上下文环境中的this,用 _this 保存,
- 在箭头函数中使用的this其实就是上下文环境中定义的 _this
-
-
this 任何方法改变不了
- 因为箭头函数没有 this
- call / apply / bind 不能改变 箭头函数的 this 指向
函数的参数默认值
- 如果调用函数时没有传递实参, 那么就使用默认值
- 直接再形参后面使用 等于号(=) 进行赋值
- 普通函数和箭头函数都可以使用
用法示例
- 普通函数的参数默认值
function fn(a = 100, b = 200) {
// 形参 a 的默认值是 100
console.log(a)
console.log(b)
}
fn()
fn(10)
fn(10, 20)
- 箭头函数的参数默认值
let fun = (a = 1000, b = 2000) => {
console.log(a, b)
}
fun()
fun(100)
fun(100, 200)
注意:
箭头函数只要设置参数默认值, 不管多少个形参, 都得写小括号
let fun = a=100 => {} // 报错 let fun = (a=100)=> {} // 正确写法
模板字符串
使用 反引号``
特点:
-
可以换行书写
-
可以直接进行变量的拼接,解析 ${} 中的变量
-
模板字符串可以调用函数(常见于面试题)
-
字符串里面的内容是函数的参数
-
${} 把字符串切开, 组合成一个数组当作第一个参数
-
从左到右开始依次是每一个 ${}
// 代码示例:特点3
<script>
function fn(a, b, c) {
console.log(a)
console.log(b)
console.log(c)
}
var num = 100
var num2 = 200
/*
利用模板字符串调用函数
1. ${} 切开字符串 ['hello ', ' world ', ' 你好']
2. ${ num } 里面得 num 就是函数的第二个参数
3. ${ num2 } 里面的 num2 就是函数的第三个参数
*/
fn`hello ${ num } world ${ num2 } 你好`
</script>
/*
打印结果:
[ 'hello ', ' world ', ' 你好' ]
100
200
*/
扩展(展开) 运算符
- 作用: 对象、数组遍历取值简写形式
-
在函数的实参位置或者数组或者对象里面使用的时候是 “展开”
let arr1 = [1, 2, 3, 4] console.log(...arr1) console.log(Math.max(...arr1)) let arr2 = [...arr1, 5, 6, 7, 8] console.log(arr2) // *************separator************ let obj = { name: 'Jack', age: 18, gender: '男' } let obj2 = { ...obj, // 重新给gender这个属性赋值,会覆盖...obj中原来的gender值 gender: '女' } console.log(obj, obj2)
-
合并运算符
- 在函数的形参位置使用的时候是 “合并”
- 作用: 箭头函数没有 arguments, 我们使用 合并运算符整一个
function fn(...a) { // 定义一个 a 变量, 从第一个实参开始到最后一个实参全部获取, 放在一个数组里面 console.log(a) } fn(100, 200, 300, 400, 500, 600) function fn(a, ...b) { // 定义一个 a 变量, 接收第一个实参 // 定义一个变量 b, 从第二个开始到最后一个实参放在一个数组里面 console.log(a) console.log(b) } fn(100, 200, 300, 400, 500, 600) function fn(a, b, ...c) { // 定义一个 a 变量, 接收第一个实参 // 定义一个 b 变量, 接收第二个实参 // 定义一个变量 c, 从第三个开始到最后一个实参放在一个数组里面 console.log(a) console.log(b) console.log(c) } fn(100, 200, 300, 400, 500, 600) let fn = (...arg) => { console.log(arg) } fn(10, 20, 30, 40, 50)
解构赋值
对象解构赋值
-
取对象中属性的值,赋值给变量
-
注意: 对象的解构赋值,当“变量名”和“对象属性名”一致,不是按顺序的,是根据“键”名称
let {a, b, c} = {'c':10, 'b':9, 'a':8 } log(a, b, c) // 8 9 10
-
eg1:
let obj = { name:"卡卡西", age:28, gendar:"男", score:100 } let { name:name1,age:age1,gendar:gendar1,score:score1 } = obj console.log(name1,age1,gendar1,score1); // 卡卡西 28 男 100
eg2:
// 如果声明的“变量名”和“对象属性名”一致,则可以简写 let obj = { name:"卡卡西", age:28, gendar:"男", score:100 } let { name,age,gendar,score } = obj console.log( name,age,gendar,score ) // 卡卡西 28 男 100
数组的解构赋值
let arr = [11, 22, 33, 44]
let [n1, n2, n3, n4, n5] = arr
// 如果是有多的,则赋值失败,但是n5申明成功了
console.log(n1, n2, n3, n4, n5); // 11 22 33 44 undefined
// 如果只取部分
let [n1, , , n4] = arr
console.log(n1, n4); // 11 44
应用-解构赋值结合函数声明
function test({ name, age, gender,score=100 }) {
console.log(name, age, gender, score);
}
test({
name: "鹿丸",
age: 18,
gender: "男"
})
// 鹿丸 18 男 100
对象成员的简写
let name = "宇智波佐助"
let age = "18"
let gender = "男"
let obj = {
name,
age,
gender,
sayHi: ()=>{
console.log("你好呀");
}
}
console.log(obj);
obj.sayHi()
##Symbol
概述
ES6引入了一种新的原始数据类型symbo1
,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:undefined、nul1、Boolean、string、Number、object
;
只有
直接调用symbol函数才能
生成一个Symbol,注意symbo1函数前不能使用new命令,否则会报错。
symbol函数可以接受一个字符串作为参数,仅仅表示对Symbol的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let s1 = symbol('foo')
let s2 = symbol('bar')
console.log(s1) // -> symbol(foo)
console.log(s2) // -> symbol(bar)
console.log(s1 === s2) // -> false
注意,symbol函数的参数只是表示对当前Symbol值的描述
,因此相同参数的symbo1函数的返回值是不相等
// 没有参数的情况
let s1 = Symbol()
let s2 = Symbol()
console.log(s1 === s2) // -> false
// 相同参数的情况
let s1 = Symbol('foo')
let s1 = Symbol('foo')
console.log(s1 === s2) // -> false
Symbol作为对象属性
Symbol值可以作为标识符,用于对象的属性名,由于每一个Symbol值都是不相等的,这意味着就能保证不会出现同名的属性,能防止某一个键被不小心改写或覆盖的情况。
注意:
- 在对象的内部,使用Symbol值定义属性时,Symbol值
必须放在方括号之中
;- Symbol作为对象属性名时,
不能使用点运算符
,使用点运算符后,这个属性名就是一个普通字符串了,而不是Symbol类型;- Symbol作为属性名,具有隐藏性,是
不可枚举属性
,该属性不会出现在for…in、for…of循环中,也不会被Object.keys()、Object.getownpropertyNames()、JSON.stringify() 返回。但是,有一个object.getownpropertysymbo1s方法,可以获取指定对象的所有Symbol属性名。
1et mySymbol = Symbol()
//第一种写法
let a = {}
a[mySymbol] = 'Hello'
//第二种写法
let a = {
[mySymbol]:'Hel1o'
}
//第三种写法
leta={}
object.defineproperty(a,mySymbol,{value:'Hello'})
Symbol.for
有时,我们希望重新使用同一个 Symbol 值,Symbol.for
方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
Symbol.for()
与Symbol()
这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()
不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key
是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")
30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")
30 次,会返回 30 个不同的 Symbol 值。
内置Symbol
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.species
Symbol.match
Symbol.replace
Symbol.search
Symbol.split
Symbol.toPrimitive
Symbol.toStringTag
Symbol.unscopables
Symbol.iterator
,对象的Symbol.iterator
属性,指向该对象的默认生成遍历器
的方法。
Symbol的属性
Symbol.prototype.description
description 是一个只读属性,它会返回 Symbol 对象的可选描述的字符串。
description属性和Symbol.keyFor()方法的区别是:
- description能返回所有Symbol类型数据的描述,而Symbol.keyFor()只能返回Symbol.for()在全局注册过的描述
1 // Symbol()定义的数据
2 let a = Symbol("acc");
3 a.description // "acc"
4 Symbol.keyFor(a); // undefined
5
6 // Symbol.for()定义的数据
7 let a1 = Symbol.for("acc");
8 a1.description // "acc"
9 Symbol.keyFor(a1); // acc
10
11 // 未指定描述的数据
12 let a2 = Symbol();
13 a2.description // undefined
14
15
16 Symbol('desc').toString(); // "Symbol(desc)"
17 Symbol('desc').description; // "desc"
18 Symbol('').description; // ""
19 Symbol().description; // undefined
Set和WeakSet
Set
- 类比Java中的 HashSet集合
- Set没有索引, key和value是一样的
ES6 提供了新的数据结构 Set
。它类似于数组,但是成员的值都是唯一的,没有重复的值。需要记录不同成员的又不希望重复记录的情况下可以用到Set
如何生成Set:
let set1 = new Set()
let set2 = new Set([1,2,3])
Set 实例的属性:
Set.prototype.size
:返回Set
实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。
四个操作方法:
Set.prototype.add(value)
:添加某个值,返回 Set 结构本身, 可以链式编程添加多次。Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value)
:返回一个布尔值,表示该值是否为Set
的成员。Set.prototype.clear()
:清除所有成员,没有返回值。
由于Set中值不会重复,可以用来Set来做数组去重
四个遍历方法:
Set.prototype.keys()
:返回键名遍历器Set.prototype.values()
:返回键值遍历器Set.prototype.entries()
:返回键值对遍历器Set.prototype.forEach()
:使用回调函数遍历每个成员
注意:Set实例中key和value是一样的,所以keys()
和values()
这两个方法的结果是一样的
实例
Set中查找某个值是否已经存在的时间复杂度是O(1),而数组的indexOf
方法时间复杂度是O(n),又由于Set中值不会重复,所以可以使用Set做数组去重:
//使用indexOf 缺点:时间复杂度O(n^2)性能低下, NaN要做特殊处理
function deduplicate1(arr) {
let temp =[]
for (let i = 0; i < arr.length; i++) {
if(temp.indexOf(arr[i]) === -1){
temp.push(arr[i])
}
}
return temp
}
//使用对象解决性能问题 但是数组里不能有对象,null,undefined,Boolean值,也无法区分字符串和数字
function deduplicate2(arr) {
let temp = {}
for (let i = 0; i < arr.length; i++) {
if(!temp[arr[i]]){
temp[arr[i]] = true
}
}
return Object.keys(temp)
}
//使用Set来去重
function deduplicate3(arr) {
let temp = [...(new Set(arr))]
return temp
}
WeakSet
WeakSet
结构与 Set
类似,也是不重复的值的集合。但是,它与 Set
有两个区别。
-
WeakSet
的成员只能是对象,而不能是其他类型的值。 -
WeakSet
中的对象都是弱引用
如果一个对象没有任何引用,那么此对象会尽快被垃圾回收,释放掉它占用的内存。
即垃圾回收机制不考虑 WeakSet
对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet
之中。
WeakSet
结构有以下三个方法。
WeakSet.prototype.add(value)
:向 WeakSet 实例添加一个新成员。WeakSet.prototype.delete(value)
:清除 WeakSet 实例的指定成员。WeakSet.prototype.has(value)
:返回一个布尔值,表示某个值是否在WeakSet
实例之中。
WeakSet
不能遍历,是因为成员都是弱引用,随时可能消失。
示例:
let div = document.querySelector('div')
let set = new Set()
set.add(div)
//...some code
document.body.removeChild(div)
div = null //dom对象仍在内存中,因为Set中仍然引用此对象
let div = document.querySelector('div')
let weakset = new WeakSet()
weakset.add(div)
//...some code
document.body.removeChild(div)
div = null //dom对象的已经没有引用,将被垃圾回收机制回收
Map和WeakMap
Map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键,是一种更完善的 Hash 结构实现。
生成Map实例:
const map1 = new Map();
const map2 = new Map([
['name', '张三'],
['title', 'Author']
]);
Map 实例的属性:
Map.prototype.size
:返回Map
实例的成员总数。
Map实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。
四个操作方法:
Map.prototype.set(key,value)
:设置键名key
对应的键值为value
,然后返回整个 Map 结构。如果key
已经有值,则键值会被更新,否则就新生成该键。Map.prototype.get(key)
:读取key
对应的键值,如果找不到key
,返回undefined
。Map.prototype.has(key)
:返回一个布尔值,表示某个键是否在当前 Map 对象之中。Map.prototype.delete(key)
:删除某个键,返回true
。如果删除失败,返回false
。Map.prototype.clear()
:清除所有成员,没有返回值。
四个遍历方法:
Map.prototype.keys()
:返回键名遍历器Map.prototype.values()
:返回键值遍历器Map.prototype.entries()
:返回键值对遍历器Map.prototype.forEach()
:使用回调函数遍历每个成员
实例1:扩展对象
当我们有一系列对象,想记录每个对象一种属性。假设有100只鸡,需要记录每只鸡的重量,有两种思路:
- 想办法用笔写到鸡身上
- 记录到一个本本上
class Chicken {
}
// 100只鸡
let chickenList = []
for (let i = 0; i < 100; i++) {
chickenList.push(new Chicken())
}
// 方法1:记录到鸡身上
chickenList.forEach(function(chicken, index){
chicken.weight = getWeight(chicken);
});
// 方法2:记录到本本上
let notebook = [];
chickenList.forEach(function(chicken, index){
notebook[index] = getWeight(chicken);
});
第1种思路存在以下问题:
- 破坏了鸡的卖相,有时候这是很严重的事情,比如你想把一只5斤的鸡当成6斤卖出去,结果鸡身上直接写“我只有5斤”(修改了原有对象,可能导致意外的行为)
- 可能碰到一些战斗鸡,一个字都写不上去(对象冻结了或者有不可覆盖的属性)
- 可能写到一些本来就写了字的地方,导致根本看不清(与对象原有属性冲突)
再看第2种方法,存在以下问题:
- 本本无法和鸡精准地一一对应,只能靠一些索引或者标记(例如给每只鸡起一个名字)去(不可靠)地记录对应关系(无法精准地对比到是哪一个对象)
这时候就可以使用Map
扩展对象
// 记录到另一个本本上
let notebook = new Map();
chickenList.forEach(function(chicken, index){
notebook.set(chicken, getWeight(chicken));
});
实例2:完善私有属性的实现
回顾之前的Symbol
实现的私有属性的版本里,仍然存在着可以被特殊api遍历的缺陷。
基于Map
的解决思路:
用一个闭包内的Map
来扩展每个生成的对象
var Person = (function() {
var map = new Map();
function Person(name) {
map.set(this,name);
}
Person.prototype.getName = function() {
return map.get(this);
};
return Person;
}());
WeakMap
与之前介绍的WeakSet
类似,WeakMap
与 Map
有两个区别。
WeakMap
的键只能是对象,而不能是其他类型的值。WeakMap
中对键的引用是弱引用
同样地,WeakMap
不能遍历,是因为成员都是弱引用,随时可能消失。
WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
。
注意:WeakMap
弱引用的只是键名,而不是键值。键值依然是正常引用。
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
实例:完善私有属性的实现
前面基于Map
的实现还存在一个问题:
当Person
实例的外部引用消除时,闭包中的Map
仍然有Person
实例作为键的引用,Person
实例不会被垃圾回收,必须等到所有的Person
实例的外部引用消除,Map
所在的闭包也会消除,最后Person
实例才会被垃圾回收
为了解决这个问题,使用WeakMap
进一步完善:
var Person = (function() {
var wm = new WeakMap();
function Person(name) {
wm.set(this,name);
}
Person.prototype.getName = function() {
return wm.get(this);
};
return Person;
}());
Proxy
在ES6之前Object.defineProperty
可以拦截对象属性的读取和修改操作,Proxy 可以理解成比这个API更强大的,在目标对象之前架设一层的“拦截”。外界对该Proxy对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
注意:只有对生成的 Proxy 实例操作才能起到拦截的作用
生成Proxy实例:
var proxy = new Proxy(target, handler);
- target :需要代理的对象
- handler :拦截函数的集合
如果handler是空对象则代表任何操作都不会拦截
let obj = {}
/*handler为空对象*/
let proxy = new Proxy(obj, {});
proxy.a = 1
//obj.a //1
对属性的读取进行拦截:
proxy.time // 35
proxy.name // 35
proxy.title // 35
下面是 Proxy 支持的拦截操作一览,一共 13 种。
-
get(target, propKey, receiver):拦截对象属性的读取,比如
proxy.foo
和proxy['foo']
。 -
set(target, propKey, value, receiver):拦截对象属性的设置,比如
proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。 -
has(target, propKey):拦截
propKey in proxy
的操作,返回一个布尔值。 -
deleteProperty(target, propKey):拦截
delete proxy[propKey]
的操作,返回一个布尔值。 -
ownKeys(target):拦截
Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。 -
getOwnPropertyDescriptor(target, propKey):拦截
Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。 -
defineProperty(target, propKey, propDesc):拦截
Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。 -
getPrototypeOf(target):拦截
Object.getPrototypeOf(proxy)
,返回一个对象。 -
setPrototypeOf(target, proto):拦截
Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 -
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。 -
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)
。 -
isExtensible(target):拦截
Object.isExtensible(proxy)
,返回一个布尔值。 -
preventExtensions(target):拦截
Object.preventExtensions(proxy)
,返回一个布尔值。
Proxy给了开发者拦截语言默认行为的权限,可以不改变原有对象或函数的情况下,轻松运用在很多场景。例如:统计函数调用次数,实现响应式数据观测(Vue 3.0),实现不可变数据(Immutable)等等.
Reflect 反射
Reflect
是 ES6 为了操作对象而提供的新 API。ES6把原先版本中很多语言层面的API,比如Object.defineProperty
delete
in
等集中在了Reflect
的静态方法上,引入Reflect
的目的有这样几个。
(1) 将Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。也就是说,从Reflect
对象上可以拿到语言内部的方法。
(2) 修改某些Object
方法的返回结果,让其变得更合理。
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
(3)将命令式操作转变为函数调用,避免更多的保留字占用。比如name in obj
和delete obj[name]
,对应Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
(4)Reflect
对象的方法与Proxy
对象的方法一一对应,想要调用默认行为,直接在Reflect
上调用同名方法,简单可靠,省去人工写默认行为的代码。
let proxy = new Proxy({}, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target, name, value, receiver);
if (success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
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)
上面这些方法的作用,与Proxy
对象handler
的方法是一一对应的。
Iterator
Iterator
(遍历器、迭代器) 是一个对象,Iterator
对象需要包含一个next
方法,该方法返回一个对象,此对象有两个属性,一个value
表示当前结果,一个done
表示是否可以继续迭代
let it = makeIterator();
function makeIterator() {
let nextIndex = 0;
return {
next: function() {
return nextIndex < 5 ?
{value: nextIndex++, done: false} :
{value: undefined, done: true};
}
};
}
ES6 规定,如果数据结构的Symbol.iterator
属性是一个方法,该方法返回Iterator
对象,就可以认为此数据结构是“可遍历的”(iterable)
interface Iterable {
[Symbol.iterator]() : Iterator,
}
interface Iterator {
next(value?: any) : IterationResult,
}
interface IterationResult {
value: any,
done: boolean,
}
实例:
let obj = {
[Symbol.iterator]:makeIterator
}
ES6中以下场合会默认调用 Iterator
接口(即Symbol.iterator
方法),
for...of
循环- 数组解构
- 扩展运算符
yield*
- 其他隐式调用的地方,例如
new Set(['a','b'])
,Promise.all()
等
ES6中以下数据结构默认为可遍历对象,即默认部署了Symbol.iterator
属性
- Array
- Map
- Set
- String
- 函数的 arguments 对象
- NodeList 对象
Generator函数
基本概念
Generator(生成器) 函数是 ES6 提供的一种异步编程解决方案,并且Generator函数的行为与传统函数完全不同。
定义Generator函数
function* f() {
}
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部可以使用yield
关键字,定义不同的内部状态(yield
在英语里的意思就是“产出”)。
执行Generator函数
执行 Generator 函数,函数本身不会执行,而是会返回一个遍历器对象
,同时该对象也是可遍历的
,因为在其原型链上也具有Symbol.iterator
方法,并且改方法返回的对象就是该遍历器对象自身
function* f() {
console.log(1)
}
let a = f()
a[Symbol.iterator]() === a // true
因此,Generator函数返回的对象也可以被遍历,相当于每次调用此对象next()
的value
来作为遍历结果
只有执行了该遍历器对象的next()
方法,Generator函数才会执行:
function* f() {
console.log(1)
}
let a = f()
a.next() // 打印1 返回{value:undefined,done:true}
yield 和 yield*
Generator函数中可以使用yield
关键字来定义函数返回的遍历器对象每次next()
后的value
function* f() {
yield 1
}
let a = f()
a.next() // 返回 {value: 1, done: false}
并且a每次执行next()
,都会在下一个yield
处暂停,直到后面没有yield
关键字,则执行剩余代码,并且返回done:true
:
function* f() {
console.log('step1')
yield 1
console.log('step2');
yield 2
console.log('step3');
}
let a = f()
a.next() // 打印step1 返回 {value: 1, done: false}
a.next() // 打印step2 返回 {value: 2, done: false}
a.next() // 打印step3 返回 {value: undefined, done: true}
yield
本身没有返回值,yield
的返回值是下一次next()
函数传入的值。
所以next()
方法的作用有两个
- 执行本次
yield
到下一个yield
之间的代码 - 将形参的值传给本次
yield
的返回值
next()
和yield
实现了函数内外控制权的转移。
function* f() {
console.log('start');
let result = yield 1
console.log('result:',result);
}
let a = f()
yield*
等同于遍历某个对象,并且yield
每个结果
function* f() {
yield 'start'
yield* [1, 2, 3]
/*等同于*/
// for(let value of [1,2,3]){
// yield value
// }
yield 'end'
}
let a = f()
a.next() // 返回 {value: 'start', done: false}
a.next() // 返回 {value: 1, done: false}
a.next() // 返回 {value: 2, done: false}
a.next() // 返回 {value: 3, done: false}
a.next() // 返回 {value: 'end', done: false}
a.next() // 返回 {value: undefined, done: true}
Generator函数配合自动执行器
直接循环存在的问题
Generator函数是一种新的异步编程解决方案,但是每次手动调用next()
很麻烦,如果我们写一个循环来执行next()
呢?
function* f() {
yield 1
console.log('完成1');
yield 2
console.log('完成2');
}
let it = f()
let done = false
while (!done){
done = it.next().done
}
看似是没有问题,但是如果yield
后面本身就是一个异步操作,就会有问题
function* f() {
yield readFile(file1)
console.log('耶,完成了1');
yield readFile(file2)
console.log('耶,完成了2');
}
let it = f()
let done = false
while (!done){
done = it.next().done
}
//耶,完成了1
//耶,完成了2
如果我们的需求是让异步操作执行完毕后再执行yield
后面的代码,那么上述执行顺序就不符合需求。验证:
function* f() {
yield readFile(file1,function (err,data) {
console.log('读取到数据1:' + data)
})
console.log('耶,完成了1');
yield readFile(file2,function (err,data) {
console.log('读取到数据2:' + data)
})
console.log('耶,完成了2');
}
let it = f()
let done = false
while (!done){
done = it.next().done
}
//耶,完成了1
//耶,完成了2
//读取到数据1:111
//读取到数据2:222
Thunk函数
在 JavaScript 语言中,Thunk 函数是指将多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// Thunk版本的readFile(单参数版本)
const {readFile} = require('fs')
const path = require('path')
const file1 = path.join(__dirname,'./text/1.txt')
const file2 = path.join(__dirname,'./text/2.txt')
let Thunk = function (fileName) {
return function (callback) {
return readFile(fileName, callback);
};
};
let readFileThunk = Thunk(file1);
readFileThunk(function(err,data){
console.log(String(data));
});
有一个
thunkify
库可以方便的将api
变成Thunk
函数
自动执行器
写一个自动执行器run
函数,每次将it.next()
的逻辑封装到nextStep()
中,并且将nextStep
作为回调函数传给Thunk化后的读取文件函数。
// Thunk版本的readFile(单参数版本)
const {readFile} = require('fs')
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
let Thunk = function (fileName) {
return function (callback) { //result.value
return readFile(fileName, callback);
};
};
function* f() {
let data1 = yield Thunk(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield Thunk(file2)
console.log('耶,完成了2,数据是' + data2);
}
function run(f) {
let it = f();
function nextStep(err, data) {
var result = it.next(data);
if (result.done) return;
result.value(nextStep); //执行readFile,并且把nextStep作为回调传入
}
nextStep();
}
run(f)
如此一来,基于自动执行器,只要异步操作是Thunk函数或者返回Promise的情况下,写异步逻辑在形式上就如同写同步逻辑一样,非常简洁。
co模块
co模块是对一个封装的更好的自动执行器,它支持yield
的类型,不光包含thunk函数,还有Promise对象,数组,对象,甚至Generator函数
const { promisify } = require("util");
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
const readFileP = promisify(readFile)
let Thunk = function (fileName) {
return function (callback) { //result.value
return readFile(fileName, callback);
};
};
/*Thunk*/
function* f() {
let data1 = yield Thunk(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield Thunk(file2)
console.log('耶,完成了2,数据是' + data2);
}
/*Promise*/
function* f() {
let data1 = yield readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
/*数组(并发)*/
function* f() {
let data = yield [readFileP(file1),readFileP(file2)]
console.log('耶,完成了,数据是' + data);
}
/*对象(并发)*/
function* f() {
let data = yield {data1:readFileP(file1),data2:readFileP(file2)}
console.log('耶,完成了,数据是' + JSON.stringify(data));
}
/*Generator函数*/
function* f() {
function* f1() {
return yield {data1:readFileP(file1),data2:readFileP(file2)}
}
let data = yield f1()
console.log('耶,完成了,数据是' + JSON.stringify(data));
}
co(f)
经过一个co模块执行后的Generator函数会返回一个Promise对象:
co(f).then(()=>{
console.log('co执行完毕');
})
async函数
基本概念
async
函数是什么?一句话,它就是 Generator 函数的语法糖。
将上一章的代码改成 async 函数的版本:
const { promisify } = require("util");
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
const readFileP = promisify(readFile)
function* f() {
let data1 = yield readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
//async函数的版本
async function f() {
let data1 = await readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = await readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
比较后就会发现,async
函数的版本就是将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
。
定义async函数
使用async关键字定义一个async函数:
async function f() {
let data1 = await readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = await readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
执行async函数
执行async
函数则相当于执行了一个自动运行的Generator函数,async
函数如果返回的结果不是Promise,则会运行结果包装成一个Promise返回:
async function f() {
console.log(1);
}
f().then(()=>{
console.log(2);
})
async function f() {
console.log(1);
return 'done'
}
f().then(value => {
console.log(value);
})
await关键字
与yield
类似,async
函数中可以使用await
关键字,await
关键字后面一般会写一个Promise实例,async
函数执行的过程中,每次遇到await
关键字,会将控制权转回外部环境。
- 如果
await
后面是Promise实例,则会等到该 Promise实例被resolve后,才会把本次await
到下次await
之间的代码推到MircoTask(微任务)
中等待执行,并且await
的返回值是该Promise实例resolve的值 - 如果
await
后面不是Promise实例,则会立即将本次await
到下次await
之间的代码推到MircoTask(微任务)
中等待执行,并且await
的返回值是等于await
后面表达式的值:
async function f() {
let data = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve('a')
}, 2000)
})
console.log(data);
}
//f()
//console.log('end')
如果await
后面不是Promise 实例
async function f() {
let data = await 'a'
console.log(data);
}
f()
console.log('end');
//end
//a
async函数的错误处理
如果Promise被reject或抛出错误,await之后的代码不会执行,因此,需要使用try..catch
对await
进行错误捕捉:
async function f() {
try {
let data = await new Promise((resolve, reject) => {
setTimeout(() => {
reject('123')
}, 2000)
})
//后续代码无法执行
console.log('done');
}catch (e) {
console.log('发生错误:',e);
}
}
f()
async函数处理并发异步任务
如果,async
函数中的每个await
都是等到前面await
resolve后才会执行,如果想并发执行,可以使用Promise.all
:
/*并发处理异步*/
async function f() {
let time1 = new Date()
let [data1,data2] = await Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('123')
}, 2000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('123')
}, 3000)
})
])
console.log(data1,data2,'用时:'+ (new Date() - time1));
}
f()
async函数与Promise的对比
用async
函数写异步逻辑相比Promise会更加简洁,在处理不同异步结果相互依赖,错误处理,if…else分支等情况时更加简便:
const {readFile} = require('fs')
const { promisify } = require("util");
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
const file3 = path.join(__dirname, './text/3.txt')
const readFileP = promisify(readFile)
function f1() {
readFileP(file1).then(data1 =>{
console.log('耶,完成了1,数据是' + data1);
return readFileP(file2)
}).then(data2 => {
console.log('耶,完成了1,数据是' + data2);
return readFileP(file3)
}).then(data3 => {
console.log('耶,完成了1,数据是' + data3);
})
}
async function f2() {
let data1 = await readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = await readFileP(file2)
console.log('耶,完成了2,数据是' + data1 + data2);
let data3 = await readFileP(file3)
console.log('耶,完成了2,数据是' + data1 + data2 + data3);
}
f()