1. 程式人生 > >溫習JS及相關常用的ES6新語法(二)

溫習JS及相關常用的ES6新語法(二)

generator

generator(生成器)是ES6標準引入的新的資料型別。一個generator看上去像一個函式,但可以返回多次。
函式在執行過程中,如果沒有遇到return語句(函式末尾如果沒有return,就是隱含的return undefined;),控制權無法交回被呼叫的程式碼。
generator跟函式很像,定義如下:

function* foo(x) {
    yield x + 1;
    yield x + 2;
    return x + 3;
}

generator和函式不同的是,generator由function*定義(注意多出的*號),並且,除了return語句,還可以用yield返回多次。


generator就是能夠返回多次的“函式”?返回多次有啥用?
還是舉個栗子吧。

我們以一個著名的斐波那契數列為例,它由0,1開頭(這個數列從第3項開始,每一項都等於前兩項之和):
0 1 1 2 3 5 8 13 21 34 ...
要編寫一個產生斐波那契數列的函式,可以這麼寫:

function fib(max) {
    var
        t,
        a = 0,
        b = 1,
        arr = [0, 1];
    while (arr.length < max) {
        [a, b] = [b, a + b];  //解構賦值
arr.push(b); } return arr; } // 測試: fib(5); // [0, 1, 1, 2, 3] fib(10); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

函式只能返回一次,所以必須返回一個Array。但是,如果換成generator,就可以一次返回一個數,不斷返回多次。用generator改寫如下:

function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield
a; [a, b] = [b, a + b]; n ++; } return; }

直接呼叫試試:
fib(5); // fib {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
直接呼叫一個generator和呼叫函式不一樣,fib(5)僅僅是建立了一個generator物件,還沒有去執行它。

呼叫generator物件有兩個方法,一是不斷地呼叫generator物件的next()方法:

var f = fib(5);
f.next(); // {value: 0, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}

next()方法會執行generator的程式碼,然後,每次遇到yield x;就返回一個物件{value: x, done: true/false},然後“暫停”。返回的value就是yield的返回值,done表示這個generator是否已經執行結束了。如果done為true,則value就是return的返回值。

當執行到done為true時,這個generator物件就已經全部執行完畢,不要再繼續呼叫next()了。

第二個方法是直接用for … of迴圈迭代generator物件,這種方式不需要我們自己判斷done:

'use strict'

function* fib(max) {
    var
        t,
        a = 0,
        b = 1,
        n = 0;
    while (n < max) {
        yield a;
        [a, b] = [b, a + b];
        n ++;
    }
    return;
}
for (var x of fib(10)) {
    console.log(x); // 依次輸出0, 1, 1, 2, 3, ...
}

generator和普通函式相比,有什麼用?

因為generator可以在執行過程中多次返回,所以它看上去就像一個可以記住執行狀態的函式,利用這一點,寫一個generator就可以實現需要用面向物件才能實現的功能。例如,用一個物件來儲存狀態,得這麼寫:

var fib = {
    a: 0,
    b: 1,
    n: 0,
    max: 5,
    next: function () {
        var
            r = this.a,
            t = this.a + this.b;
        this.a = this.b;
        this.b = t;
        if (this.n < this.max) {
            this.n ++;
            return r;
        } else {
            return undefined;
        }
    }
};

用物件的屬性來儲存狀態,相當繁瑣。

generator還有另一個巨大的好處,就是把非同步回撥程式碼變成“同步”程式碼。這個好處要等到後面學了AJAX以後才能體會到。

沒有generator之前的黑暗時代,用AJAX時需要這麼寫程式碼:

ajax('http://url-1', data1, function (err, result) {
    if (err) {
        return handle(err);
    }
    ajax('http://url-2', data2, function (err, result) {
        if (err) {
            return handle(err);
        }
        ajax('http://url-3', data3, function (err, result) {
            if (err) {
                return handle(err);
            }
            return success(result);
        });
    });
});

回撥越多,程式碼越難看。

有了generator的美好時代,用AJAX時可以這麼寫:

