ES6經常使用但被忽略的方法(第五彈Promise和Iterator)

寫在開頭

  • ES6經常使用但被忽略的方法 系列文章,整理做者認爲一些平常開發可能會用到的一些方法、使用技巧和一些應用場景,細節深刻請查看相關內容鏈接,歡迎補充交流。

相關文章

Promise

特色

  1. 對象的狀態不受外界影響。有三種狀態:pending(進行中)、fulfilled(已成功)和rejected(已失敗)。只有異步操做的結果,能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。
  2. 一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。 Promise對象的狀態改變,只有兩種可能:從pending變爲fulfilled和從pending變爲rejected

使用

  • Promise對象是一個構造函數,用來生成Promise實例。構造函數接受一個函數做爲參數,該函數的兩個參數分別是resolverejectresolve函數的做用是,將Promise對象的狀態從「未完成」變爲「成功」(即從 pending 變爲 resolved);reject函數的做用是,將Promise對象的狀態從「未完成」變爲「失敗」(即從 pending 變爲 rejected)。
const promise = new Promise(function(resolve, reject) {
  if (/* 異步操做成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});
複製代碼
  • Promise實例生成之後,能夠用then方法分別指定resolved狀態和rejected狀態的回調函數。方法能夠接受兩個回調函數做爲參數。第一個回調函數是Promise對象的狀態變爲resolved時調用,第二個回調函數是Promise對象的狀態變爲rejected時調用。第二個函數是可選的,不必定要提供。
promise.then(function(value) {
  // success
}, function(error) {
  // failure
});
複製代碼
  • Promise 新建後就會當即執行。 then方法指定的回調函數,將在當前腳本全部同步任務執行完纔會執行。JavaScript事件循環機制

Promise.resolve()

  • 將現有對象轉爲 Promise 對象。
  • 不一樣參數狀況:
    1. 參數是一個 Promise 實例,直接返回這個實例。
    2. 參數是一個thenable對象(thenable對象指的是具備then方法的對象),將這個對象轉爲 Promise 對象,而後就當即執行thenable對象的then方法。
    let thenable = {
      then: function(resolve, reject) {
        resolve(42);
      }
    };
    
    let p1 = Promise.resolve(thenable);
    p1.then(function(value) {
      console.log(value);  // 42
    });
    複製代碼
    1. 參數不是具備then方法的對象,或根本就不是對象,返回一個新的Promise 對象,狀態爲resolved
    const p = Promise.resolve('detanx');
    
    p.then(function (s){
      console.log(s)
    });
    // detanx
    複製代碼
    1. 不帶有任何參數,直接返回一個resolved狀態的 Promise 對象。當即resolve()Promise 對象,是在本輪「事件循環」(event loop)的結束時執行,而不是在下一輪「事件循環」的開始時。
    setTimeout(function () {
      console.log('three');
    }, 0);
    
    Promise.resolve().then(function () {
      console.log('two');
    });
    
    console.log('one');
    
    // one
    // two
    // three
    複製代碼

Promise.reject()

  • Promise.reject(reason)方法也會返回一個新的 Promise 實例,該實例的狀態爲rejectedPromise.reject()方法的參數,會原封不動地做爲reject的理由,變成後續方法的參數。 這一點與Promise.resolve方法不一致。
const thenable = {
  then(resolve, reject) {
    reject('出錯了');
  }
};

Promise.reject(thenable)
.catch(e => {
  console.log(e === thenable)
})
// true
複製代碼

Promise.prototype.catch()

  • Promise.prototype.catch()方法是.then(null, rejection).then(undefined, rejection)的別名(能夠查看手寫Promise的該方法實現),用於指定發生錯誤時的回調函數。
// 寫法一
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// 寫法二
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});
複製代碼
  • 若是 Promise 狀態已經變成resolved,再拋出錯誤是無效的。
const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok
複製代碼
  • Promise 對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。
getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // 處理前面三個Promise產生的錯誤
});
複製代碼
  • 通常不要在then()方法裏面定義 Reject 狀態的回調函數(即then的第二個參數),老是使用catch方法。
// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });
複製代碼
  • 跟傳統的try/catch代碼塊不一樣的是,若是沒有使用catch()方法指定錯誤處理的回調函數,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.prototype.finally()

  • finally()方法用於指定無論 Promise 對象最後狀態如何,都會執行的操做。 該方法是 ES2018 引入標準的。
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {
    console.log('detanx');
});
// 'detanx'
複製代碼
  • finally方法的回調函數不接受任何參數,這意味着沒有辦法知道,前面的 Promise 狀態究竟是fulfilled仍是rejected。方法裏面的操做,應該是與狀態無關的,不依賴於Promise的執行結果。finally方法老是會返回原來的值。
// 實現
Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

// 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.race()和Promise.allSettled()

  • 相同點
    1. 方法都是用於將多個 Promise 實例,包裝成一個新的 Promise 實例。
    2. Promise.all()全部的實例返回都是fulfilledPromise.allSettled()的返回值都是數組。
  • 不一樣點
    1. 有時候,咱們不關心異步操做的結果,只關心這些操做有沒有結束。這時,Promise.allSettled()方法就頗有用。若是沒有這個方法,想要確保全部操做都結束,就很麻煩。Promise.all()方法沒法作到這一點。
    const urls = [ /* ... */ ];
    const requests = urls.map(x => fetch(x));
    
    try {
      await Promise.all(requests);
      console.log('全部請求都成功。');
    } catch {
      console.log('至少一個請求失敗,其餘請求可能還沒結束。');
    }
    複製代碼
    1. Promise.all()全部的實例中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值。Promise.allSettled()中的實例fulfilled時,對象有value屬性,rejected時有reason屬性,對應兩種狀態的返回值。
    const resolved = Promise.resolve(42);
    const rejected = Promise.reject(-1);
    
    const allSettledPromise = Promise.allSettled([resolved, rejected]);
    
    allSettledPromise.then(function (results) {
      console.log(results);
    });
    // [
    //    { status: 'fulfilled', value: 42 },
    //    { status: 'rejected', reason: -1 }
    // ]
    複製代碼
    1. 若是做爲參數的 Promise 實例,本身定義了catch方法,那麼它一旦被rejected,並不會觸發Promise.all()catch方法。若是實例沒有本身的catch方法,就會調用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移除
    .catch(e => e);
    
    Promise.all([p1, p2])
    .then(result => console.log(result))
    .catch(e => console.log(e));
    // ["hello", Error: 報錯了]
    => // 移除p2的catch
    // // Error: 報錯了
    複製代碼
    1. Promise.race()只要有一個實例率先改變狀態,狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給回調函數。
    const p = Promise.race([1,
      new Promise(function (resolve, reject) {
        setTimeout(() => reject(new Error('request timeout')), 5000)
      })
    ]);
    
    p
    .then(console.log)
    .catch(console.error);
    // 1
    複製代碼

Iterator 和 for...of 循環

  • ES6-Iterator 和 for...of 循環
  • 它是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。
  • Iterator 的做用:
    1. 爲各類數據結構,提供一個統一的、簡便的訪問接口;
    2. 使得數據結構的成員可以按某種次序排列;
    3. 新的遍歷命令for...of循環,Iterator 接口主要供for...of消費。
  • 遍歷過程:
    1. 建立一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。
    2. 第一次調用指針對象的next方法,能夠將指針指向數據結構的第一個成員。
    3. 第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。
    4. 不斷調用指針對象的next方法,直到它指向數據結構的結束位置。
    • 每一次調用next方法,都會返回數據結構的當前成員的信息(返回一個包含valuedone兩個屬性的對象。value屬性是當前成員的值,done屬性是一個布爾值,表示遍歷是否結束。)
    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};
        }
      };
    }
    複製代碼
    • 對於遍歷器對象來講,done: falsevalue: undefined屬性都是能夠省略的。
    // makeIterator函數能夠簡寫成下面的形式
    function makeIterator(array) {
      var nextIndex = 0;
      return {
        next: function() {
          return nextIndex < array.length ?
            {value: array[nextIndex++]} :
            {done: true};
        }
      };
    }
    複製代碼
  • 使用 TypeScript 的寫法
    • 遍歷器接口(Iterable)、指針對象(Iterator)和next方法返回值的規格能夠描述以下。
    interface Iterable {
      [Symbol.iterator]() : Iterator,
    }
    
    interface Iterator {
      next(value?: any) : IterationResult,
    }
    
    interface IterationResult {
      value: any,
      done: boolean,
    }
    複製代碼

