0.0 概述
本文總結(jié)了js中函數(shù)相關(guān)的大部分用法,對(duì)函數(shù)用法不是特別清晰的同學(xué)可以了解一下。
1.0 簡(jiǎn)介
同其他語(yǔ)言不同的是,js中的函數(shù)有2種含義。
普通函數(shù):同其他語(yǔ)言的函數(shù)一樣,是用于封裝語(yǔ)句塊,執(zhí)行多行語(yǔ)句的語(yǔ)法結(jié)構(gòu)。
構(gòu)造函數(shù):不要把它當(dāng)作函數(shù),把它當(dāng)作class,內(nèi)部可以使用this表示當(dāng)前對(duì)象。
【注】后續(xù)代碼基于ES6&ES7標(biāo)準(zhǔn),筆者是在nodejs v10.7.0環(huán)境下運(yùn)行(你也可以選擇其他支持ES6的node版本)。
1.1 函數(shù)的聲明
雖然普通函數(shù)和構(gòu)造函數(shù),含義有所不同,可是聲明方法卻完全一樣。
1.1.0 函數(shù)聲明
function sort(arr) {
let ret = [...arr];
let length = ret.length;
for (let i = 0; i < length; i++) {
for (let j = i + 1; j < length; j++) {
if (ret[i] > ret[j]) {
[ret[j], ret[i]] = [ret[i], ret[j]];
}
}
}
return ret;
}
1.1.1 函數(shù)表達(dá)式
let sort = function (arr) {
let ret = [...arr];
...
...
return ret;
}
函數(shù)表達(dá)式和普通函數(shù)聲明的區(qū)別在于,普通函數(shù)聲明會(huì)提升,函數(shù)表達(dá)式不會(huì)提升。
“提升”的意思是說(shuō): 在函數(shù)聲明前就可以調(diào)用這個(gè)函數(shù)。不必先聲明后調(diào)用。
js會(huì)在運(yùn)行時(shí),將文件內(nèi)所有的
函數(shù)聲明,都提升到文件最頂部,這樣你可以在代碼任意位置訪(fǎng)問(wèn)這個(gè)函數(shù)。而現(xiàn)在根據(jù)ES6標(biāo)準(zhǔn),使用
var修飾的函數(shù)表達(dá)式會(huì)提升,使用let修飾的則不會(huì)提升。
1.1.2 使用Function構(gòu)造函數(shù)聲明
let sort = new Function("arr", `
function sort(arr) {
let ret = [...arr];
let length = ret.length;
for (let i = 0; i < length; i++) {
for (let j = i + 1; j < length; j++) {
if (ret[i] > ret[j]) {
[ret[j], ret[i]] = [ret[i], ret[j]];
}
}
}
return ret;
}
`);
這種使用Function構(gòu)造方法創(chuàng)建的函數(shù),同函數(shù)聲明產(chǎn)生的函數(shù)是完全相同的。
構(gòu)造函數(shù)接收多個(gè)字符串作為參數(shù),最后一個(gè)參數(shù)表示函數(shù)體,其他參數(shù)表示參數(shù)名。
像上面這個(gè)例子和1.1.0中的聲明完全相同。
這種聲明方式,沒(méi)有發(fā)現(xiàn)有什么優(yōu)點(diǎn),并不推薦使用。
1.2 閉包
閉包,簡(jiǎn)單說(shuō)就是在函數(shù)中聲明的函數(shù),也就是嵌套函數(shù)。它能夠延長(zhǎng)父作用域部分變量的生命周期。
閉包可以直接使用其所在函數(shù)的任何變量,這種使用是引用傳遞,而不是值傳遞,這一點(diǎn)很重要。
let f = function generator() {
let arr = [1, 2, 3, 4, 5, 6, 7];
let idx = 0;
return {
next() {
if (idx >= arr.length) {
return { done: true };
} else {
return { done: false, value: arr[idx++] };
}
}
}
}
let gen = f();
for (let i = 0; i < 10; i++) {
console.log(gen.next());
}
上面的代碼中,generator函數(shù)中的閉包next()可直接訪(fǎng)問(wèn)并修改所在函數(shù)中的變量arr和idx。
一般說(shuō)來(lái),閉包需要實(shí)現(xiàn)尾遞歸優(yōu)化。
尾遞歸是指,如果一個(gè)函數(shù),它的最后一行代碼是一個(gè)閉包的時(shí)候,會(huì)在函數(shù)返回時(shí),釋放父函數(shù)的??臻g。
這樣一來(lái),依賴(lài)閉包的遞歸函數(shù)就不怕棧溢出了(nodejs在64位機(jī)器上可達(dá)到1萬(wàn)多層的遞歸才會(huì)溢出,有可能是根據(jù)內(nèi)存情況動(dòng)態(tài)計(jì)算的)。
ES6明確要求支持尾遞歸。
而據(jù)網(wǎng)絡(luò)上資料說(shuō),nodejs需要在嚴(yán)格模式下,使用--harmony選項(xiàng),可以開(kāi)啟尾遞歸。
然而我使用下列代碼發(fā)現(xiàn),并沒(méi)有開(kāi)啟(nodejs版本為v10.3.0)。
// File: test.js
// Run: node --harmony test.js
"use strict"
function add(n, sum) {
if (n == 0) {
console.trace();
return sum;
} else {
return add(n - 1, sum + n);
}
}
console.log(add(10, 0));
/*
輸出為:
Trace
at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:5:11)
at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10)
at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10)
at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10)
at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10)
at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10)
at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10)
at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10)
at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10)
at add (/Users/hongyuwang/Desktop/javascript/learn/learn.js:8:10)
55
*/
1.3 匿名函數(shù)
我們經(jīng)常在js的代碼中看見(jiàn)下面這種寫(xiě)法:
(function(){
...
...
...
})();
將一個(gè)匿名函數(shù)直接執(zhí)行,如果剛接觸js的同學(xué)可能覺(jué)得這是脫褲子放屁。
但是這個(gè)匿名函數(shù)的最大作用在于作用域隔離,不污染全局作用域。
如果沒(méi)有匿名函數(shù)包裹,代碼中聲明的所有變量都會(huì)出現(xiàn)在全局作用域中,造成不必要的變量覆蓋麻煩和性能上的損失。
ES6中這種寫(xiě)法可以?huà)仐壛?,因?yàn)镋S6引入了塊作用域:
{
...
...
...
}
作用和上面的匿名函數(shù)相同。
另外ES6中增加了一種匿名函數(shù)的寫(xiě)法:
//ES6以前的寫(xiě)法
function Teacher(name){
this.name = name;
var self = this;
setTimeout(function(){
console.log('Teacher.name = ' + self.name);
}, 3000);
}
//現(xiàn)在這樣寫(xiě)
function Student(name){
this.name = name;
setTimeout(() => {
console.log('Student.name = ' + this.name);
}, 3000);
}
新的匿名函數(shù)的在寫(xiě)法上有2處不同:
- 去掉了
function關(guān)鍵字 - 在參數(shù)列表和函數(shù)體之間增加了
=>符號(hào)
而它也帶來(lái)了一個(gè)巨大的好處:
匿名函數(shù)中的
this對(duì)象總是指向聲明時(shí)所在的作用域的this,不再指向調(diào)用時(shí)候的this對(duì)象了。
這樣我們就可以像上面的例子那樣,很直觀地使用this,不用擔(dān)心出現(xiàn)任何問(wèn)題。
所以比較強(qiáng)烈推薦使用新的匿名函數(shù)寫(xiě)法。
1.4 構(gòu)造函數(shù)和this
1.4.1 基本面向?qū)ο笳Z(yǔ)法
下面來(lái)介紹構(gòu)造函數(shù),js沒(méi)有傳統(tǒng)面向?qū)ο蟮恼Z(yǔ)法,但是它可以使用函數(shù)來(lái)模擬。
了解js面向?qū)ο髾C(jī)制之前,可以先看一下,其他標(biāo)準(zhǔn)面向?qū)ο笳Z(yǔ)言的寫(xiě)法,比如java,我們聲明一個(gè)類(lèi)。
class Person{
//構(gòu)造函數(shù)
Person(String name, int age){
this.name = name;
this.age = age;
Person.count++;
}
//屬性
String name;
int age;
//setter&getter方法
String getName(){
return this.name;
}
void setName(String name){
this.name = name;
}
int getAge(){
return this.age;
}
void setAge(int age){
this.age = age;
}
//靜態(tài)變量
static int count = 0;
//靜態(tài)方法
public int getInstanceCount(){
return Person.count;
}
}
由此可知,一個(gè)類(lèi)主要包含如下元素:構(gòu)造函數(shù),屬性,方法,靜態(tài)屬性,靜態(tài)方法。
在js中,我們可以使用js的構(gòu)造函數(shù),來(lái)完成js中的面向?qū)ο蟆?/p>
js的構(gòu)造函數(shù)就是用來(lái)做面向?qū)ο舐暶鳎暶?code>類(lèi))的。
構(gòu)造函數(shù)的聲明語(yǔ)法同普通函數(shù)完全相同。
//構(gòu)造函數(shù)
function Person(name, age){
//屬性
this.name = name;
this.age = age;
//setter&getter
this.getName = function(){
return this.name;
}
this.setName = function(name){
this.name = name;
}
this.getAge = function(){
return this.age;
}
this.setAge = function(age){
this.age = age;
}
Person.count++;
}
//靜態(tài)變量
Person.count = 0;
//靜態(tài)方法
Person.getInstanceCount = function(){
return Person.count;
}
可以發(fā)現(xiàn),構(gòu)造函數(shù)中同普通函數(shù)相比,特別的地方在于使用了this,同其他面向?qū)ο蟮恼Z(yǔ)言一樣,this表示當(dāng)前的實(shí)例對(duì)象。
把我們用js聲明的類(lèi)與java的類(lèi)相對(duì)比,二者除了寫(xiě)法不同之外,上述關(guān)鍵元素也都包含了。
1.4.2 prototype
js使用上面的方法聲明了類(lèi)之后,就可以使用new關(guān)鍵字來(lái)創(chuàng)建對(duì)象了。
let person = new Person("kaso", 20);
console.log("person.name=" + person.getName() + ", person.age=" + person.getAge());
//輸出:person.name=kaso, person.age=20
let person1 = new Person("jason", 25);
console.log("person.name=" + person.getName() + ", person.age=" + person.getAge());
//輸出:person.name=jason, person.age=25
創(chuàng)建對(duì)象,訪(fǎng)問(wèn)屬性,訪(fǎng)問(wèn)方法,都沒(méi)問(wèn)題,看起來(lái)挺好的。
但是當(dāng)我們執(zhí)行一下這段代碼,會(huì)發(fā)現(xiàn)有些不對(duì):
console.log(person.getName === person1.getName);
//輸出:false
原來(lái)構(gòu)造函數(shù)在執(zhí)行的時(shí)候,會(huì)將所有成員方法,為每個(gè)對(duì)象生成一份copy,而對(duì)于類(lèi)成員函數(shù)來(lái)說(shuō),保留一份copy就足夠了,而不同的對(duì)象可以用this來(lái)區(qū)分。上面的做法很明顯,內(nèi)存被白白消耗了。
基于上述問(wèn)題,js引入了prototype關(guān)鍵字并規(guī)定:
存儲(chǔ)在prototype中的方法和變量可以在類(lèi)的所有對(duì)象中共享。
因此,上面的構(gòu)造函數(shù)可以修改成這樣:
function Person(name, age){
this.name = name;
this.age = age;
Person.count++;
}
Person.prototype.getName = function(){
return this.name;
}
Person.prototype.setName = function(name){
this.name = name;
}
Person.prototype.getAge = function(){
return this.age;
}
Person.prototype.setAge = function(age){
this.age = age;
}
Person.count = 0;
Person.getInstanceCount = function(){
return Person.count;
}
運(yùn)行效果和之前的寫(xiě)法相同,只是這次創(chuàng)建不同的對(duì)象時(shí),成員方法不再創(chuàng)建多個(gè)副本了。
需要注意的是,成員變量不需要放到prototype中,可以想想為什么。
1.4.3 apply和call
js函數(shù)中繞不過(guò)的一個(gè)問(wèn)題就是,方法里面的this到底指向哪里?
最官方的說(shuō)法是:this指向調(diào)用此方法的對(duì)象。
對(duì)于類(lèi)似于java這種面向?qū)ο蟮恼Z(yǔ)言來(lái)講,this永遠(yuǎn)指向所在類(lèi)的對(duì)象實(shí)例。
對(duì)于js中也是這樣,如果我們規(guī)規(guī)矩矩地像上一節(jié)介紹的那樣使用,this也會(huì)指向所在類(lèi)的對(duì)象實(shí)例。
但是,js也提供了更為靈活的語(yǔ)法,它可以讓一個(gè)方法被不同的對(duì)象調(diào)用,即使不是同一個(gè)類(lèi)的對(duì)象,也就是可以將同一個(gè)函數(shù)的this,設(shè)為不同的值。
這是一個(gè)極為靈活的語(yǔ)法,可以完成其他語(yǔ)言類(lèi)似接口(interface),擴(kuò)展(extension),模版(template)的功能。
實(shí)現(xiàn)此功能的方法有2個(gè):apply和call,二者實(shí)現(xiàn)的功能完全相同,即改變函數(shù)的this指向,只是函數(shù)傳遞參數(shù)方式不同。
call接受可變參數(shù),同函數(shù)調(diào)用一樣,需將參數(shù)一一列出。
apply只接受2個(gè)參數(shù),第一個(gè)就是新的this指向的對(duì)象,第二個(gè)參數(shù)是原參數(shù)用數(shù)組保存起來(lái)。
代碼如下:
let obj = {
print(a, b, c){
console.log(`this is obj.print(${a}, $, ${c})`);
}
}
let obj1 = {
print(a, b, c){
console.log(`this is obj1.print(${a}, $, ${c})`);
}
}
function test(a, b, c){
this.print(a, b, c);
}
test.apply(obj, [1, 2, 3]);
test.call(obj, 4, 5, 7);
test.apply(obj1, [1, 2, 3]);
test.call(obj1, 4, 5, 7);
/* 輸出:
this is obj.print(1, 2, 3)
this is obj.print(4, 5, 7)
this is obj1.print(1, 2, 3)
this is obj1.print(4, 5, 7)
*/
1.4.4 繼承
面向?qū)ο?大特征:封裝,繼承,多態(tài),其中最重要的就是繼承,多態(tài)也依賴(lài)于繼承的實(shí)現(xiàn)。可以說(shuō)實(shí)現(xiàn)了繼承,就實(shí)現(xiàn)了面向?qū)ο蟆?/p>
java中的繼承很簡(jiǎn)單:
class Student extends Person{
... ...
}
Student繼承之后自動(dòng)獲得Person的所有成員變量和成員方法。
因此,我們?cè)趯?shí)現(xiàn)js繼承的時(shí)候,主要就是獲取到父類(lèi)的成員變量和成員方法。
最簡(jiǎn)單的實(shí)現(xiàn)就是,將父類(lèi)的成員變量和方法直接copy到子類(lèi)中。
這需要做2件事:
- 為了copy成員方法,可以將Student的prototype指向父類(lèi)的prototype
- 為了copy成員屬性,子類(lèi)構(gòu)造函數(shù)需要調(diào)用父類(lèi)構(gòu)造函數(shù)
function Student(name, age){
Person.call(self, name, age);
}
Student.prototype = Person.prototype;
上面代碼可以達(dá)到繼承的目的,但是會(huì)產(chǎn)生兩個(gè)問(wèn)題
- 如果我向Student中添加新的成員方法時(shí),會(huì)同時(shí)加入到父類(lèi)中
- 多層次繼承無(wú)法實(shí)現(xiàn),即當(dāng)所調(diào)用的方法在父類(lèi)中找不到的時(shí)候,不會(huì)去父類(lèi)的父類(lèi)中去查找
所以我們不能直接將Person.prototype直接給Student.prototype。
經(jīng)過(guò)思考,一個(gè)可行方案是,令子類(lèi)prototype指向父類(lèi)的一個(gè)對(duì)象,即像這樣:
Student.prototype = new Person();
這樣做,可以解決上面的2個(gè)問(wèn)題。
但是它仍然有些瑕疵:會(huì)調(diào)用2次父類(lèi)構(gòu)造函數(shù),造成一定的性能損失。
所以我們的終極繼承方案是這樣的:
function Student(name, age){
Person.call(self, name, age);
}
function HelpClass(){}
HelpClass.prototype = Person.prototype;
Student.prototype = new HelpClass();
上面關(guān)鍵代碼的意義在于,用一個(gè)空的構(gòu)造函數(shù)代替父類(lèi)構(gòu)造函數(shù),這樣調(diào)用了一個(gè)空構(gòu)造函數(shù)的代價(jià)會(huì)小于調(diào)用父類(lèi)構(gòu)造函數(shù)。
另外上述代碼可以用Object.create函數(shù)簡(jiǎn)化:
function Student(name, age){
Person.call(self, name, age);
}
Student.prototype = Object.create(Person.prototype);
這就是我們最終的繼承方案了??梢詫?xiě)成下面的通用模式。
function extend(superClass){
function subClass(){
superClass.apply(self, arguments);
}
subClass.prototype = Object.create(superClass.prototype);
return subClass;
}
let Student = extend(Person);
let s = new Student('jackson', '34');
console.log("s.getName() = " + s.getName() + ", s.getAge() = " + s.getAge());
//輸出為:s.getName() = jackson, s.getAge() = 34
當(dāng)然實(shí)現(xiàn)一個(gè)完整的繼承還需要完善其他諸多功能,在這里我們已經(jīng)解決了最根本的問(wèn)題。
1.5 generator函數(shù)和co
generator是ES6中提供的一種異步編程的方案。有點(diǎn)像其他語(yǔ)言(lua, c#)中的協(xié)程。
它可以讓程序在不同函數(shù)中跳轉(zhuǎn),并傳遞數(shù)據(jù)。
1.5.1 基本用法介紹
看下面的代碼:
function *generatorFunc(){
console.log("before yield 1");
yield 1;
console.log("before yield 2");
yield 2;
console.log("before yield 3");
let nextTransferValue = yield 3;
console.log("nextTransferValue = " + nextTransferValue);
}
let g = generatorFunc();
console.log("before next()");
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next(1024));
/*輸出:
before next()
before yield 1
{ value: 1, done: false }
before yield 2
{ value: 2, done: false }
before yield 3
{ value: 3, done: false }
nextTransferValue = 1024
{ value: undefined, done: true }
*/
可以看到generator函數(shù)有3要素:
- 需要在函數(shù)名字前面,加上
* - 需要在函數(shù)體中使用
yield - 調(diào)用的時(shí)候需要使用
next()函數(shù)
另外還有一些其他規(guī)則:
- generator函數(shù)內(nèi)的第一行代碼,需要在第一個(gè)
next()執(zhí)行后執(zhí)行 - 函數(shù)在執(zhí)行
next()時(shí),停頓在yield處,并返回yield后面的值,yield后的代碼不再執(zhí)行。 -
next()返回的形式是一個(gè)對(duì)象:{value: XXX, done: false},這個(gè)對(duì)象中,value表示yield后面的值,done表示是否generator函數(shù)已經(jīng)執(zhí)行完畢,即所有的yield都執(zhí)行過(guò)了。 -
next()可以帶參數(shù),表示將此參數(shù)傳遞給上一個(gè)yield,因?yàn)樯洗螆?zhí)行next()的時(shí)候,代碼停留在上次yield的位置了,再執(zhí)行next()的時(shí)候,會(huì)從上次yield的位置繼續(xù)執(zhí)行代碼,同時(shí)可以令yield表達(dá)式有返回值。
從上述介紹中可以看出,generator除了在函數(shù)中跳轉(zhuǎn)之外,還可以通過(guò)next()來(lái)返回不同的值。
了解過(guò)ES6的同學(xué)應(yīng)該知道,這種next()序列,特別符合迭代器的定義。
因此,我們可以很容易把generator的函數(shù)的返回值組裝成數(shù)組,還可以用for..of表達(dá)式來(lái)遍歷。
function *generatorFunc(){
yield 1;
yield 2;
yield 3;
}
let g = generatorFunc();
for(let i of g){
console.log(i);
}
/*
輸出:
1
2
3
*/
function *generatorFunc(){
yield 1;
yield 2;
yield 3;
}
let g = generatorFunc();
console.log(Array.from(g));
/*
輸出:
[1, 2, 3]
*/
除了上述規(guī)則外,generator還有一個(gè)語(yǔ)法yield *,它可以連接另一個(gè)generator函數(shù),類(lèi)似于普通函數(shù)間調(diào)用。用于一個(gè)generator函數(shù)調(diào)用另一個(gè)generator函數(shù),也可用于遞歸。
function *generatorFunc(){
yield 3;
yield 4;
yield 5;
}
function *generatorFunc1(){
yield 1;
yield 2;
yield * generatorFunc();
yield 6;
}
let g = generatorFunc1();
console.log(Array.from(g));
/*
輸出:
[1, 2, 3, 4, 5, 6]
*/
除了獲取數(shù)組外,我們還可以使用generator的yield和next特性,來(lái)做異步操作。
js中的異步操作我們一般使用Promise來(lái)實(shí)現(xiàn)。
請(qǐng)看下列代碼及注釋。
let g = null;
function *generatorFunc(){
//第一個(gè)請(qǐng)求,模擬3s后臺(tái)操作
let request1Data = yield new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("123");
}, 3000);
}).then((d) => {
//令函數(shù)繼續(xù)運(yùn)行,并把promise返回的數(shù)據(jù)通過(guò)next傳給上一個(gè)yield,代碼會(huì)運(yùn)行到下一個(gè)yield
g.next(d);
});
//輸出第一個(gè)請(qǐng)求的結(jié)果
console.log('request1Data = ' + request1Data);
//同上,開(kāi)始第二個(gè)請(qǐng)求
let request2Data = yield new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("456");
}, 3000);
}).then((d) => {
g.next(d);
});
//第二個(gè)請(qǐng)求
console.log('request2Data = ' + request2Data);
}
g = generatorFunc();
g.next();
console.log('completed');
/*
輸出:
completed(馬上輸出)
request1Data = 123(3s后輸出)
request2Data = 456(6s后輸出)
*/
我們換一種寫(xiě)法:
let g = null;
function *request1(){
return yield new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("123");
}, 3000);
}).then((d) => {
g.next(d);
});
}
function *request2(){
return yield new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("456");
}, 3000);
}).then((d) => {
g.next(d);
});
}
function *generatorFunc(){
let request1Data = yield *request1();
console.log('request1Data = ' + request1Data);
let request2Data = yield *request2();
console.log('request2Data = ' + request2Data);
}
g = generatorFunc();
g.next();
console.log('completed');
/*
輸出同上
*/
運(yùn)行結(jié)果是相同的,所以我們可以看到,generator函數(shù)能夠把異步操作寫(xiě)成同步形式,從而避免了回調(diào)地獄的問(wèn)題。
異步變成同步,不知道能夠避免多少因?yàn)榛卣{(diào),作用域產(chǎn)生的問(wèn)題,代碼邏輯也能急劇簡(jiǎn)化。
1.5.2 generator函數(shù)的自動(dòng)運(yùn)行
雖然我們可以通過(guò)generator消除異步代碼,但是使用起來(lái)還是不太方便的。
需要把generator對(duì)象提前聲明保存,然后還要在異步的結(jié)果處寫(xiě)next()。
經(jīng)過(guò)觀察發(fā)現(xiàn),這些方法的出現(xiàn)都是有規(guī)律的,所以可以通過(guò)代碼封裝來(lái)將這些操作封裝起來(lái),從而讓generator函數(shù)的運(yùn)行,就像普通函數(shù)一樣。
提供這樣功能的是co.js(可以點(diǎn)這里跳轉(zhuǎn)),大神寫(xiě)的插件,用于generator函數(shù)的自動(dòng)運(yùn)行,簡(jiǎn)單的說(shuō)它會(huì)幫你自動(dòng)執(zhí)行next()函數(shù),所以借助co.js,你只需要編寫(xiě)yield和異步函數(shù)即可。
使用co.js,上面的異步代碼可以寫(xiě)成這樣:
let co = require('./co');
function *request1(){
return yield new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("123");
}, 3000);
});
}
function *request2(){
return yield new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("456");
}, 3000);
});
}
function *generatorFunc(){
let request1Data = yield *request1();
console.log('request1Data = ' + request1Data);
let request2Data = yield *request2();
console.log('request2Data = ' + request2Data);
}
co(generatorFunc);
console.log('completed');
/*
輸出同上
*/
可以看到,借助co.js你只需要寫(xiě)yield就能夠把異步操作寫(xiě)成同步調(diào)用的形式。
注意,請(qǐng)使用promise來(lái)進(jìn)行異步操作。
1.6 async和await
使用generator + Promise + co.js可以較為方便地實(shí)現(xiàn)異步轉(zhuǎn)同步。
而js的新標(biāo)準(zhǔn)中,上面的操作已經(jīng)提供了語(yǔ)法層面的支持,并將異步轉(zhuǎn)同步的寫(xiě)法,簡(jiǎn)化成了2個(gè)關(guān)鍵字:await和async。
同樣實(shí)現(xiàn)上節(jié)中的異步調(diào)用功能,代碼如下:
async function request1(){
return await new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("123");
}, 3000);
});
}
async function request2(){
return await new Promise((resolve, reject) => {
setTimeout(()=>{
resolve("456");
}, 3000);
});
}
async function generatorFunc(){
let request1Data = await request1();
console.log('request1Data = ' + request1Data);
let request2Data = await request2();
console.log('request2Data = ' + request2Data);
}
generatorFunc();
console.log('completed');
/*
輸出同上
*/
await/async使用規(guī)則如下:
- await只能用在async函數(shù)中。
- await后面可以接任何對(duì)象。
- 如果await后面接的是普通對(duì)象(非Promise,非async),則會(huì)馬上返回,相當(dāng)于沒(méi)寫(xiě)await。
- 如果await后面是Promise對(duì)象,await會(huì)等待Promise的resolve執(zhí)行后,才會(huì)繼續(xù)向下執(zhí)行,然后await會(huì)返回resolve傳遞的參數(shù)。
- 如果await后面是另一個(gè)async函數(shù),則會(huì)等待另一個(gè)async完成后繼續(xù)執(zhí)行。
- 調(diào)用一個(gè)async函數(shù)會(huì)返回一個(gè)Promise對(duì)象,async函數(shù)中的返回值相當(dāng)于調(diào)用了Promise的resolve方法,async函數(shù)中拋出異常相當(dāng)于調(diào)用了Promise的reject方法。
- 通過(guò)上一條規(guī)則可知,雖然await/async使用了Promise來(lái)執(zhí)行異步,但是我們卻可以在使用這兩個(gè)個(gè)關(guān)鍵字的時(shí)候,不寫(xiě)任何的Promise。
- 另外,如果await后面的表達(dá)式可能拋出異常,則需要在await語(yǔ)句上增加
try-catch語(yǔ)句,否則異常會(huì)導(dǎo)致程序執(zhí)行中斷。
await/async本身就是用來(lái)做異步操作轉(zhuǎn)同步寫(xiě)法的,它的規(guī)則和用法也很明確,只要牢記上面幾點(diǎn),你就能用好它們。
//拋出異常的async方法
async function generatorFunc1(){
console.log("begin generatorFunc1");
throw 1001;
}
//async方法返回的是Promise對(duì)象,使用Promise.catch捕獲異常
generatorFunc1().catch((e) => {
console.log(`catch error '${e}' in Promise.catch`);
})
//正常帶返回值的async方法
async function generatorFunc2(){
console.log("begin generatorFunc2");
return 1002;
}
//async方法返回的是Promise對(duì)象,使用Promise.then獲取返回的數(shù)據(jù)
generatorFunc2().then((data)=>{
console.log(`data = ${data}`);
})
//await后帶的async方法若拋出異常,可以在await語(yǔ)句增加try-catch捕獲異常
async function generatorFunc3(){
console.log("begin generatorFunc3");
try{
await generatorFunc1();
}catch(e){
console.log(`catch error '${e}' in generatorFunc3`);
}
}
generatorFunc3();
console.log('completed');
/* 輸出:
begin generatorFunc1
begin generatorFunc2
begin generatorFunc3
begin generatorFunc1
completed
catch error '1001' in Promise.catch
data = 1002
catch error '1001' in generatorFunc3
*/
--完--