ECMAScript6
6可以转为5.1,babel转码器可以完成。traceur转码器也可以
let , const
let的特性如下:
- let命令生成的变量,只在let所在代码块有作用。
- for循环的特别之处,循环变量的部分是一个父作用域,循环体内部是一个单独的子作用域。
- let不能变量提升,
- let会绑定块作用域,不受外部影响(暂时性死区)
- 不允许重复声明
- ES6中对块作用域的概念进行了强调,几乎禁止了变量提升,只有在定义了变量之后才能使用。声明函数必须要在大括号中,否则报错
const的特性如下:
- 与let基本特性相同,特点是声明一个只读变量。
- const本质上是保证变量指向的那个内存地址所保存的数据不得改动。
如果是简单类型(数值,字符串,布尔值),值就保存在变量指向的地址中,因此不能改变。
但如果是对象,数组,那么固定的只是一个指针,里面的值是可以变化的。当然所固定的指针是不能指向别的地方的。
如果真的想将对象冻结,应该使用Object.freeze
方法。
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
上面代码中,常量foo
指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
ES6声明变量的六种方法
var , function , let , const , import , class
顶层对象
在浏览器中是window,在Node 中是 global
var 声明全局变量时的还是属于顶层对象,let声明时,就变成不再属于顶层对象了。
统一顶层对象,用global,但还在提议,需要用时可以查看此处。
变量的解构赋值
解构:从数组和对象中取值,对变量赋值
数组:
- 基本用法:两边解构一样,就可以直接赋值。(有Iterator接口,都可以采用数组形式解构赋值。)
- 默认值:解构赋值允许有默认值。undefined时,默认值(可以是函数)生效,
对象:
- 用法:变量与属性相同则能赋值。
- 好处:可以将现有的方法赋值到某个变量里,使用起来很方便。
- 特点:匹配模式:变量。
- 默认值:同上
- 注意: { 在行首的就是代码块,let就不起作用。
字符串:
- 用法:相当于类似的数组对象。有length属性。
数值,布尔值:
- 用法:先转化为对象。
函数参数:
- 特点:可指定默认值,解构失败后采用默认值。
圆括号:
- 不要在解构体内部加括号,真的要加时,要特别注意
用途:
- 交换变量值
- 返回多个值
- 方便参数与变量名对应起来。
- 提取JSON数据
- 设置函数默认值
- 遍历Map结构
- 输入模块的指定方法
字符串的拓展
1.字符的unicode表示法
- \uxxxx的形式表示一个字符
- 大于FFFF的,需要加大括号。
2.codePointAt()
- charCodeAt方法无法识别4个字节的字符,需要用codePointAt
- 字符转化为编码
3.String.fromCodePoint()
- fromCharCode这个方法不能识别4个字节的字符,fromCodePoint可以
- 编码转换为字符
4.遍历器接口
for … of,优点是可以识别4个字节的码点。
for (let codePoint of 'foo') {
console.log(codePoint)
}
// "f"
// "o"
// "o"
5.normalize()
解决语义相同,编码方式不一致导致的问题。
6.includes() , startsWith() , endsWith()
用于判断一个字符串是否包含另一个字符串,以前只有indexOf()
7.repeart()
表示一个字符的重复次数
8.padStart() , padEnd()
补全字符串
9.matchAll()
返回正则表达式对当前字符串的所有匹配
10.模板字符串
反引号 ` 可以多行输出字符串,.trim()方法可以消除换行。
如果字符串中有变量,就把变量写在&{}
之中
11.标签模板
非常牛逼,可应对各种转换,各种交叉应用,需要时翻看。
12.String.raw()
模板字符串的处理函数。
总结:模板字符串里面有很多规则,需要花更多时间研究。
正则拓展
1.RegExp构造函数
ES5对于有flag(修饰符)的正则,用RegExp()不能更改,ES6可以,处理方式是忽略第一次的。
2.字符串的正则方法
match() , replace() , search() , split() ,ES5中定义在String,ES6定义在RegExp上。
3.u修饰符
字符拓展。
4.unicode属性
判断表达式中是否使用了u修饰符
5.y修饰符
y→sticky→"粘连"修饰符
与g,global类似,但前后必须粘连才可以识别
sticky属性判断正则是否用来y修饰符
6.flags属性
source返回正则正文,flags返回修饰符
7.s修饰符dotAll
解决.
无法匹配行终止符的问题。dotAll属性就是判断是否用s修饰符
8.支持后行断言
9.unicode属性类
可以匹配某种属性的所有字符,就是系统把部分字符定义为一种属性,非常的方便使用。(ES2018)
10.具名组匹配
用圆括号进行组匹配,由此方法也可以应用到解构赋值和替换中。
11.matchAll()
返回一个遍历器,可以一次过找到所有匹配结果,比数组的好处在于,当匹配结果是一个很大的数组,那么遍历器比较节省资源。
数值拓展
1.二进制与八进制
要用0b 和0o 开头来表示。转为10进制要用Number。
2.isFinite() , isNaN()
判断是否有限,和是否为NaN
3.parseInt(), parseFloat()
移植到Number对象上。
4.isInteger()
判断是否为整数。如果对精度要求太高,不适合使用。
5.EPSILON
最小常量,小于该数,均无意义。
6.安全整数,isSafeInteger()
JavaScript 能够准确表示的整数范围在-2^53
到2^53
之间(不含两个端点),超过这个范围,无法精确表示这个值。
isSafeInteger()用来判断数是否落在这个范围。
7.Math对象的扩展
trunc() , sign() , cbrt() , clz32() , imul() , fround() , hypot() , expm1() , log1p() , log10() , log2() .
sinh() , cosh() , tanh() , asinh() , acosh() , atanh()
指数运算符:** ;
函数拓展
1.可以设定默认值,用解构赋值与默认值结合,undefined才能触发默认值。
2.length属性
返回函数中没有指定默认值的参数个数
注意:如果设置默认值不是尾参数,length属性就不再计算后面的参数了。
3.作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域。等初始化结束,这个作用域就会消失。如果没有设置默认值,是不会出现的。
4.rest
…变量名,可以把输入的参数转化为数组。
5.严格模式
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
6.name属性,一直支持,有些修改
7.箭头函数
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
不可以用new, 不可以用arguments,不能用yield,this是固定的,指向定义时指向的对象。总之,不会用,就不要用。
嵌套箭头函数,可以改写多重嵌套的函数。
双冒号运算符
为了实现函数绑定,绑定后,后面函数的this指向前面的对象,
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);
const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj, key) {
return obj::hasOwnProperty(key);
}
8.尾调用优化
Tail Call ,函数执行到最后一句,调用别的函数。
如果是尾调用,就不需要保留调用者,内存直接释放,提高性能。
尾递归,特别的,尾递归可以防止栈溢出。
注意:尾调用优化只在严格模式下可以执行。
数组的扩展
1.扩展运算符
...
spread 扩展运算符 , 必须放在函数的调用参数中,一定是被函数的括号包围
替代apply
// ES5 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f(...args);
其他应用:
- 复制数组
- 合并数组
- 与解构赋值结合,用于生成数组
- 将字符串转化为真正的数组
- 实现了与遍历器的接口的对象,可以通过…转化为真正的数组
- 只要有遍历器的对象都可以使用
...
,例如:Map,Set ,Generator函数
2.Array.from()
Array.from
方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
3.Aarry.of()
Array.of
方法用于将一组值,转换为数组。
4.copyWithin()
数组实例的copyWithin
方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
5.find(),findIndex()
数组实例的find
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true
的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
。
数组实例的findIndex
方法的用法与find
方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1
。
6.fill()
fill
方法使用给定值,填充一个数组。
7.数组实例的entries() , keys() , values()
keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历
8.flat() ,flatMap()
数组的成员有时还是数组,Array.prototype.flat()
用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
flatMap()
方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()
),然后对返回值组成的数组执行flat()
方法。该方法返回一个新数组,不改变原数组。
9.数组的空位
ES5对空位定义不明确,ES6中,明确转为undefined
对象扩展
1.属性的简洁表示法
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
//就是说,{foo}蕴含着{foo:foo}。不会就先不这么写,有需要时可以用一下。
2.属性名表达式
支持[] 表示属性名,[]里面的内容要先进行运算
let lastWord = 'last word';
const a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
3.方法的name属性
注意:getter , setter ,取值,存值函数在对象中定义函数时,不可以直接用name属性
const obj = {
get foo() {},
set foo(x) {}
};
obj.foo.name
// TypeError: Cannot read property 'name' of undefined
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
4.属性的可枚举性和遍历
可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。
目前,有四个操作会忽略enumerable
为false
的属性。
for...in
循环:只遍历对象自身的和继承的可枚举的属性。Object.keys()
:返回对象自身的所有可枚举的属性的键名。JSON.stringify()
:只串行化对象自身的可枚举的属性。Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。
属性的遍历
- for…in
- Object.keys(obj)
- Object.getOwnPropertyNames(obj)
- Object.getOwnPropertySymbols(obj)
- Reflect.ownKeys(obj)
5.super关键字
this关键字总是指向函数所在的当前对象,super指向当前对象的原型对象。
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
对象新增方法
1.Object.is()
与===
基本相同,不同之处只有两个:一是+0
不等于-0
,二是NaN
等于自身。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
2.Object.assign()
合并对象
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
注意点:
- 浅拷贝,只传指针
- 同名属性替换
- 会把数组误认为对象。
- 对于取值函数,会先取值,再复制
常见用途:
- 为对象添加属性
- 为对象添加方法
- clone对象
- 合并多个对象
- 为属性指定默认值
3.Object.getOwnPropertyDescriptors()
返回指定对象所有自身属性(非继承属性)的描述对象。主要是解决assign无法正确拷贝get,set的问题
4.__proto__
属性,Object.setPrototypeOf() , Object.getPrototypeOf()
__prote__
不要使用,用后面两个替代。
5.keys() , values() , entries()
遍历的方法,可以遍历key , value , 键值对-entries, for…of…
6.Object.formEntries()
可以将键值对数组转化为对象。
Symbol
符号,标记的意思。
是第七种数据类型,主要解决属性名冲突的问题,前面6种是:undefined,null,String,Number,boolean,Object.
必须放在方括号中。
还有一些其他特性,用到时再关注。
Set和Map数据结构
基本用法:
set结构不会添加重复元素
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
// 去除数组的重复成员
[...new Set(array)]
插入两个对象时,总是认为两个对象不相等,但NaN总是相等的。
Set实例的属性和方法
属性:
Set.prototype.constructor :构造函数
set.prototype.size : set实例的成员总数
方法:
分两种,操作方法,遍历方法
操作方法:
- add
- delete
- has
- clear
遍历方法:
- keys
- values
- entries
- forEach
WeakSet
可以保证一个实例只在一个类中被使用,不会出现内存泄露的问题。
MAP
JS中的对象,本质上是键值对的集合。但传统上,只能是字符串,Map数据结构可以让其他各种类型的值当做键。
Object -> 提供“字符串:值”的对应。
Map -> 提供”值:值“的对应。
Map实际上是跟内存绑定的。NaN在Map中是同一个键,因为指向同一个地方,跟内存绑定。
Map与JSON,array , object之间的转换方法。
WeakMap
只能用对象作为键,是一种弱引用,当指向的对象销毁后也会自动销毁。
proxy
翻译为代理。相当于架设一层“拦截”,外面的人要访问里面的东西,都需要经过这个代理proxy才可以。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
proxy代理了target,通过proxy实例指向target,property是代理的“过滤器”,可以重写target中的一些方法,上面就是重写get,每次执行.
运算时,都返回35,不在执行target中的点运算,但如果“过滤器”是一个空对象,那么就会直接访问target,proxy相当于一个指向target的指针,是target的一个引用。
Reflect
reflect 反应,体现的意思。
从reflect对象上可以拿到语言内部的方法。未来的新方法将只部署在reflect对象上。
reflect对象的方法和proxy对象的方法一一对应,在proxy中可以很方便的使用reflect对象的方法去执行默认行为。
有了这个对象,会让代码变得更加容易懂,原有的一些不方便的写法都会变得方便一些。
Promose对象
异步编程的一种解决方案。避免了层层嵌套。可以将异步操作以同步操作的流程表达出来。
缺点:一旦新建立即执行,中途无法取消,内部错误不会反应到外部,在pending状态时,无法得知目前进展到哪一个阶段。
如果某些事件不断地反复的发生,一般来说,使用Stream模式是比promise更好的选择。
//promise实例
const promise = new Promise(function(resolve, reject) {
// ... some code,写你异步操作的事情。
//resolve和reject函数是系统自带的,无需自己部署,
if (/* 异步操作成功 */){
resolve(value);//将promise对象的status,pending->resolved
} else {
reject(error);//将promise对象的status,pending->rejected
}
});
//then方法
//接收两个回调函数作为参数,参数1:当promise对象状态变为resolved时调用。参数2(可选):当promise对象状态变为rejected的时候调用。
//两个函数都接受Promise对象传出的值作为参数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
//promise新建后立刻执行。
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// Promise
// Hi!
// resolved
//可以看出,promise新建后,立刻执行,then方法执行会在同步任务执行完毕后才会执行。
//promise对象与then函数的关系。
/*
在promise对象中,resolve和reject函数可以传递参数,这个参数就是then中的两个回调函数中的参数。
比较特殊的是,当这个传递的参数是另外一个promise对象时,另外一个Promise对象的状态会覆盖掉当前promsie对象的状态。下面举例说明。
*/
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
//p1是一个promise,3秒之后变成rejected,p2在1秒之后resolve。由于resolve返回的方法是p1,所以p2自己的状态信息无效了,取而代之的是p1的状态,当p2执行then时,实际上,是针对p1的then。
//一般来说,调用resolve或reject以后,promise的使命就完成了,后续的操作放在then中执行。不应写在resolve和reject后面。为了避免这种问题的出现,最好在前面加return.
Promise.prototype.then()
then返回一个新的promise实例,因此可以用链式写法,then会监听promise对象的状态变化情况,变化了才会发生调用。
getJSON("/post/1.json").then(function(post) {
return getJSON(post.commentURL);
}).then(function funcA(comments) {
console.log("resolved: ", comments);
}, function funcB(err){
console.log("rejected: ", err);
});
//第一个then方法指定的回调函数,返回的是另外一个promise对象,第二个then等待这个新的promise对象状态发生变化,如果变为resolved,就调用funcA,如果变为rejected,调用funcB
可以用.then方法使异步操作有序进行。
Promise.prototype.catch()
.catch()是.then(null,rejection)或.then(undefined,rejection)的别名,只是一种写法而已。
reject方法的作用,等同于抛出错误。
值得注意的是,一旦Promise的状态变成了resolved,再抛出错误就是无效的。
promise 对象的错误具有“冒泡”性质,会一直传递到下一个catch语句。(这里我的理解是,其实then中执行了两个异步任务,第一个是resolve,返回一个Promise对象,第二个是reject,同样也返回一个promise,而且这个状态一直为rejected,所以就会跳过中间的所有then中的resolve处理函数,所以会传递到catch中。)
一般定义then中的rejected状态处理函数,一般用catch,可以任务catch就是一个语法糖,可以让代码语义更清晰。
promise内部错误不会影响到promise外部的代码,通俗的说法是“promise会吃掉错误”。下面是一个例子
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
//上诉promise执行失败,但是不会影响123的输出。
这个脚本放在服务器执行,退出码就是0
(即表示执行成功)。不过,Node 有一个unhandledRejection
事件,专门监听未捕获的reject
错误,上面的脚本会触发这个事件的监听函数,可以在监听函数里面抛出错误。
两个catch级联使用时,第二个catch会捕获第一个catch抛出的错误。
promise.prototype.finally()
不管Promise最后的状态如何,执行完then,catch之后,一定会执行finally。finally不接收任何参数,与promise的状态无关,可以认为是then的特例:
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
//如果没有finally,就要把同样的语句写两遍。
//finally总是返回原来的值。
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
promise.all()
将多个Promise实例包装成一个新的promise实例。
const p = Promise.all([p1, p2, p3]);
//可理解为:p.status = p1.status && p2.status && p3.status;其中rejected=0,resolved=1.
p的状态由p1,p2,p3决定。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
注意,如果作为参数的 Promise 实例,自己定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法。
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
//这里catch之后,会返回一个新的promise对象,然后新的promise执行完catch之后状态变为resolve,
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
Promise.race()
race,竞赛的意思,all,全部的意思
与all类似,race也是将多个promise实例封装成一个新的promise实例,不同的是机制不一样。
p1,p2,p3只要有一个改变状态,p的状态就跟着改变。率先改变的promise实例的返回值,就传递给p的回调函数。
下面是一个例子,如果指定时间内没有获得结果,就将promise的状态变为reject。
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
//由于p1在5秒内没有状态改变,p2改变为rejected,因此p也跟随p2一起改变.调用.catch。
Promise.resolve()
将现有对象转化为Promise对象,就用resolve方法
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
Promise.resolve()
方法的参数分4种情况:
- 参数是个promise对象 -> do nothing , return itself.
- 参数是一个thenable对象(具有then方法的对象) -> 对象转化为promise对象,立即执行then方法
- 参数中没有then方法,或根本不是对象。 -> 返回一个promise对象,该对象的状态从一生成就是resolved,其参数会传递给回调函数。
- 不带任何参数 -> 返回一个resolved状态的Promise对象。
Promise.reject()
返回一个状态为rejected 的promise对象。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
注意,Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。这一点与Promise.resolve
方法不一致。
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
//直接把错误丢出来,不会执行then方法。
上面代码中,Promise.reject
方法的参数是一个thenable
对象,执行以后,后面catch
方法的参数不是reject
抛出的“出错了”这个字符串,而是thenable
对象。
应用
加载图片
const preloadImage = function (path) {
return new Promise(function (resolve, reject) {
const image = new Image();
image.onload = resolve;
image.onerror = reject;
image.src = path;
});
};
generator与promise结合:
使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise
对象。
function getFoo () {//7
return new Promise(function (resolve, reject){
resolve('foo');
});
}
const g = function* () {
try {
const foo = yield getFoo();//4
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
const it = generator();//2
//done属性是generator函数中的一个属性,如果运行到return或者到函数结束了,done就会变成true
function go(result) {//5//8
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));//6//9
}, function (error) {
return go(it.throw(error));
});
}
go(it.next());//3
}
run(g);//1
//这里的意思是:generator函数每次yield一个promise对象。
上面代码的 Generator 函数g
之中,有一个异步操作getFoo
,它返回的就是一个Promise
对象。函数run
用来处理这个Promise
对象,并调用下一个next
方法。
Promise.try()
让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?回答是可以的,并且还有两种写法。第一种写法是用async
函数来写。
const f = () => console.log('now');
(async () => f())();
console.log('next');
// now
// next
//f是同步的话,上面第二行会立即执行,f是异步的话,可以用.then指定下一步。
(async () => f())()
.then(...)
.catch(...)
第二种写法是使用new Promise()
。
const f = () => console.log('now');
(
() => new Promise(
resolve => resolve(f())
)
)();
console.log('next');
// now
// next
try就是上诉写法的一种替代的语法糖,这只是个提案。
事实上,Promise.try
就是模拟try
代码块,就像promise.catch
模拟的是catch
代码块。
Iterator 和 for…of 循环
Iterator , 遍历器,主要解决多种集合形式的数据结构(object,array,map,set)带来的问题。只要部署了Iterator接口,就可以完成遍历操作。而与之配合的语句是for … of。
遍历过程:
创建一个指针,指向初始位置,使用对象的next方法,完成遍历,直至done
next调用会返回两个值,一个是value,当前值,一个是done,是否已完成遍历。
模拟next例子如下:
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
yield 读音/yi:ld / ,意思是,生产的意思。
generator函数的语法
generator 生成器的意思。
yield,生产的意思,每次执行.next,就会执行一下。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
yield可以简单的认为是在一个函数里面可return多次的语句。
next中的参数问题:
这个函数可以设置一个参数,其作用是替代掉上一次yield的值。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
//当执行foo(5).next()时,这个相当于return 5+1
//再次.next()时,这时next没有输入参数,这时yield (x + 1)这个代码块就相当于undefined,算出来y就是NaN,y/3也是NaN,输出值就是NaN
//因此next函数的参数,就是替换掉上一次yield语句,如果没有参数则传入undefined。
throw() 抛出生成器中的内部错误,
return()给定返回值,终结遍历。
next(), throw(),return()三个函数都是用不同的值去替代yield表达式。
next()
是将yield
表达式替换成一个值。throw()
是将yield
表达式替换成一个throw
语句。return()
是将yield
表达式替换成一个return
语句。
yield*表达式,用于执行generator中的generator
generator函数的异步应用
解决回调地狱 -> promise -> 未能根本解决,因为只是一种新写法 -> generator函数
协程:多线程互相协作,完成异步任务。
协程有点像函数,又有点像线程。它的运行流程大致如下。
- 第一步,协程
A
开始执行。 - 第二步,协程
A
执行到一半,进入暂停,执行权转移到协程B
。 - 第三步,(一段时间后)协程
B
交还执行权。 - 第四步,协程
A
恢复执行。
function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
上面代码的函数asyncJob
是一个协程,它的奥妙就在其中的yield
命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield
命令是异步两个阶段的分界线。
协程遇到yield
命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield
命令,简直一模一样。
协程与generator函数实现
generator是一个封装的异步任务,相当于一个异步任务容器。
指针对象的throw方法抛出的错误可以被generator中的try…catch代码块捕获。出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
thunk函数可以把多参数函数,替换程一个只接收回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
thunkify模块
thunkify模块,可以完成generator的自动执行,因为thunkify在函数内部,可以保证函数执行外面的操作,然后回到函数本身中来,这非常适合generator函数的管理。
yield后面是thunk函数时,可以自动执行脚本。
thunk不是唯一的解决方案,要想自动控制generator,关键是怎么完成接收和交还程序的执行权的问题。回调函数可以,promise对象也可以。
co模块
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
var co = require('co');
co(gen);
co模块用于generator函数的自动执行的。co函数返回的是一个promise对象。
co使用的条件是:yield后面只能是thunk,promise,所有成员都是promise的数组或对象。
co可支持并发。
// 数组的写法
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
// 对象的写法
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2),
};
console.log(res);
}).catch(onerror);
//另外一个例子。
co(function* () {
var values = [n1, n2, n3];
yield values.map(somethingAsync);
});
function* somethingAsync(x) {
// do something async
return y
}
node中的stream
node提供stream模式读写数据,特点是一次只处理数据的一部分,数据分成一块块依次处理。
Stream模式使用EventEmitter API,会释放三个事件。
- data事件:下一个数据块已经准备好。
- end事件:整个“数据流”处理完了。
- error事件:发生错误
使用race()判断三个事件哪一个最先发生,只有当data事件最先发生时,才进入下一个数据块。从而可以用一个while循环完成所有数据的读取。
const co = require('co');
const fs = require('fs');
const stream = fs.createReadStream('./les_miserables.txt');
let valjeanCount = 0;
co(function*() {
while(true) {
const res = yield Promise.race([
new Promise(resolve => stream.once('data', resolve)),
new Promise(resolve => stream.once('end', resolve)),
new Promise((resolve, reject) => stream.once('error', reject))
]);
if (!res) {
break;
}
stream.removeAllListeners('data');
stream.removeAllListeners('end');
stream.removeAllListeners('error');
valjeanCount += (res.toString().match(/valjean/ig) || []).length;
}
console.log('count:', valjeanCount); // count: 1120
});
async函数
是generator的语法糖。
内置执行器,把yield改成await,*改成async
async返回promise对象。await后面是一个promise对象。
这个函数解决的问题是上面一直谈到的自动执行,我们要用generator方法让函数自动执行,就必须用co模块或者thunkify模块这种第三方执行器,但ES6中,async函数自带了执行器。
await应该放在try…catch中。
下面的例子使用try...catch
结构,实现多次重复尝试。
const superagent = require('superagent');
const NUM_RETRIES = 3;
async function test() {
let i;
for (i = 0; i < NUM_RETRIES; ++i) {
try {
await superagent.get('http://google.com/this-throws-an-error');
break;//await执行不成功这句不会执行。
} catch(err) {}
}
console.log(i); // 3
}
test();
await互不依赖可以同时触发。
第二点,多个await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
上面代码中,getFoo
和getBar
是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo
完成以后,才会执行getBar
,完全可以让它们同时触发。
// 写法一,推荐写法,比较直观。
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
await只能用在async函数中,用在普通函数中会报错。
如果希望多个请求并发执行,可以使用Promise.all
方法。当三个请求都会resolved
时,下面两种写法效果相同。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = [];
for (let promise of promises) {
results.push(await promise);
}
console.log(results);
}
esm模块支持顶层async函数。
async函数可以保留运行堆栈:
const a = () => {
b().then(() => c());
};
上面代码中,函数a
内部运行了一个异步任务b()
。当b()
运行的时候,函数a()
不会中断,而是继续执行。等到b()
运行结束,可能a()
早就运行结束了,b()
所在的上下文环境已经消失了。如果b()
或c()
报错,错误堆栈将不包括a()
。
现在将这个例子改成async
函数。
const a = async () => {
await b();
c();
};
上面代码中,b()
运行的时候,a()
是暂停执行,上下文环境都保存着。一旦b()
或c()
报错,错误堆栈将包括a()
。
async实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async并发读取,继发输出
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
//上面函数是一个一个url去读取,然后顺序打印。这样很慢。
//下面函数是同步一起读取,然后顺序打印。async函数只有内部是继发执行的,外部不受影响。
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
异步遍历器
异步遍历器的最大的语法特点,就是调用遍历器的next
方法,返回的是一个 Promise 对象。
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
就是用Promise作为中介,让遍历器next()后生成promise对象,在调用then返回{value,done},样子就跟同步时很像:
async function f() {
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
console.log(await asyncIterator.next());
// { value: 'a', done: false }
console.log(await asyncIterator.next());
// { value: 'b', done: false }
console.log(await asyncIterator.next());
// { value: undefined, done: true }
}
可以用all方法并发执行。
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
const [{value: v1}, {value: v2}] = await Promise.all([
asyncIterator.next(), asyncIterator.next()
]);
console.log(v1, v2); // a b
for await…of
前面介绍过,for...of
循环用于遍历同步的 Iterator 接口。新引入的for await...of
循环,则是用于遍历异步的 Iterator 接口。
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b
上面代码中,createAsyncIterable()
返回一个拥有异步遍历器接口的对象,for...of
循环自动调用这个对象的异步遍历器的next
方法,会得到一个 Promise 对象。await
用来处理这个 Promise 对象,一旦resolve
,就把得到的值(x
)传入for...of
的循环体。
也就是说,of后面是一个带了异步遍历器的对象,x用来存await处理promise对象返回的值,传入循环体。
如果next返回的promise对象被rejected了。就会报错!可以用try…catch捕捉。
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}
Node v10 支持异步遍历器,Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异。
// 传统写法
function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
readStream.on('data', (chunk) => {
console.log('>>> '+chunk);
});
readStream.on('end', () => {
console.log('### DONE ###');
});
}
// 异步遍历器写法
async function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
for await (const chunk of readStream) {
console.log('>>> '+chunk);
}
console.log('### DONE ###');
}
异步generator
异步generator返回一个异步遍历器对象。
在语法上,异步 Generator 函数就是async
函数与 Generator 函数的结合。
async function* gen() {
yield 'hello';
}
const genObj = gen();
//.next就是去执行下一个yield语句,.then是把返回结果当做一个promise对象。
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。
// 同步 Generator 函数
function* map(iterable, func) {
const iter = iterable[Symbol.iterator]();
while (true) {
const {value, done} = iter.next();
if (done) break;
yield func(value);
}
}
// 异步 Generator 函数
async function* map(iterable, func) {
const iter = iterable[Symbol.asyncIterator]();
while (true) {
const {value, done} = await iter.next();
if (done) break;
yield func(value);
}
}
上面代码中,map
是一个 Generator 函数,第一个参数是可遍历对象iterable
,第二个参数是一个回调函数func
。map
的作用是将iterable
每一步返回的值,使用func
进行处理。上面有两个版本的map
,前一个处理同步遍历器,后一个处理异步遍历器,可以看到两个版本的写法基本上是一致的。
异步 Generator 函数内部,能够同时使用await
和yield
命令。可以这样理解,await
命令用于将外部操作产生的值输入函数内部,yield
命令用于将函数内部的值输出。
普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步 Iterator 对象
js中的函数总结:
普通函数:function name( args ){}
async函数:async function name(args){//await 关键字}
generator函数:function* name(args){//yield关键字}
异步generator:async function* name(args){//yield,await关键字}
对比项/函数 | 普通函数 | async函数 | generator函数 | 异步generator |
---|---|---|---|---|
特殊关键字 | 无 | await | yield | await,yield |
主要方法 | 无 | .then | .next | .next , .then |
主要解决问题 | 一般问题 | 异步函数顺序执行 | 协程,并发处理 | 相同数据结构异步操作。 |
英文意思 | 无 | await是等待的意思,每次到这里都先等待上面的完成了再往下执行,所以显然接收的是一个promise对象 | yield产出的意思,每次next都产出一个东西,next传入的参数就是替代yield语句的,因为产出了的东西就放在这个位置 | await可以将外面函数产生的值传入到内部,yield可以输出函数内部的值。在异步generator函数中可以使用这两个功能。 |
返回值 | 返回指定类型 | await返回promise对象 | yield返回{value,done},value就是返回值,done是表示生成器是否已经跑完。 | 与前面 |
由上面所述,异步generator也可以用在同步的数据结构中,只是没有异步操作,所以没有await。
yield*
yield*
语句也可以跟一个异步遍历器。
async function* gen1() {
yield 'a';
yield 'b';
return 2;
}
async function* gen2() {
// result 最终会等于 2
const result = yield* gen1();
}
上面代码中,gen2
函数里面的result
变量,最后的值是2
。
与同步 Generator 函数一样,for await...of
循环会展开yield*
。
(async function () {
for await (const x of gen2()) {
console.log(x);
}
})();
// a
// b
class的基本语法
class是第一种语法糖,需要封装时,可以用。
当然也可以用构造函数就可以了。
class的继承
跟java中的继承类似。用super建立与父类的通讯。
module的语法
模块,用import关键字完成父子模型的依赖。
模块内部的所有变量,外部无法获取。必须适用export关键字输出该变量。
as可以重命名。
输入变量应该全部当做是只读,否则很难差错。
用* as 别名
的办法,可以把一个模块整体加载进来,再通过.
运算符就可以正常访问加载进来的变量,函数等。
//写法1
import { area, circumference } from './circle';
console.log('圆面积:' + area(4));
console.log('圆周长:' + circumference(14));
//写法2
import * as circle from './circle';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
export default 就是指定默认输出,这个输出只有一个。import的时候,就不需要加大括号。
import()可以按需加载,条件加载。
直接import语句会运行前加载,就是会无视判断,加载文件时就加载。
module的加载实现
浏览器脚本异步加载:
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
defer
与async
的区别是:defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的
ES6模块是符号链接,不是复制。CommonJS是复制。