默認 Iterator 接口

  • 當使用for...of循環遍歷某種數據結構時,該循環會自動去尋找 Iterator 接口。默認的 Iterator 接口部署在數據結構的Symbol.iterator屬性,或者說,一個數據結構只要具備Symbol.iterator屬性,就能夠認爲是「可遍歷的」(iterable)。
const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
};
複製代碼
  • 原生具有Iterator接口的數據結構:ArrayMapSetStringTypedArray、函數的 arguments 對象、NodeList 對象
  • 一個對象若是要具有可被for...of循環調用的 Iterator 接口,就必須在Symbol.iterator的屬性上部署遍歷器生成方法(原型鏈上的對象具備該方法也可)。
class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    }
    return {done: true, value: undefined};
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
  console.log(value); // 0, 1, 2
}
複製代碼
  • 相似數組的對象調用數組的Symbol.iterator能夠遍歷;普通對象部署數組的Symbol.iterator方法,並沒有效果。
// 相似數組對象
let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}
// 普通對象,下標不是數字
let iterable = {
  a: 'a',
  b: 'b',
  c: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // undefined, undefined, undefined
}
複製代碼
  • 若是Symbol.iterator方法對應的不是遍歷器生成函數(即會返回一個遍歷器對象),解釋引擎將會報錯。
