javascript函數(shù)全解

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ù)中的變量arridx。

一般說(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è):applycall,二者實(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的yieldnext特性,來(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)鍵字:awaitasync。

同樣實(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
*/

--完--

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 異步編程對(duì)JavaScript語(yǔ)言太重要。Javascript語(yǔ)言的執(zhí)行環(huán)境是“單線(xiàn)程”的,如果沒(méi)有異步編程,根本...
    呼呼哥閱讀 7,399評(píng)論 5 22
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 6,446評(píng)論 9 19
  • 含義 async函數(shù)是Generator函數(shù)的語(yǔ)法糖,它使得異步操作變得更加方便。 寫(xiě)成async函數(shù),就是下面這...
    oWSQo閱讀 2,040評(píng)論 0 2
  • async 函數(shù) 含義 ES2017 標(biāo)準(zhǔn)引入了 async 函數(shù),使得異步操作變得更加方便。 async 函數(shù)是...
    huilegezai閱讀 1,313評(píng)論 0 6
  • 成功概率要比錯(cuò)誤概率小,所以錯(cuò)誤是常態(tài)。同時(shí),錯(cuò)誤會(huì)促使與事件本身的緊密程度。 很多錯(cuò)誤不是偶然發(fā)生的,而是對(duì)事件...
    w小郭閱讀 93評(píng)論 0 0

友情鏈接更多精彩內(nèi)容