try {
    r1 = yield ajax('http://url-1', data1);
    r2 = yield ajax('http://url-2', data2);
    r3 = yield ajax('http://url-3', data3);
    success(r3);
}
catch (err) {
    handle(err);
}

看上去是同步的程式碼,實際執行是非同步的。
例子:
要生成一個自增的ID,可以編寫一個next_id()函式:

var current_id = 0;

function next_id() {
    current_id ++;
    return current_id;
}

由於函式無法儲存狀態,故需要一個全域性變數current_id來儲存數字
閉包的方式:

function xxa() {
    var a=0;
     return function(){
        a++;
        console.log(a);
     };
}
var fc=xxa();
fc();  //1
fc();  //2
fc();  //3
fc();  //4

不用閉包,試用generator改寫:

function* next_id() {
   var a = 1;
   while(1) yield a++;
}

標準物件

在JavaScript的世界裡,一切都是物件。

但是某些物件還是和其他物件不太一樣。為了區分物件的型別,我們用typeof操作符獲取物件的型別,它總是返回一個字串:

typeof 123; // 'number'
typeof NaN; // 'number'
typeof 'str'; // 'string'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Math.abs; // 'function'
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'

可見,number、string、boolean、function和undefined有別於其他型別。特別注意null的型別是object,Array的型別也是object,如果我們用typeof將無法區分出null、Array和通常意義上的object——{}。

包裝物件

除了這些型別外,JavaScript還提供了包裝物件,熟悉Java的小夥伴肯定很清楚int和Integer這種曖昧關係。

number、boolean和string都有包裝物件。沒錯,在JavaScript中,字串也區分string型別和它的包裝型別。包裝物件用new建立
雖然包裝物件看上去和原來的值一模一樣,顯示出來也是一模一樣,但他們的型別已經變為object了!所以,包裝物件和原始值用===比較會返回false:

typeof new Number(123); // 'object'
new Number(123) === 123; // false

typeof new Boolean(true); // 'object'
new Boolean(true) === true; // false

typeof new String('str'); // 'object'
new String('str') === 'str'; // false

所以閒的蛋疼也不要使用包裝物件!尤其是針對string型別!!!
如果我們在使用Number、Boolean和String時,沒有寫new會發生什麼情況?

此時,Number()、Boolean和String()被當做普通函式,把任何型別的資料轉換為number、boolean和string型別(注意不是其包裝型別)

var n = Number('123'); // 123,相當於parseInt()或parseFloat()
typeof n; // 'number'

var b = Boolean('true'); // true
typeof b; // 'boolean'

var b2 = Boolean('false'); // true! 'false'字串轉換結果為true!因為它是非空字串!
var b3 = Boolean(''); // false

var s = String(123.45); // '123.45'
typeof s; // 'string'

總結一下,有這麼幾條規則需要遵守:

  • 不要使用new Number()、new Boolean()、new String()建立包裝物件;
  • 用parseInt()或parseFloat()來轉換任意型別到number;
  • 用String()來轉換任意型別到string,或者直接呼叫某個物件的toString()方法;
  • 通常不必把任意型別轉換為boolean再判斷,因為可以直接寫if (myVar) {…};
  • typeof操作符可以判斷出number、boolean、string、function和undefined;
  • 判斷Array要使用Array.isArray(arr);
  • 判斷null請使用myVar === null;
  • 判斷某個全域性變數是否存在用typeof window.myVar === ‘undefined’;
  • 函式內部判斷某個變數是否存在用typeof myVar === ‘undefined’。

Date

在JavaScript中,Date物件用來表示日期和時間。

要獲取系統當前時間,用:

var now = new Date();
now; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
now.getFullYear(); // 2015, 年份
now.getMonth(); // 5, 月份,注意月份範圍是0~11,5表示六月
now.getDate(); // 24, 表示24號
now.getDay(); // 3, 表示星期三
now.getHours(); // 19, 24小時制
now.getMinutes(); // 49, 分鐘
now.getSeconds(); // 22, 秒
now.getMilliseconds(); // 875, 毫秒數
now.getTime(); // 1435146562875, 以number形式表示的時間戳