var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj] // TypeError: [] is not a function
複製代碼

調用 Iterator 接口的場合

  1. 解構賦值
    • 對數組和 Set 結構進行解構賦值時,會默認調用Symbol.iterator方法。
  2. 擴展運算符
    • 擴展運算符(...)也會調用默認的 Iterator 接口。
  3. yield*
    • yield*後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。
  4. 其餘
    • for...ofArray.from()Map(), Set(), WeakMap(), WeakSet()(好比new Map([['a',1],['b',2]]))、Promise.all()Promise.race()...

遍歷器對象的 return()和throw()

  • 遍歷器對象除了具備next方法,還能夠具備return方法和throw方法。若是你本身寫遍歷器對象生成函數,那麼next方法是必須部署的,return方法和throw方法是否部署是可選的。
  • for...of循環提早退出(一般是由於出錯,或者有break語句),就會調用return方法。若是一個對象在完成遍歷前,須要清理或釋放資源,就能夠部署return方法。
function readLinesSync(file) {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          return { done: false };
        },
        return() {
          file.close();
          return { done: true };
        }
      };
    },
  };
}
// 觸發return狀況一
for (let line of readLinesSync(fileName)) {
  console.log(line);
  break;
}

// 觸發return狀況二
for (let line of readLinesSync(fileName)) {
  console.log(line);
  throw new Error();
}
複製代碼
  • throw方法主要是配合 Generator 函數使用,下一彈會提到。

for...of 循環

  • 做爲遍歷全部數據結構的統一的方法。
  • 和其餘遍歷對比
    1. for...of會正確識別 32UTF-16 字符。有着同for...in同樣的簡潔語法,可是沒有for...in那些缺點。不一樣於forEach方法,它能夠與breakcontinuereturn配合使用。
    for (let x of 'a\uD83D\uDC0A') {
      console.log(x);
    }
    // 'a'
    // '\uD83D\uDC0A'
    複製代碼
    1. forEach沒法中途跳出forEach循環,break命令或return命令都不能奏效。
    2. for...in循環缺點,一是數組的鍵名是數字,可是for...in循環是以字符串做爲鍵名「0」、「1」、「2」等等。二是for...in循環不只遍歷數字鍵名,還會遍歷手動添加的其餘鍵,甚至包括原型鏈上的鍵。最後某些狀況下,for...in循環會以任意順序遍歷鍵名。for...in循環主要是爲遍歷對象而設計的,不適用於遍歷數組。
  • 其餘類型的數據可使用其餘技巧轉爲有Iterator接口的數據結構再使用for...of進行遍歷。
    1. 對象
    • 使用Object.keys方法將對象的鍵名生成一個數組,而後遍歷這個數組。
    for (var key of Object.keys(someObject)) {
      console.log(key + ': ' + someObject[key]);
    }
    複製代碼
    • 使用 Generator 函數將對象從新包裝一下。
    function* entries(obj) {
      for (let key of Object.keys(obj)) {
        yield [key, obj[key]];
      }
    }
    
    for (let [key, value] of entries(obj)) {
      console.log(key, '->', value);
    }
    // a -> 1
    // b -> 2
    // c -> 3
    複製代碼
    1. 相似數組的對象但不具備 Iterator 接口
    • 使用Array.from方法將其轉爲數組。
    let arrayLike = { length: 2, 0: 'a', 1: 'b' };
    // 報錯
    for (let x of arrayLike) {
      console.log(x);
    }
    // 正確
    for (let x of Array.from(arrayLike)) {
      console.log(x);
    }
    複製代碼
相關文章
相關標籤/搜索