异步编程在JavaScript中非常重要,过多的异步编程也带了回调嵌套的问题。
解决方法
(1)拆解function:将各步拆解为单个的function (2)事件发布/监听模式 一方面,监听某一事件,当事件发生时,进行相应回调操作;另一方面,当某些操作完成后,通过发布事件触发回调,这样就可以将原本捆绑在一起的代码解耦。 (3)Promise
readFile('./sample.txt').then(content => { let keyword = content.substring(0, 5); return queryDB(keyword); }).then(res => { return getData(res.length); }).then(data => { console.log(data); }).catch(err => { console.warn(err); });(4)generator generator是es6中的一个新的语法。在function关键字后添加*即可将函数变为generator。
const gen = function* () { yield 1; yield 2; return 3; }执行generator将会返回一个遍历器对象,用于遍历generator内部的状态。
let g = gen(); g.next(); // { value: 1, done: false } g.next(); // { value: 2, done: false } g.next(); // { value: 3, done: true } g.next(); // { value: undefined, done: true }可以看到,generator函数有一个最大的特点,可以在内部执行的过程中交出程序的控制权,yield相当于起到了一个暂停的作用;而当一定情况下,外部又将控制权再移交回来。 (5)async/await 可以看到,上面的方法虽然都在一定程度上解决了异步编程中回调带来的问题。然而:
function拆分的方式其实仅仅只是拆分代码块,时常会不利于后续维护; 事件发布/监听方式模糊了异步方法之间的流程关系;Promise虽然使得多个嵌套的异步调用能够通过链式的API进行操作,但是过多的then也增加了代码的冗余,也对阅读代码中各阶段的异步任务产生了一定干扰;通过generator虽然能提供较好的语法结构,但是毕竟generator与yield的语境用在这里多少还有些不太贴切。 因此,这里再介绍一个方法,它就是es7中的async/await。 基本上,任何一个函数都可以成为async函数,以下都是合法的书写形式: async function foo () {}; const foo = async function () {}; const foo = async () => {}; 在async函数中可以使用await语句。await后一般是一个Promise对象。 async function foo () { console.log('开始'); let res = await post(data); console.log(`post已完成,结果为:${res}`); };当上面的函数执行到await时,可以简单理解为,函数挂起,等待await后的Promise返回,再执行下面的语句。 值得注意的是,这段异步操作的代码,看起来就像是“同步操作”。这就大大方便了异步代码的编写与阅读。
简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果,语法上说,Promiese是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。Promise 就是用同步的方式写异步的代码,用来解决回调问题 从控制台打印出来一个Promise 对象来看下 可以看到,它是一个构造函数,既有属于自己私有的 resolve, reject, all, race等方法,也有protype 原型上的 then, catch等方法。
基本用法
Promise.resolve() 的用法 Promise.resolve()方法可以将现有对象转为Promise 对象。 var p = Promise.resolve($.ajax('/something.data')); p.then((val) => {console.log(val)}); 它等价于 var p = new Promise(resolve => { resolve($.ajax('/something.data')) }); p.then((val) => {console.log(val)}); Promise.reject() 用法 此方法和Promise.resolve()方法类似,除了rejecet 代表状态为 Rejected,不多说。Promise.all() 用法 用于将多个Promise 实例包装成一个新的 Promise实例,参数为一组 Promise 实例组成的数组。 var p = Promise.all([p1,p2,p3]);当 p1, p2, p3 状态都 Resolved 的时候,p 的状态才会 Resolved;只要有一个实例 Rejected ,此时第一个被 Rejected 的实例的返回值就会传递给 P 的回调函数。 应用场景:假设有一个接口,需要其它两个接口的数据作为参数,此时就要等那两个接口完成后才能执行请求。
var p1 = new Promise((resolve, reject) => { setTimeout(resolve, 1000, 'P1'); }); var p2 = new Promise((resolve, reject) => { setTimeout(resolve, 2000, 'P2'); }); // 同时执行p1和p2,并在它们都完成后执行then: Promise.all([p1, p2]).then((results) => { console.log(results); // 获得一个Array: ['P1', 'P2'] }); Promise.race() 用法 和Promise.all 类似,区别是 Promise.race() 只要监听到其中某一个实例改变状态,它的状态就跟着改变,并将那个改变状态实例的返回值传递给回调函数。 应用场景: 可以通过多个异步任务来进行容错处理,多个接口返回同样的数据,只要有一个接口生效返回数据即可。Promise.prototype.then() then 方法是定义在 Promise 的原型对象上的,作用是为 Promise 实例添加状态改变时的回调函数;then() 返回一个新的Promise 实例,因此可以支持链式写法。Promise.prototype.catch() catch 方法是一个语法糖,看下面代码就明白了,用于指定发生错误时的回调函数。 var p = new Promise((resolve, rejecet) => { if (...) {resolve()}; else {reject()}; }) p.then((val) => { console.log('resolve:', val); }).catch((err) => console.log('reject:', err)); // 等同于 p.then((data) => { console.log(data); }, (err) => { console.log(err); }) // 后一种写法更好,语义更清晰,第一种方法在第一个函数里面出错的话时在第二个函数里监听不到变化的。 Promise.try() 实际开发中,经常遇到一种情况:不知道或者不想区分,函数 f 是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作,都用 then 方法指定下一步流程,用 catch 方法处理 f 抛出的错误。 (1)第一种方法,缺陷是不能识别同步请求。 const f = () => console.log('now'); Promise.resolve().then(f); console.log('next'); // next // now(2)new Promise() 写法
const f = () => console.log('now'); ( () => new Promise( resolve => resolve(f()) ) )(); console.log('next'); // now // next(3)Promise.try 写法,替代new Promise() 方法,更简洁。
const f = () => console.log('now'); Promise.try(f); console.log('next'); // now // next总结
两个特点:
状态不受外界影响,只有异步操作的结果会影响到它,pending(进行中),reject(已失败),resolved(已完成)状态只能改变一次感受:
promise 首先是一个构造函数,所以需要new 出来一个实例来使用像是一个 ajax 函数外面加了一层包裹层,封装了一下下,实现了代码层面的同步效果缺点也很明显,就是代码语义化不够,一眼看去都是Promise 的 API,then catch 等等,不能很快明白代码究竟想表达什么意思,这也是 async 函数出现的原因,async ,可能是异步操作的终极方案了。JS语言的传统方法是通过构造函数,定义并生成新对象,是一种基于原型的面向对象系统。在ES6中新增加了类的概念,可以使用 class 关键字声明一个类,之后以这个类来实例化对象。 构造函数示例
function Point(x, y) { this.x = x; this.y = y; } Point.prototype.toString = function () { return '(' + this.x + ', ' + this.y + ')'; }; var p = new Point(1, 2);ES6示例
class Point{ constructor(x,y){ this.x = x; this.y = y; } toString(){ return '(' +this.x +')' + this.y +')'; } }ES6的类,完全可以看做是构造函数的另一种写法类的数据类型就是函数,类本身就是指向构造函数。构造函数上的prototype属性,在ES6的“类”上继续存在,事实上,类的所有方法都定义在类的prototype上。
class Person(){ toString(){ //..... } toNum(){ //... } } //等同于 Person.prototype = { toString(){} toNum(){} }在类上调用方法就是在原型上调用方法。由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。
Object.assign(Point.prototype,{ toString(){}, toNum(){} });constructor 方法 constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
class Point { } // 等同于 class Point { constructor() {} }上面代码中,定义了一个空的类Point,JavaScript 引擎会自动为它添加一个空的constructor方法。
constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
class Foo { constructor() { return Object.create(null); } } new Foo() instanceof Foo // false上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。
类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。
class Foo { constructor() { return Object.create(null); } } Foo() // TypeError: Class constructor Foo cannot be invoked without 'new'类的实例对象 生成类的实例对象的写法,与 ES5 完全一样,也是使用new命令。前面说过,如果忘记加上new,像函数那样调用Class,将会报错。
class Point { // ... } // 报错 var point = Point(2, 3); // 正确 var point = new Point(2, 3);与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
与 ES5 一样,类的所有实例共享一个原型对象。
var p1 = new Point(2,3); var p2 = new Point(3,2); p1.__proto__ === p2.__proto__ //true上面代码中,p1和p2都是Point的实例,它们的原型都是Point.prototype,所以__proto__属性是相等的。这也意味着,可以通过实例的__proto__属性为“类”添加方法。
Class 的静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。 class Foo { static classMethod() { return 'hello'; } } Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function 如果静态方法包含this关键字,这个this指的是类,而不是实例。 class Foo { static bar () { this.baz(); } // 静态方法可以与非静态方法重名 static baz () { console.log('hello'); } baz () { console.log('world'); } } Foo.bar() // hello 父类的静态方法,可以被子类继承。 class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { } Bar.classMethod() // 'hello' 静态方法也是可以从super对象上调用的。 class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { static classMethod() { return super.classMethod() + ', too'; } } Bar.classMethod() // "hello, too"class的继承
Class 可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。使用继承的方式,子类就拥有了父类的方法。 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } } class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() } } 如果子类中有constructor构造函数,则必须使用调用super。如果不调用super方法,子类就得不到this对象。 class Point { /* ... */ } class ColorPoint extends Point { constructor() { } } let cp = new ColorPoint(); // ReferenceError 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则 会报错。 class Point { constructor(x, y) { this.x = x; this.y = y; } } class ColorPoint extends Point { constructor(x, y, color) { this.color = color; // ReferenceError super(x, y); this.color = color; // 正确 } }super 关键字
super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
super作为函数调用时,代表父类的构造函数。ES6要求,子类的构造函数必须执行一次super函数。 class A { constructor() { console.log(new.target.name); } } class B extends A { constructor() { super(); } } new A() // A new B() // B作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。 2. super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A { p() { return 2; } } class B extends A { constructor() { super(); console.log(super.p()); // 2 } } let b = new B();上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。 由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。
Class 和传统构造函数有何区别
Class 在语法上更加贴合面向对象的写法Class 实现继承更加易读、易理解,对初学者更加友好、本质还是语法糖,使用prototype