時區

Date物件表示的時間總是按瀏覽器所在時區顯示的,不過我們既可以顯示本地時間,也可以顯示調整後的UTC時間:

var d = new Date(1435146562875);
d.toLocaleString(); // '2015/6/24 下午7:49:22',本地時間(北京時區+8:00),顯示的字串與作業系統設定的格式有關
d.toUTCString(); // 'Wed, 24 Jun 2015 11:49:22 GMT',UTC時間,與本地時間相差8小時

那麼在JavaScript中如何進行時區轉換呢?實際上,只要我們傳遞的是一個number型別的時間戳,我們就不用關心時區轉換。任何瀏覽器都可以把一個時間戳正確轉換為本地時間。

時間戳是個什麼東西?時間戳是一個自增的整數,它表示從1970年1月1日零時整的GMT時區開始的那一刻,到現在的毫秒數。假設瀏覽器所在電腦的時間是準確的,那麼世界上無論哪個時區的電腦,它們此刻產生的時間戳數字都是一樣的,所以,時間戳可以精確地表示一個時刻,並且與時區無關。

面向物件程式設計

JavaScript的面向物件程式設計和大多數其他語言如Java、C#的面向物件程式設計都不太一樣。如果你熟悉Java或C#,很好,你一定明白麵向物件的兩個基本概念:

類:類是物件的型別模板,例如,定義Student類來表示學生,類本身是一種型別,Student表示學生型別,但不表示任何具體的某個學生;

例項:例項是根據類建立的物件,例如,根據Student類可以創建出xiaoming、xiaohong、xiaojun等多個例項,每個例項表示一個具體的學生,他們全都屬於Student型別。

所以,類和例項是大多數面向物件程式語言的基本概念。

不過,在JavaScript中,這個概念需要改一改。JavaScript不區分類和例項的概念,而是通過原型(prototype)來實現面向物件程式設計。

原型是指當我們想要建立xiaoming這個具體的學生時,我們並沒有一個Student型別可用。那怎麼辦?恰好有這麼一個現成的物件:

var robot = {
    name: 'Robot',
    height: 1.6,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

我們看這個robot物件有名字,有身高,還會跑,有點像小明,乾脆就根據它來“建立”小明得了!

於是我們把它改名為Student,然後創建出xiaoming:

var Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

var xiaoming = {
    name: '小明'
};

xiaoming.__proto__ = Student;

注意最後一行程式碼把xiaoming的原型指向了物件Student,看上去xiaoming彷彿是從Student繼承下來的:

xiaoming.name; // '小明'
xiaoming.run(); // 小明 is running...

xiaoming有自己的name屬性,但並沒有定義run()方法。不過,由於小明是從Student繼承而來,只要Student有run()方法,xiaoming也可以呼叫:

JavaScript的原型鏈和Java的Class區別就在,它沒有“Class”的概念,所有物件都是例項,所謂繼承關係不過是把一個物件的原型指向另一個物件而已。

如果你把xiaoming的原型指向其他物件:

var Bird = {
    fly: function () {
        console.log(this.name + ' is flying...');
    }
};

xiaoming.__proto__ = Bird;

現在xiaoming已經無法run()了,他已經變成了一隻鳥:
xiaoming.fly(); // 小明 is flying...
在JavaScrip程式碼執行時期,你可以把xiaoming從Student變成Bird,或者變成任何物件。

請注意,上述程式碼僅用於演示目的。在編寫JavaScript程式碼時,不要直接用obj.__proto__去改變一個物件的原型,並且,低版本的IE也無法使用__proto__Object.create()方法可以傳入一個原型物件,並建立一個基於該原型的新物件,但是新物件什麼屬性都沒有,因此,我們可以編寫一個函式來建立xiaoming:

// 原型物件:
var Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

function createStudent(name) {
    // 基於Student原型建立一個新物件:
    var s = Object.create(Student);
    // 初始化新物件:
    s.name = name;
    return s;
}

var xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true

這是建立原型繼承的一種方法,JavaScript還有其他方法來建立物件

建立物件

JavaScript對每個建立的物件都會設定一個原型,指向它的原型物件。

當我們用obj.xxx訪問一個物件的屬性時,JavaScript引擎先在當前物件上查詢該屬性,如果沒有找到,就到其原型物件上找,如果還沒有找到,就一直上溯到Object.prototype物件,最後,如果還沒有找到,就只能返回undefined。
例如,建立一個Array物件:
var arr = [1, 2, 3];
其原型鏈是:
arr ----> Array.prototype ----> Object.prototype ----> null
函式也是一個物件,它的原型鏈是:
foo ----> Function.prototype ----> Object.prototype ----> null

很容易想到,如果原型鏈很長,那麼訪問一個物件的屬性就會因為花更多的時間查詢而變得更慢,因此要注意不要把原型鏈搞得太長。

建構函式

除了直接用{ … }建立一個物件外,JavaScript**還可以用一種建構函式的方法來建立物件**。它的用法是,先定義一個建構函式:

function Student(name) {
    this.name = name;
    this.hello = function () {
        alert('Hello, ' + this.name + '!');
    }
}

這是一個普通函式,但是在JavaScript中,可以用關鍵字new來呼叫這個函式,並返回一個物件

var xiaoming = new Student('小明');
xiaoming.name; // '小明'
xiaoming.hello(); // Hello, 小明!

注意,如果不寫new,這就是一個普通函式,它返回undefined。但是,如果寫了new,它就變成了一個建構函式,它繫結的this指向新建立的物件,並預設返回this,也就是說,不需要在最後寫return this;。

新建立的xiaoming的原型鏈是:
xiaoming ----> Student.prototype ----> Object.prototype ----> null
用new Student()建立的物件還從原型上獲得了一個constructor屬性,它指向函式Student本身:

xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true

Object.getPrototypeOf(xiaoming) === Student.prototype; // true

xiaoming instanceof Student; // true

用一張圖來表示這些亂七八糟的關係就是:

紅色箭頭是原型鏈。注意,Student.prototype指向的物件就是xiaoming、xiaohong的原型物件,這個原型物件自己還有個屬性constructor,指向Student函式本身。但是xiaoming、xiaohong這些物件可沒有prototype這個屬性,不過可以用__proto__這個非標準用法來檢視。
現在我們就認為xiaoming、xiaohong這些物件“繼承”自Student

不過還有一個小問題:
xiaoming和xiaohong各自的name不同,這是對的,否則我們無法區分誰是誰了。

xiaoming和xiaohong各自的hello是一個函式,但它們是兩個不同的函式,雖然函式名稱和程式碼都是相同的!

如果我們通過new Student()建立了很多物件,這些物件的hello函式實際上只需要共享同一個函式就可以了,這樣可以節省很多記憶體。

要讓建立的物件共享一個hello函式,根據物件的屬性查詢原則,我們只要把hello函式移動到xiaoming、xiaohong這些物件共同的原型上就可以了,也就是Student.prototype:
修改程式碼如下:

function Student(name) {
    this.name = name;
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
};

用new建立基於原型的JavaScript的物件就是這麼簡單!
為了區分普通函式和建構函式,按照約定,建構函式首字母應當大寫,而普通函式首字母應當小寫,這樣,一些語法檢查工具如jslint將可以幫你檢測到漏寫的new。
最後,我們還可以編寫一個createStudent()函式,在內部封裝所有的new操作。一個常用的程式設計模式像這樣:

function Student(props) {
    this.name = props.name || '匿名'; // 預設值為'匿名'
    this.grade = props.grade || 1; // 預設值為1
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
};

function createStudent(props) {
    return new Student(props || {})
}

這個createStudent()函式有幾個巨大的優點:一是不需要new來呼叫,二是引數非常靈活,可以不傳,也可以這麼傳:

var xiaoming = createStudent({
    name: '小明'
});

xiaoming.grade; // 1

如果建立的物件有很多屬性,我們只需要傳遞需要的某些屬性,剩下的屬性可以用預設值。由於引數是一個Object,我們無需記憶引數的順序。如果恰好從JSON拿到了一個物件,就可以直接創建出xiaoming。

原型繼承

在傳統的基於Class的語言如Java、C++中,繼承的本質是擴充套件一個已有的Class,並生成新的Subclass。

由於這類語言嚴格區分類和例項,繼承實際上是型別的擴充套件。但是,JavaScript由於採用原型繼承,我們無法直接擴充套件一個Class,因為根本不存在Class這種型別。

但是辦法還是有的
現在,我們要基於之前的Student擴展出PrimaryStudent,可以先定義出PrimaryStudent:

function PrimaryStudent(props) {
    // 呼叫Student建構函式,繫結this變數:
    Student.call(this, props);

    this.grade = props.grade || 1;
}

但是,呼叫了Student建構函式不等於繼承了Student,PrimaryStudent建立的物件的原型是:
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Object.prototype ----> null
必須想辦法把原型鏈修改為:
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Student.prototype ----> Object.prototype ----> null
這樣,原型鏈對了,繼承關係就對了。新的基於PrimaryStudent建立的物件不但能呼叫PrimaryStudent.prototype定義的方法,也可以呼叫Student.prototype定義的方法。
但是:
PrimaryStudent.prototype = Student.prototype;
是不行的!如果這樣的話,PrimaryStudent和Student共享一個原型物件,那還要定義PrimaryStudent幹啥?

我們必須藉助一箇中間物件來實現正確的原型鏈,這個中間物件的原型要指向Student.prototype。為了實現這一點,參考道爺(就是發明JSON的那個道格拉斯)的程式碼,中間物件可以用一個空函式F來實現:

// PrimaryStudent建構函式:
function PrimaryStudent(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 空函式F:
function F() {
}

// 把F的原型指向Student.prototype:
F.prototype = Student.prototype;

// 把PrimaryStudent的原型指向一個新的F物件,F物件的原型正好指向Student.prototype:
PrimaryStudent.prototype = new F();

// 把PrimaryStudent原型的建構函式修復為PrimaryStudent:
PrimaryStudent.prototype.constructor = PrimaryStudent;

// 繼續在PrimaryStudent原型(就是new F()物件)上定義方法:
PrimaryStudent.prototype.getGrade = function () {
    return this.grade;
};

// 建立xiaoming:
var xiaoming = new PrimaryStudent({
    name: '小明',
    grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2

// 驗證原型:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true

// 驗證繼承關係:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true

用一張圖來表示新的原型鏈:

注意,函式F僅用於橋接,我們僅建立了一個new F()例項,而且,沒有改變原有的Student定義的原型鏈。

如果把繼承這個動作用一個inherits()函式封裝起來,還可以隱藏F的定義,並簡化程式碼:

function inherits(Child, Parent) {
    var F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

這個inherits()函式可以複用:

function Student(props) {
    this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

function PrimaryStudent(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 實現原型繼承鏈:
inherits(PrimaryStudent, Student);

// 繫結其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function () {
    return this.grade;
};

JavaScript的原型繼承實現方式就是:

  1. 定義新的建構函式,並在內部用call()呼叫希望“繼承”的建構函式,並繫結this;
  2. 藉助中間函式F實現原型鏈繼承,最好通過封裝的inherits函式完成;
  3. 繼續在新的建構函式的原型上定義新方法。

class繼承

在上面我們看到了JavaScript的物件模型是基於原型實現的,特點是簡單,缺點是理解起來比傳統的類-例項模型要困難,最大的缺點是繼承的實現需要編寫大量程式碼,並且需要正確實現原型鏈。

有沒有更簡單的寫法?有!

新的關鍵字class從ES6開始正式被引入到JavaScript中。class的目的就是讓定義類更簡單。

我們先回顧用函式實現Student的方法:

function Student(name) {
    this.name = name;
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

如果用新的class關鍵字來編寫Student,可以這樣寫:

class Student {
    constructor(name) {
        this.name = name;
    }

    hello() {
        alert('Hello, ' + this.name + '!');
    }
}

比較一下就可以發現,class的定義包含了建構函式constructor和定義在原型物件上的函式hello()(注意沒有function關鍵字),這樣就避免了Student.prototype.hello = function () {…}這樣分散的程式碼。
最後,建立一個Student物件程式碼和前面完全一樣:

var xiaoming = new Student('小明');
xiaoming.hello();

用class定義物件的另一個巨大的好處是繼承更方便了。想一想我們從Student派生一個PrimaryStudent需要編寫的程式碼量。現在,原型繼承的中間物件,原型物件的建構函式等等都不需要考慮了,直接通過extends來實現:

class PrimaryStudent extends Student {
    constructor(name, grade) {
        super(name); // 記得用super呼叫父類的構造方法!
        this.grade = grade;
    }

    myGrade() {
        alert('I am at grade ' + this.grade);
    }
}

注意PrimaryStudent的定義也是class關鍵字實現的,而extends則表示原型鏈物件來自Student。子類的建構函式可能會與父類不太相同,例如,PrimaryStudent需要name和grade兩個引數,並且需要通過super(name)來呼叫父類的建構函式,否則父類的name屬性無法正常初始化。

PrimaryStudent已經自動獲得了父類Student的hello方法,我們又在子類中定義了新的myGrade方法。

ES6引入的class和原有的JavaScript原型繼承有什麼區別呢?實際上它們沒有任何區別,class的作用就是讓JavaScript引擎去實現原來需要我們自己編寫的原型鏈程式碼。簡而言之,用class的好處就是極大地簡化了原型鏈程式碼。

你一定會問,class這麼好用,能不能現在就用上?

現在用還早了點,因為不是所有的主流瀏覽器都支援ES6的class。如果一定要現在就用上,就需要一個工具把class程式碼轉換為傳統的prototype程式碼,可以試試Babel這個工具。

promise

在JavaScript的世界中,所有程式碼都是單執行緒執行的。

由於這個“缺陷”,導致JavaScript的所有網路操作,瀏覽器事件,都必須是非同步執行。非同步執行可以用回撥函式實現:

function callback() {
    console.log('Done');
}
console.log('before setTimeout()');
setTimeout(callback, 1000); // 1秒鐘後呼叫callback函式
console.log('after setTimeout()');

觀察上述程式碼執行,在Chrome的控制檯輸出可以看到:

before setTimeout()
after setTimeout()
(等待1秒後)
Done

可見,非同步操作會在將來的某個時間點觸發一個函式呼叫。

AJAX就是典型的非同步操作。比如:
把回撥函式success(request.responseText)和fail(request.status)寫到一個AJAX操作裡很正常,但是不好看,而且不利於程式碼複用。

有沒有更好的寫法?比如寫成這樣:

var ajax = ajaxGet('http://...');
ajax.ifSuccess(success)
    .ifFail(fail);

這種鏈式寫法的好處在於,先統一執行AJAX邏輯,不關心如何處理結果,然後,根據結果是成功還是失敗,在將來的某個時候呼叫success函式或fail函式。
這種“承諾將來會執行”的物件在JavaScript中稱為Promise物件。

Promise有各種開源實現,在ES6中被統一規範,由瀏覽器直接支援。
我們先看一個最簡單的Promise例子:生成一個0-2之間的隨機數,如果小於1,則等待一段時間後返回成功,否則返回失敗:

function test(resolve, reject) {
    var timeOut = Math.random() * 2;
    log('set timeout to: ' + timeOut + ' seconds.');
    setTimeout(function () {
        if (timeOut < 1) {
            log('call resolve()...');
            resolve('200 OK');
        }
        else {
            log('call reject()...');
            reject('timeout in ' + timeOut + ' seconds.');
        }
    }, timeOut * 1000);
}

這個test()函式有兩個引數,這兩個引數都是函式,如果執行成功,我們將呼叫resolve(‘200 OK’),如果執行失敗,我們將呼叫reject(‘timeout in ’ + timeOut + ’ seconds.’)。可以看出,test()函式只關心自身的邏輯,並不關心具體的resolve和reject將如何處理結果。

有了執行函式,我們就可以用一個Promise物件來執行它,並在將來某個時刻獲得成功或失敗的結果:

var p1 = new Promise(test);
var p2 = p1.then(function (result) {
    console.log('成功:' + result);
});
var p3 = p2.catch(function (reason) {
    console.log('失敗:' + reason);
});

變數p1是一個Promise物件,它負責執行test函式。由於test函式在內部是非同步執行的,當test函式執行成功時,我們告訴Promise物件:

// 如果成功,執行這個函式:
p1.then(function (result) {
    console.log('成功:' + result);
});

當test函式執行失敗時,我們告訴Promise物件:

p2.catch(function (reason) {
    console.log('失敗:' + reason);
});

Promise物件可以串聯起來,所以上述程式碼可以簡化為:

new Promise(test).then(function (result) {
    console.log('成功:' + result);
}).catch(function (reason) {
    console.log('失敗:' + reason);
});

實際測試一下,看看Promise是如何非同步執行的:

new Promise(function (resolve, reject) {
    log('start new Promise...');
    var timeOut = Math.random() * 2;
    log('set timeout to: ' + timeOut + ' seconds.');
    setTimeout(function () {
        if (timeOut < 1) {
            log('call resolve()...');
            resolve('200 OK');
        }
        else {
            log('call reject()...');
            reject('timeout in ' + timeOut + ' seconds.');
        }
    }, timeOut * 1000);
}).then(function (r) {
    log('Done: ' + r);
}).catch(function (reason) {
    log('Failed: ' + reason);
});

可見Promise最大的好處是在非同步執行的流程中,把執行程式碼和處理結果的程式碼清晰地分離了:

Promise還可以做更多的事情,比如,有若干個非同步任務,需要先做任務1,如果成功後再做任務2,任何任務失敗則不再繼續並執行錯誤處理函式。

要序列執行這樣的非同步任務,不用Promise需要寫一層一層的巢狀程式碼。有了Promise,我們只需要簡單地寫:

job1.then(job2).then(job3).catch(handleError);

其中,job1、job2和job3都是Promise物件。

相關推薦

溫習JS相關常用ES6語法()

generator generator(生成器)是ES6標準引入的新的資料型別。一個generator看上去像一個函式,但可以返回多次。 函式在執行過程中,如果沒有遇到return語句(函式末尾如果沒有return,就是隱含的return undefined

溫習JS相關常用ES6語法(一)

Map和Set JavaScript的預設物件表示方式{}可以視為其他語言中的Map或Dictionary的資料結構,即一組鍵值對。 但是JavaScript的物件有個小問題,就是鍵必須是字串。但實際上Number或者其他資料型別作為鍵也是非常合理的。 為

ES6常用語法學習筆記

簡介 ES6是 JavaScript 語言的下一代標準,已經在 2015 年 6 月正式釋出了。它的目標,是使得 JavaScript 語言可以用來編寫複雜的大型應用程式,成為企業級開發語言。 1.let和const let let和var 差不多,都是用來宣告變數,但是let的宣告

總結常見的ES6語法特性。

等號 模塊 不能 文件 import 處理程序 定義 ogl 進行 前言 ES6是即將到來的新版本JavaScript語言的標準,他給我們帶來了更“甜”的語法糖(一種語法,使得語言更容易理解和更具有可讀性,也讓我們編寫代碼更加簡單快捷),如箭頭函數(=>)、class

ES6語法之---對象字面量擴展、模板字符串(5)

ons 可靠的 小數 為我 寫法 define 當前 BE dde 這節課學習ES6中對象字面量擴展和新增模板字符串 第一部分:對象字面量擴展 1.簡潔寫法   ES6對於對象字面量屬性提供了簡寫方式。   1.1:屬性簡寫 //傳統寫法

ES6語法概覽

Freelance last num cep sse 恢復 top book get 簡介 ES6是JavaScript語言的新一代標準,加入了一些新的功能和語法,正式發布於2015年6月,亦稱ES2015;該標準由ECMA(歐洲計算機制造聯合會)的第39號技術專家委員會(

javascript es6語法

es6 一些新的語法與用法 1: let新的方式定義變數 相比於之前的var宣告變數 有兩個好處 1.1 : 之前用var 定義變數 只有函式才會起到作用域的左右 在{ } 物件這樣的作用域裡面不起左右 而用let則會在{ } 裡面也會開啟一個作用域 1.2 : 沒有變數的提升 2:

javascript es6語法

es6 一些新的語法與用法 1: let新的方式定義變數 相比於之前的var宣告變數 有兩個好處 1.1 : 之前用var 定義變數 只有函式才會起到作用域的左右 在{ } 物件這樣的作用域裡面不起左右 而用let則會在{ } 裡面也會開啟一個作用域 1.2

Es6語法

目錄 1.變數宣告     1.1 let 宣告的變數是區域性變數,只在範圍內有效     1.2 const 修飾的變數不能自增 2. 解構表示式     2.1 陣列     &nb

Java中String,StringBuffer,StringBuilder的區別相關常用函式

今天在刷演算法題的時候,題目上傳入的引數型別是StringBuffer,而需要返回的卻是String型別。其中我在寫的時候用了兩種不同的寫法產生的結果相同,但是速度卻不同。所以在網上查找了一些資料。下面做一個String,StringBuffer,StringBuilder的區別及常用函式介

ES6語法】--import()

import()方法是用來代替require,實現動態載入;例項: 要使用import()+angular路由實現動態載入, 構建工具: webpack    1、要使用import() 需要使用babel進行轉換,依賴babel-loaderbabel-corebabel-

CURL模擬表單post提交相關常用引數的使用(包括提交表單同時上傳檔案)

<form action="doLogin" method="post"> <input type="text" name="username" value="admin"/> <input type="password" nam

03-es6 語法 Promise 的了解和基本使用

res spa The div json $.ajax con err 參數 // Promise // 1.說明 : Promise 是 es6 中提出的新語法 // 2.作用 : 用 ‘用戶編寫同步代碼‘ 的方式 ‘處理異步‘ 的一種解決方案 // 3.

從零開始學 Web 之 ES6(四)ES6基礎語法

一、Promise Promise是一個物件,代表了未來某個將要發生的事件(,這個事件通常是一個非同步操作) 有了Promise物件, 可以將非同步操作以同步的流程表達出來, 避免了層層巢狀的回撥函式(俗稱'回撥地獄')。 ES6的Promise是一個建構函式, 用來生成promise例項。 1、prom

python序列之元組的概念相關函式的總結(

接著上篇沒寫完的,上篇沒看的建議從上篇開始看,是按順序寫的. 這裡引入一道題,方便大家理解;Write a Python program to convert a tuple to a string.(用python將元組轉化成字串) 程式碼: tup = ('e',

熟悉JS中的常用選擇器屬性、方法的調用

model bower 文本 font [1] 選擇 val yellow width 選擇器、屬性及方法調用的配合使用: <style> #a{ width: 200px; he

js常用頁面的操作

自動刷新 meta his http navi cati 自動 href 一次 一、手動刷新 1,history.go(0) 2,location.reload() 3,location=location 4,location.assign(location) 5,doc

[js高手之路] es6系列教程 - Map詳解以及常用api

.com size style 系列教程 image clear rsquo images div ECMAScript 6中的Map類型是一種存儲著許多鍵值對的有序列表。鍵值對支持所有的數據類型. 鍵 0 和 ‘0’會被當做兩個不同的鍵,不會發生強

關於ES6 Class語法相關總結

定義類 成對 new source ret str 創建 方法 () 其實關於ES6,網上已經有很多的資料可供查詢,教程可參考阮一峰大神的ES6入門,本文只是對Class這一語法做一個總結: 一、Class基本語法 constructor方法 constructor是類的默

npm-node相關 git常用命令

I. 臨時使用npm源 // 臨時從淘寶的映象源安裝node-sass npm --registry https://registry.npm.taobao.org install node-sass II. 持久使用npm源 // 設定npm的源為淘寶的映象源 npm c