深入理解ES6

xszi發表於2018-12-15

本文系《深入理解ES6》讀書筆記

第一章 塊級作用域繫結

var 宣告初始化變數, 宣告可以提升,但初始化不可以提升。

一、塊級宣告:

  1. let
  2. const
    • 預設使用,在某種程度上實現程式碼不可變,減少錯誤發生的機率
    • 如果常量是物件,則物件中的值可以修改
  3. 不能重複宣告,宣告不會提升

二、臨時死區:

JS引擎在掃描程式碼發現變數宣告時,要麼將它們提升至作用域頂部(var宣告),要麼將宣告放到TDZ(臨時死區)中(letconst宣告)。訪問TDZ中的變數會觸發執行時錯誤。只有執行過變數宣告語句後,變數才會從TDZ中移出,然後可以正常訪問。

第一種情況:

if(condition) {
    console.log(typeof value); //引用錯誤!
    let value = "blue"; //TDZ
}
複製程式碼

第二種情況:

console.log(typeof value); //'undefined'-->這裡不報錯,只有變數在TDZ中才會報錯
if(condition) {
    let value = "blue";
}
複製程式碼

三、循壞中塊作用域繫結

  • 立即呼叫(IIFE)

  • letconst之所以可以在運用在for-infor-of迴圈中,是因為每次迭代會建立一個新的繫結(const在for迴圈中會報錯)。

四、全域性塊作用域繫結

var 可能會在無意中覆蓋一個已有的全域性屬性, letconst會在全域性作用域下建立一個新的繫結,但該繫結不會新增為全域性物件的屬性。換句話說,使用letconst不能覆蓋全域性變數,而只能遮蔽它。如果不是為全域性物件建立屬性,使用letconst要安全得多。

注:如果希望在全域性物件下定義變數,仍然可以使用var。這種情況常見於在瀏覽器中跨frame或跨window訪問程式碼

第二章 字串和正規表示式

一、UTF-8碼位

名詞解釋:

  • 碼位: 每一個字元的“全球唯一的識別符號,從0開始的數值”
  • 字元編碼:表示某個字元的數值或碼位即為該字元的字元編碼。
  • 基本多文種平面(BMP,Basic Multilingual Plane)

    在UTF-16中,前2^16個碼位均以16位的編碼單元表示,這個範圍被稱作基本多文種平面

二、codePointAt()、 String.fromCodePoint() 和 normalize()

  • 兩個方法對應於charCodeAt()fromCharCode()
  • normalize(): 規範的統一,適用於比較排序,國際化。

三、正規表示式 u 和 y 修飾符、正規表示式的複製、flag屬性

  • u: 編碼單元 ---> 字元模式

這個方法儘管有效,但是當統計長字串中的碼位數量時,運動效率很低。因此,你也可以使用字串迭代器解決效率低的問題,總體而言,只要有可能就嘗試著減小碼位計算的開銷。

檢測u修飾符支援:

function hasRegExpU() {
    try {
        var pattern = new RegExp('.', 'u');
        return true;
    } catch (ex) {
        return false;
    }
}
複製程式碼
  • y: 第一次匹配不到就終止匹配

當執行操作時, y修飾符會把上次匹配後面一個字元的索引儲存到lastIndexOf中;如果該操作匹配的結果為空,則lastIndexOf會被重置為0。g修飾符的行為類似。

1. 只有呼叫exec()和test()這些正規表示式物件的方法時才會涉及lastIndex屬性;
2. 呼叫字串的方法,例如match(),則不會觸發粘滯行為。
複製程式碼
  • 正規表示式的複製
var re1 = /ab/i;
re2 = new RegExp(re1); //沒有修飾符複製
re3 = new RegExp(re1, "g"); //有修飾符(ES6)
複製程式碼
  • flag屬性 --- 獲取正規表示式的修飾符

es5方法獲取正規表示式的修飾符:

function getFlags(re) {
    var text = re.toString();
    return text.substring(text.lastIndexOf('/' + 1, text.length);
}
複製程式碼

模板字面量

多行字串

基本的字串格式化(字串佔位符)

HTML轉義

  • 標籤模板
function passthru(literals, ...substitutions) {
    //返回一個字串
    let result = "";
    //根據substitutions的數量來確定迴圈的執行次數
    for(let i=0; i<substitutions.length; i++){
        result += literals;
        result += substitutions[i]
        console.log(literals[i]) 
        console.log(substitutions[i])
    }
    
    //合併最後一個literal
    result += literals[literals.length - 1];
    return result;
}

let count = 10;
price = 0.25;
message = passthru`${count} items cost $${(count * price).toFixed(2)}`;

console.log(message)
複製程式碼
  • String.raw
String.raw`assda\\naadasd`

//程式碼模擬(略)
複製程式碼

第三章 函式

一、預設引數值

ES5預設引數值

下面函式存在什麼問題 ???

function makeRequest(url, timeout, callback) {
    
    timeout = timeout || 2000;
    callback = callback || function() {};
    
}
複製程式碼

假如timeout傳入值0,這個值是合法的,但是也會被視為一個假值,並最終將timeout賦值為2000。在這種情況下,更安全的選擇是通過typeof檢查引數型別,如下:

function makeRequest(url, timeout, callback) {
    
    timeout = (typeof timeout !== 'undefined') ? timeout :2000;
    callback = (typeof callback !== 'undefined') ? callback : function() {};
    
}
複製程式碼

ES5預設引數值

function makeRequest(url, timeout = 2000, callback) {
    
    //函式的其餘部分
    
}

//特別注意:此時 null 是一個合法值,所以不會使用 timeout 預設值,即 timeout = null
makeRequest('/foo', null, function(body){
    doSomething(body);
})
複製程式碼

二、預設引數值對arguments的影響**

  • ES5:

非嚴格模式:引數變化,arguments物件隨之改變;

嚴格模式:無論引數如何變化,arguments物件不再隨之改變;

  • ES6

非嚴格模式/嚴格模式:無論引數如何變化,arguments物件不再隨之改變;

注: 在引用引數預設值的時候,只允許引用前面引數的值,即先定義的引數不能訪問後定義的引數。這可以用預設引數的臨時死區來解釋。如下:

function add(first = second, second) {
    return first + second;
}

console.log(add(1, 1)); //2
console.log(add(undefined, 1)) //丟擲錯誤

//解釋原理:
//add(1, 1)
let first = 1;
let second = 1;
//add(undefined, 1)
let first = second;
let second = 1; //處於臨時死區
複製程式碼

三、不定引數的使用限制

  1. 每個函式最多隻能宣告一個不定引數,而且一定要放在所有引數的末尾。
  2. 不定引數不能用於物件字面量setter之中(因為物件字面量setter的引數有且只有一個,而在不定引數的定義中,引數的數量可以無限多

無論是否使用不定引數,arguments物件總是包含所有傳入函式的引數。

四、展開運算子

let value = [25, 50, 75, 100];
//es5
console.log(Math.max.apply(Math, values); //100
//es6
console.log(Math.max(...values)); //100
複製程式碼

五、name 屬性

兩個有關函式名稱的特例:

  1. 通過bind()函式建立的函式,其名稱將帶有“bound”字首;
  2. 通過Function建構函式建立的函式,其名稱將是“anonymous”.
var doSomething = function() {
    //空函式
}

console.log(doSomething.bind().name); //'bound doSomething'
console.log((new Function()).name); //'anonymous(匿名)'
複製程式碼

切記: 函式name屬性的值不一定引用同名變數,它只是協助除錯用的額外資訊,所以不能使用name屬性的值來獲取對於函式的引用。

六、明確函式的多重用途

JS函式有兩個不同的內部方法:[[call]][[Construct]]

  • 當通過new關鍵字呼叫函式是,執行的是 [[Construct]] 函式,它負責建立一個通常被稱為例項的新物件,然後再執行函式體,將this繫結到例項上(具有 [[Construct]] 方法的函式被統稱為建構函式,箭頭函式沒有 [[Construct]] 方法 );
  • 如果不通過 new 關鍵字呼叫函式,則執行 [[call]] 函式,從而直接執行程式碼中的函式體;

七、元屬性(Metaproperty)new.target

為了解決判斷函式是否通過new關鍵字呼叫的問題,new.target橫空出世 (instance of ---> new.target)

在函式外使用new.target是一個語法錯誤。

八、塊級函式

  • ES5嚴格模式下,程式碼塊中宣告函式會報錯;
  • ES6嚴格模式下, 可以在定義該函式的程式碼塊中訪問和呼叫它 (塊級函式提升,let變數不提升);
  • ES6非嚴格模式下,函式不再提升至程式碼塊的頂部,而是提升至外圍函式或全域性作用域的頂部。

九、箭頭函式

箭頭函式與傳統的JS函式不同之處主要有以下幾個方面:

  1. 沒有thissuperargumentsnew.target繫結;
  2. 不能通過new關鍵字呼叫;
  3. 沒有原型;
  4. 不可以改變this的繫結;
  5. 不支援arguments物件
  6. 不支援重複的命名引數

建立一個空函式

let doNothing = () => {};
複製程式碼

返回一個物件字面量

let getTempItem = id => ({ id: id, name: "Temp"});
複製程式碼

建立立即執行的函式

let person = ((name) => {
    
    return {
        getName: function() {
            return name;
        }
    }
    
})("xszi")

console.log(person.getName()); //xszi
複製程式碼

箭頭函式沒有this繫結

let PageHandler = {
    
    id: '123456',
    init: function() {
        document.addEventListener("click", function(event){
            this.doSomething(event.type); //丟擲錯誤
        }, false)
    },
    
    doSomething: function(type) {
        console.log("handling " + type + "for" + this.id)
    }
    
}
複製程式碼

使用bind()方法將函式的this繫結到PageHandler,修正報錯:

let PageHandler = {
    
    id: '123456',
    init: function() {
        document.addEventListener("click", (function(event){
            this.doSomething(event.type); //不報錯
        }).bind(this), false)
    },
    
    doSomething: function(type) {
        console.log("handling " + type + "for" + this.id)
    }
    
}
複製程式碼

使用箭頭函式修正:

let PageHandler = {
    
    id: '123456',
    init: function() {
        document.addEventListener("click", 
            event => this.doSomething(event.type), false);
    },
    
    doSomething: function(type) {
        console.log("handling " + type + "for" + this.id)
    }
    
}
複製程式碼
  • 箭頭函式沒有 prototype屬性,它的設計初衷是 即用即棄, 不能用來定義新的型別。
  • 箭頭函式的中this取決於該函式外部非箭頭函式的this值,不能通過call(), apply()bind()方法來改變this的值。

箭頭函式沒有arguments繫結

始終訪問外圍函式的arguments物件

十、尾呼叫優化

  • ES5中,迴圈呼叫情況下,每一個未完成的棧幀都會儲存在記憶體中,當呼叫棧變的過大時會造成程式問題。
  • ES6中尾呼叫優化,需要滿足以下三個條件:
    • 尾呼叫不訪問當前棧幀的變數(也就是說函式不是一個閉包);
    • 在函式內部,尾呼叫是最後一條語句;
    • 尾呼叫的結果作為函式值返回;

如何利用尾呼叫優化

function factorial(n) {
    if ( n<=1 ) {
        return 1;
    }
}else{
    
    //引擎無法自動優化,必須在返回後執行乘法操作 
    return n * factorial(n-1);
    //隨呼叫棧尺寸的增大,存在棧溢位的風險
}
複製程式碼
function factorial(n, p = 1) {
    if ( n<=1 ) {
        return 1 * p;
    }
}else{
    let result = n * p;
    //引擎可自動優化
    return  factorial(n-1, result);
    //不建立新的棧幀,而是消除並重用當前棧幀
}
複製程式碼

第四章 擴充套件物件的功能性

一、物件的類別

  • 普通物件
  • 特異物件
  • 標準物件(ES6中規範中定義的物件,如Array,Date)
  • 內建物件:

指令碼開始執行時存在於JS執行環境中的物件,所有標準物件都是內建物件。

var person = {
    name: 'xszi',
    sayName: function() {
        console.log(this.name);
    }
}

var person = {
    name: 'xszi',
    sayName() {
        console.log(this.name);
    }
}

//兩者唯一的區別是,簡寫方式可以使用super關鍵字
複製程式碼

二、可計算屬性名(Computed Property Name)

在物件字面量中使用方括號表示的該屬性名稱是可計算的,它的內容將被求值並被最終轉化為一個字串。

如下:

var suffix = ' name';

var person = {
    ['first' + suffix]: 'xszi',
    ['last' + suffix]: 'wang'
};

console.log(person['first name']); //xszi
console.log(person['last name']) // wang
複製程式碼

三、新增方法

ECMAScript 其中一個設計目標是:不再建立新的全域性函式,也不在Object.prototype上建立新的方法。

  • Object.is()

大多數情況下,Object.is()與'==='執行結果相同,唯一區別在於 +0-0 識別為不相等,並且NaNNaN等價。

  • Object.assign()

mixin()方法使用賦值操作符(assignment operator)= 來複制相關屬性,卻不能複製 訪問器屬性 到接受物件中,因此最終新增的方法棄用mixin而改用assign作為方法名。

Object.assign() 方法可以接受任意數量的源物件,並按指定的的順序將屬性賦值到接收物件中。所以如果多個源物件具有同名屬性,則排位靠後的源物件會覆蓋排位靠前的。

訪問器屬性Object.assign() 方法不能將提供者的訪問器屬性賦值到接收物件中。由於 Object.assign()方法執行了賦值操作,因此提供者的訪問器屬性最終會轉變為接受物件中的一個資料屬性。

eg:

 var receiver = {};
     supplier = {
         get name() {
             return 'file.js'
         }
     };
     
Object.assign(receiver, supplier);

var descriptor = Object.getOwnPropertyDescriptor(receiver, 'name');

console.log(descriptor.value); // 'file.js'
console.log(descriptor.get); // undefined
複製程式碼

四、自有屬性列舉順序

自有屬性列舉順序的基本規則是:

  1. 所有數字鍵按升序排序;
  2. 所有字串鍵按照它們被加入物件的順序排序;
  3. 所有symbol按照他們被加入的順序排序。

五、增強物件的原型

  • 改變物件的原型
Object.setPrototypeOf(targetObject, protoObject);
複製程式碼
  • 簡化原型訪問的Super引用

Super 引用相當於指向物件原型的指標,實際上也就是Object.getPrototypeOf(this), **必須要在使用簡寫方法的物件中使用 Super**引用。

Super引用不是動態變化的,它總是指向正確的物件。

六、正式的方法定義

ES6正式將方法定義為一個函式,它會有一個內部的 [[HomeObject]] 屬性來容納這個方法從屬的物件。

Super的所以引用都通過 [[HomeObject]] 屬性來確定後續的執行過程:

  1. [[HomeObject]] 屬性上呼叫Object.getPrototypeOf()方法來檢索原型的引用;
  2. 搜尋原型找到同名函式;
  3. 設定this繫結並且條約相應的方法。

第五章 解構: 使資料訪問更便捷

一、物件解構


let node = {
    type: "Indetifier",
    name: "foo"
}

let {type, name} = node;

console.log(type); // Indetifier
console.log(name); // foo

複製程式碼

不要忘記初始化程式(也就是符號右邊的值)

var {type, name}; //報錯,使用let和const同樣報錯
// 除使用解構外,使用var, let不強制要求提供初始化程式, 但是const一定需要;
複製程式碼

二、解構賦值

let node = {
    type: "Indetifier",
    name: "foo"
}

type = 'Literal', name = 5;

//使用解構語法為多個變數賦值
({type, name} = node);  //需要使用()包裹解構複製語句,{}是一個程式碼塊,不能放在左邊

console.log(type); // Indetifier
console.log(name); // foo
複製程式碼
  • 預設值與上章的 預設引數 類似
  • 為非同名佈局變數賦值
let node = {
   type: "Indetifier",
   name: "foo"
}

let { type: localType, name: localName } = node;

console.log(localType); // Indetifier
console.log(localName); // foo
複製程式碼

type: localType語法的含義是讀取名為type的屬性並將其只儲存在變數localType

  • 巢狀物件解構

三、陣列解構

  • 解構賦值

陣列解構也可用於賦值上下文,但不需要用小括號包裹表示式,這一點與物件解構的的約定不同。

let colors = ['red', 'green', 'blue'], firstColor = 'black', secondColor = 'purple';

[firstColor, secondColor] = colors;

console.log(firstColor); // 'red'
console.log(secondColor); // 'green'
複製程式碼

交換值


let a = 1, b = 2;

[a, b] = [b, a];

console.log(a); //2
console.log(b); //1

複製程式碼
  • 巢狀陣列解構(地址的解構賦值)
let colors = ['red', ['green', 'lightgreen'], 'blue'];

let [firstColor, [secondColor]] = colors;

console.log(firstColor); //red
console.log(secondColor); //green

複製程式碼
  • 不定元素(在被解構的陣列中,不定元素必須為最後一個條目,在後面繼續新增逗號會導致程式丟擲語法錯誤)
  • 混合解構(混合物件和陣列解構,使得我們從JSON配置中提取資訊時,不再需要遍歷整個結構了。)
  • 解構引數
function setCookie(name, value, options) {

    options = options || {};
    
    let secure = options.secure,
        path = options.path,
        domian= options.domain,
        expires = options.expires;
        
    //設定cookie程式碼
}

// 第三個引數對映到options中

setCookie('type', 'js', {
    secure: true,
    expires: 60000
})
複製程式碼

上面函式存在一個問題:僅檢視函式的宣告部分,無法辨識函式的預期引數,必須通過閱讀函式體才可以確定所有引數的情況。可以使用 解構引數 來優化:

function setCookie(name, value, {secure, path, domain, expires}}) {
    //設定cookie程式碼
}

setCookie('type', 'js',{
    secure: true,
    expires: 60000
})
複製程式碼
  1. 必須傳值的解構引數;
  2. 解構引數的預設值。

第六章 SymbolSymbol屬性

Symbol出現之前,人們一直通過屬性名來訪問所有屬性,無論屬性名由什麼元素構成,全部通過一個字串型別的名稱來訪問;私有名稱原來是為了讓開發者們建立非字串名稱而設計的,但是一般的技術無法檢測這些屬性的私有名稱。

通過Symbol可以為屬性新增非字串名稱,但是其隱私性就被打破了。

一、建立、使用SymbolSymbol共享體系

  • 建立、使用
let firstName = Symbol();
let person = {};

person[firstName] = 'xszi';
console.log(person[firstName]); //xszi
複製程式碼

由於Symbol是原始值,因此呼叫new Symbol()會導致程式丟擲錯誤。

Symbol函式接受一個可選引數,其可以讓你新增一段文字描述即將建立的Symbol,這段描述 不可用於屬性訪問。該描述被儲存在內部的 [[Description]] 屬性中,只有當呼叫SymboltoString()方法時才可以讀取這個屬性。

  • Symbol共享體系

有時我們可能希望在不同程式碼中共享同一個Symbol(在很大的程式碼庫中或跨檔案追蹤Symbol非常困難),ES6提供了一個可以全域性訪問的全域性Symbol登錄檔,即使用Symbol.for()方法。

let uid = Symbol.for('uid');
let object = {};

object[uid] = '12345';

console.log(object[uid]); //'12345'
console.log(uid); // 'Symbol(uid)'
複製程式碼

實現原理Symbol.for()方法首先在全域性Symbol登錄檔中搜尋鍵為‘uid’的Symbol是否存在,如果存在,直接返回已有的Symbol;否則,建立一個新的Symbol,並使用這個鍵在Symbol全域性登錄檔中註冊,隨即返回新建立的Symbol

可以使用Symbol.keyFor()方法在Symbol全域性登錄檔中檢索與Symbol有關的鍵。

Symbol全域性登錄檔是一個類似全域性作用域的共享環境,也就是說你不能假設目前環境中存在哪些鍵。當使用第三方元件時,儘量使用Symbol鍵的名稱空間減少命名衝突。如 jQuery.

二、Symbol與型別強制轉換,屬性檢索

  • console.log()會呼叫SymbolString()方法
desc = String(uid);

desc = uid + ''; //報錯,不能轉為字串型別

desc = uid / 2; //報錯,不能轉為數字型別
複製程式碼
  • 屬性檢索
    • Object.keys() 返回可列舉屬性
    • Object.getOwnPropertyNames() 不考慮可列舉性,一律返回
    • Object.getOwnProperty-Symbols() ES6用來檢索物件中的Symbol屬性

所有物件一開始沒有自己獨有的屬性,但是物件可以從原型鏈中繼承Symbol屬性。

三、通過well-know Symbol暴露內部操作

還是不怎麼理解,找到一個使用Symbol的實際場景才能更好理解!

第七章 Set集合與Map集合

Set 和 Map 主要的應用場景在於 資料重組資料儲存

Set 是一種叫做集合的資料結構,Map 是一種叫做字典的資料結構

一、用物件屬性模擬Set和Map集合

//set
var set = Object.create(null);

set.foo = true;

//檢查屬性是否存在
if(set.foo){
    //要執行的程式碼
}
複製程式碼
//map
var map = Object.create(null);

map.foo = "bar";

//獲取已存值
var value = map.foo;

console.log(value);
複製程式碼

一般來說,Set集合常被用於檢查物件中是否存在某個鍵名,而Map集合常被用來獲取已存的資訊。

所有物件的屬性名必須是字串型別,必須確保每個鍵名都是字串型別且在物件中是唯一的

二、Set集合

- 有序
- 不重複

+0和-0在Set中被認為是相等的。

Set建構函式可以接受所有可迭代物件作為引數
複製程式碼
  • Set中的方法: add、has、delete、clear,forEach,size(屬性)

forEach遍歷Set,回撥函式中value和key的值相等,我的理解: Set集合中的元素都是不重複的,所以可以把值作為鍵,即“已值為鍵”。如下:

let set = new Set([1, 2]);

set.forEach(function(value, key, ownerSet)){
    console.log(key + " " + value);
    console.log(ownerSet === set);
});
複製程式碼

在回撥函式中使用this引用

let set = new Set([1, 2]);

let processor = {
    output(value) {
        console.log(value);
    },
    process(dataSet) {
        dataSet.forEach(function(){
            this.output(value);
        }, this);
    }
};

processor.process(set);
複製程式碼

箭頭函式this

let set = new Set([1, 2]);

let processor = {
    output(value) {
        console.log(value);
    },
    process(dataSet) {
        dataSet.forEach(value => this.output(value));
    }
};

processor.process(set);
複製程式碼
  • 將Set集合轉換為陣列
let set = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
    array = [...set];
    
console.log(array); //[1, 2, 3, 4, 5]
複製程式碼
function eliminbateDuplicates(items){
    return [...new Set(items)]
}

let numbers = [1, 2, 3, 3, 3, 4, 5];
    noDuplicates = eliminateDuplicates(numbers);

console.log(noDuolicates); //[1, 2, 3, 4, 5]
複製程式碼

三、 Weak Set 集合

解決Set集合的強引用導致的記憶體洩漏問題

Weak Set集合只儲存物件的弱引用,並且不可以儲存原始值;集合中的弱引用如果是物件唯一的引用,則會被回收並釋放相應記憶體。

Weak Set集合的方法:add, has,delete

  • SetWeak Set 的區別:
差異 Set Weak Set
最大區別 儲存物件值的強引用 儲存物件值的弱引用
方法傳入非物件引數 正常 報錯
可迭代性 可迭代 不可迭代
支援forEach方法 支援 不支援
支援size屬性 支援 不支援

四、 Map集合

- 有序
- 鍵值對
複製程式碼

在物件中,無法用物件作為物件屬性的鍵名;但是Map集合中,卻可以這樣做。

let map = new Map(),
    key1 = {},
    key2 = {};
    
map.set(key1, 5);
map.set(key2, 42);

console.log(map.get(key1)); //5
console.log(map.get(key2)); //42
複製程式碼

以上程式碼分別用物件 key1key2 作為兩個鍵名在Map集合裡儲存了不同的值。這些鍵名不會強制轉換成其他形式,所以這兩個物件在集合中是獨立存在的,也就是說,不需要修改物件本身就可以為其新增一些附加資訊

Map集合的方法:setgethas(key)delete(key)clearforEachsize(屬性)

Map集合初始化過程中,可以接受任意資料型別的鍵名,為了確保它們在被儲存到Map集合中之前不會被強制轉換為其他資料型別,因而只能將它們放在陣列中,因為這是唯一一種可以準確地呈現鍵名型別的方式。

五、 Weak Map集合

無序 鍵值對

- 弱引用Map集合,集合中鍵名必須是一個物件,如果使用非物件鍵名會報錯;
- 鍵名對於的值如果是一個物件,則儲存的是物件的強引用,不會觸發垃圾回收機制。
複製程式碼
  • Weak Map 最大的用途是儲存Web頁面中的DOM元素。
let map = new WeakMap(),
    element = document.querySelector('.element');
    
map.set(element, "Original");

let value = map.get(element);
console.log(value); //"Original"

//移除element元素
element.parentNode.removeChild(element);
element = null;

//此時 Weak Map集合為空,資料被同步清除

複製程式碼
  • Weak Map集合的方法

set, get, has, delete

私有物件資料

儲存物件例項的私有資料是Weak Map的另外一個應用:

var Person = (function(){
    var privateData = {},
    privateData = 0;
    
    function Person(name){
        Object.defineProperty(this, "_id", { value: privateId++ });
        
        privateData[this._id] = {
            name: name
        };
    }
    
    Person.prototype.getName = function() {
        return privateData[this._id].name;
    }
    
    return Person;
}());

複製程式碼

上面這種方法無法獲知物件例項何時被銷燬,不主動管理的話,privateData中的資料就永遠不會消失,需要使用Weak Map來解決這個問題。

let Person = (function(){
    let privateData = new WeakMap(),
    privateData = 0;
    
    function Person(name){
        privateData.set(this, {name: name});
    }
    
    Person.prototype.getName = function() {
        return privateData.get(this).name;
    }
    
    return Person;
}());
複製程式碼

當你要在Weak Map集合與普通的Map集合之間做出選擇時,需要考慮的主要問題是,是否只用物件作為集合的鍵名。

第八章 迭代器(Iterator)和生成器(Generator)

迭代器的出現旨在消除迴圈複雜性並減少迴圈中的錯誤。

一、什麼是迭代器?

迭代器是一種特殊物件,他具有一些專門為迭代過程設計的專有埠,所有的迭代器物件都有一個next()方法,每次呼叫都返回一個結果物件。

ES5語法 實現一個迭代器

function createIterator(items) {
    
    var i = 0;
    
    return {
        next: function() {
        
            var done = (i >= items.length);
            var value = !done ? items[i++] : undefined;
            
            return {
                done: done,
                value: value
            };
        }
    }
}

var iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); //"{ value: 1, done: false}"
console.log(iterator.next()); //"{ value: 2, done: false}"
console.log(iterator.next()); //"{ value: 3, done: false}"
console.log(iterator.next()); //"{ value: undefined, done: true}"
複製程式碼

二、什麼是生成器?

生成器是一種返回迭代器的函式,通過function關鍵字後的星號(*)來表示,函式中會用到新的關鍵字yield

function *createIterator() {
    yield 1;
    yield 2;
    yield 3;
}

let iterator = createIterator();

console.log(iterator.next().value); //1
console.log(iterator.next().value); //2
console.log(iterator.next().value); //3
複製程式碼

yield的使用限制

yield關鍵字只可在生成器內部使用,在其他地方使用會導致程式丟擲語法錯誤,即使在生成器內部的函式裡使用也會報錯,與return關鍵字一樣,不能穿透函式的邊界。

不能用箭頭函式來建立生成器

三、可迭代物件和for-of迴圈

可迭代物件具有Symbol.iterator屬性,是一種與迭代器密切相關的物件。Symbol.iterator通過指定的函式可以返回一個作用於附屬物件的迭代器。

  • 檢測物件是否為可迭代物件
function isIterable(object) {
    return typeof object[Symbol.iterator] === 'function';
}

console.log(isIterable([1, 2, 3])); //true
複製程式碼
  • 建立可迭代物件

預設情況下,開發者定義的物件都是不可迭代物件,但如果給Symbol.iterator新增一個生成器,則可以將其變為可迭代物件。

let collection = {
    items: [],
    *[Symbol.iterator]() {
        for (let item of this.items) {
            yield item;
        }
    }
}

collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for (let x of collection) {
    console.log(x);
}

1
2
3
複製程式碼

四、內建迭代器

  • 集合物件迭代器

    • entries()
    • values()
    • keys()
  • 字串迭代器

  • NodeList迭代器

五、高階迭代器功能

  • 給迭代器傳遞引數
function *createIterator() {
    let first = yield 1;
    let second = yield first + 2; //4 + 2
    yield second + 3; //5 + 3
}

let iterator = createIetrator();

console.log(iterator.next()); // '{ value: 1, done: false }'
console.log(iterator.next(4)); // '{ value: 6, done: false }'
console.log(iterator.next(5)); // '{ value: 8, done: false }'
console.log(iterator.next()); // '{ value: undefined, done: true }'
複製程式碼
  • 在迭代器中丟擲錯誤

呼叫next()方法命令迭代器繼續執行(可能提供一個值),呼叫throw()方法也會命令迭代器繼續執行,但同時也丟擲一個錯誤,再此之後的執行過程取決於生成器內部的程式碼。

  • 生成器返回語句

展開運算子與for-of迴圈語句會直接忽略通過return語句指定的任何返回值,只要done一變為true就立即停止讀取其他的值。

  • 委託生成器

在生成器裡面再委託另外兩個生成器

  • 非同步任務執行(******)

生成器令人興奮的特性與非同步程式設計有關。

function run(taskDef) {
    
    //建立一個無使用限制的迭代器
    let task = taskDef();
    
    //開始執行任務
    let result = task.next();
    
    //迴圈呼叫next() 的函式
    function step() {
    
        //如果任務未完成,則繼續執行
        if(!result.done){
            result = task.next();
            //result = task.next(result.value) 向任務執行器傳遞資料
            step();
        }
    }
    
    //開始迭代執行
    step();
}
複製程式碼

第九章 JavaScript 中的類

ES6中的類與其他語言中的還是不太一樣,其語法的設計實際上借鑑了Javascript的動態性。

ES5 中的近類結構,建立一個自定義型別:

  1. 首先,建立一個建構函式;
  2. 然後,定義另一個方法並賦值給建構函式的原型。
function PersonType(name) {
    this.name = name;
}

PersonType.prototype.sayName = function() {
    console.log(this.name);
}

var person = new PersonType('waltz');
person.sayName(); //'waltz'

console.log(person instanceof PersonType); //true
console.log(person instance of Object); //true

複製程式碼

一、 類的宣告

  • 基本的類宣告方法
class PersonClass {
    
    //等價於PersonType的建構函式
    constructor(name){
        this.name = name;
    }
    
    //等價於PersonType.protoType.sayName
    sayName() {
        console.log(this.name);
    }
}

let person = new PersonClass('waltz');
person.sayName(); //'waltz'

console.log(person instanceof PersonClass); //true
console.log(person instanceof Object); //true

console.log(typeof PersonClass); //'function'
console.log(typeof PersonClass.prototype.sayName) //'function'
複製程式碼

自有屬性是例項中的屬性,不會出現在原型上,且只能在類的建構函式或方法中建立。建議你在建構函式中建立所有的自有屬性,從而只通過一處就可以控制類中的所有自有屬性。

與函式不同的是,類屬性不可被賦予新值。

二、 為何使用類語法

  • 函式宣告可以被提升,而類宣告與let宣告類似,不能被提升;真正執行宣告語句之前,它們會一直存在於臨時死區(TDZ)中。
  • 類宣告中的所有程式碼將自動執行在嚴格模式下,而且無法強行讓程式碼脫離嚴格模式執行。
  • 在自定義型別中,需要通過Object.defineProperty() 方法手工指定某個方法為不可列舉;而在類中,所有的方法都是不可列舉的。
  • 每個類都有一個名為[[Construct]]的內部方法,通過關鍵字new呼叫那些不含[[Construct]]的方法會導致程式丟擲錯誤。
  • 使用除關鍵字new以外的方式呼叫類的建構函式會導致程式丟擲錯誤。
  • 在類中修改類名會導致程式報錯。

三、類表示式

和函式的宣告形式和表示式類似。

在js引擎中,類表示式的實現與類宣告稍有不同。對於類宣告來說,通過let定義的外部繫結與通過const定義的內部繫結具有相同的名稱。而命名類表示式通過const定義名稱,從而只能在類的內部使用。

四、作為一等公民的類

在程式中。一等公民是指一個可以傳入函式,可以從函式返回,並且可以賦值給變數的值。(JS函式是一等公民)

function createIbject(classDef) {
    return new classDef();
}

let Obj = createObject(class {

    sayHi() {
        console.log('Hi!')
    }
});
obj.sayHi(); //'Hi!'
複製程式碼

類表示式還有另一種使用方式,通過立即呼叫類建構函式可以建立單例。用new呼叫類表示式,緊接著通過一對小括號呼叫這個表示式:

let person = new class {

    constructor(name) {
        this.name = name;
    }
    
    sayName() {
        console.log(this.name);
    }
}('waltz');

person.sayName(); // 'waltz'

複製程式碼

依照這種模式可以使用類語法建立單例,並且不會再作用域中暴露類的引用。

五、訪問器屬性

class CustomHtmlElement() {

    constructor(element){
        this.element = element;
    }
    
    //建立getter
    get html() {
        return this.element.innerHTML;
    }
    
    //建立setter
    set html(value) {
        this.element.innnerHTML = value;
    }
    
}

var descriptor = Object.getOwnPropertyDescriptor(CustomHtmlElement.prototype, "html");
console.log("get" in descriptor); //true
console.log("set" in descriptor); //true
console.log(descriptor.enumerable); //false
複製程式碼

六、可計算成員名稱

//類方法
let methodName = "sayName";

class PersonClass(name) {
    
    constructor(name) {
        this.name = name;
    }
    
    [methodName]() {
        console.log(this.name);
    }
};

let me = new PersonClass("waltz");
me.sayName(); // 'waltz'
複製程式碼
//訪問器屬性
let propertyName = 'html';
class CustomHTMLElement)() {
    
    constructor(element) {
        this.element = element;
    }
    
    get [propertyName]() {
        return this.element.innerHTML;
    }
    
    set [propertyName](value) {
        this.element.innerHTML = value;
    }
}
複製程式碼

七、生成器方法


class MyClass {

    *createIterator() {
        yield 1;
        yield 2;
        yield 3;
    }
    
}

let instance = new MyClass();
let iterator = instance.createIterator();

複製程式碼

如果用物件來表示集合,又希望通過簡單的方法迭代集合中的值,那麼生成器方法就派上用場了。

儘管生成器方法很實用,但如果你的類是用來表示值的 集合 的,那麼為它定義一個 預設迭代器 更有用。

八、靜態成員

直接將方法新增到建構函式中來模擬靜態成員是一種常見的模式。

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

//靜態方法
PersonType.create = function(name) {
    return new PersonType(name);
}

//例項方法
PersonType.protoType.sayName = function() {
    console.log(this.name);
};

var person = PersonType.create('waltz');
複製程式碼

類等價:

class PersonClass {

    // 等價於PersonType建構函式
    constructor(name) {
        this.name = name;
    }
    
    //等價於PersonType.prototype.sayName
    sayName() {
        console.log(this.name);
    }
    
    //等價於PersonType.create
    static create(name) {
        return new PersonClass(name);
    }
}

let person = PersonClass.create('waltz');
複製程式碼

類中的所有方法和訪問器屬性都可以用static關鍵字來定義,唯一的限制是不能將static用於定義建構函式方法。

不可在例項中訪問靜態成員,必須要直接在類中訪問靜態成員。

九、繼承與派生類

ES5實現

function Rectangle(length, width) {
    this.length = length;
    this.width = width;
}

Rectangle.prototype.getArea = function() {
    return this.length * this.width;
};

function Square(length) {
    Rectangle.call(this, length, length);
}

Square.prototype = Object.create(Rectangle.prototype, {
    constuctor: {
        value: Square,
        enumerable: true,
        writable: true,
        configurable: true
    }
});

var square = new Square(3);

console.log(square.getArea()); // 9
console.log(square instanceof Square); //true
console.log(square instanceof Rectangle); true

複製程式碼

ES6類實現

class Rectangle {
    constructor(length, width) {
        this.length = length;
        this.width = width;
    }
    
    getArea() {
        return this.length * this.width;
    }
}

class Square extends Rectangle {
    //派生類指定了建構函式則必須要呼叫 super()
    constructor(length) {
        
        //等價於Retangle.call(this, length, length)
        super(length, length);
    }
    
    //如果不使用建構函式,則當建立新的類例項時會自動呼叫 super() 並傳入所有引數
}

var square = new Square(3);

console.log(square.getArea()); //9
console.log(square instanceof Square); //true
console.log(square instanceof Rectangle); //true
複製程式碼

使用super()的小貼士:

  • 只可在派生類的建構函式中使用super(),如果嘗試在非派生類(不是用extends宣告的類)或函式中使用則會導致程式丟擲錯誤。
  • 在建構函式中訪問this之前一定要呼叫super(),它負責初始化this,如果在呼叫super()之前嘗試訪問this會導致程式錯誤。
  • 如果不想呼叫super(),則唯一的方法是讓類的建構函式返回一個物件。

類方法遮蔽 --- 派生類中的方法總會覆蓋基類中的同名方法。

靜態成員繼承 --- 如果基類有靜態成員,那麼這些靜態成員在派生類中也可用。

派生自表示式的類 --- 只要表示式可以解析為一個函式並且具有[[Constructor]]屬性和原型,那麼就可以用extends進行派生。 extends強大的功能使得類可以繼承自任意型別的表示式,從而創造更多可能性。

由於可以動態確定使用哪個基類,因而可以建立不同的繼承方法

let SerializationMixin = {
    serialize() {
        return JSON.stringify(this);
    }
};

let AreaMixin = {
    getArea() {
        return this.length * this.width;
    }
};

function mixin(...mixins) {
    var base = function() {};
    Object.assign(base.prototype, ...mixins);
    return base;
}

class Square extends mixin(AreaMixin, SerializableMixin) {
    constructor(length) {
        super();
        this.length = length;
        this.width = length;
    }
}

var x = new Square(3);
console.log(x.getArea());  //9
console.log(x.serialize()); // "{'length': 3, 'width': 3}"

//如果多個mixin物件具有相同屬性,那麼只有最後一個被新增的屬性被保留。
複製程式碼

內建物件的繼承

class MyArray extends Array {
    //空
}

var colors = new MyArray();
colors[0] = "red";

console.log(colors.length); //1
colors.length = 0;
console.log(colors[0]); //undefined

複製程式碼

Symbol.species屬性

內建物件繼承的一個實用之處,原本在內建物件中返回例項自身的方法將自動返回派生類的例項。

Symbol.species是諸多內部Symbol中的一個,它被用於定義返回函式的靜態訪問器屬性。被返回的函式是一個建構函式,每當要在例項的方法中(不是在建構函式中)建立類的例項時必須使用這個建構函式。

一般來說,只要想在類方法中呼叫this.constructor,就應該使用Symbol.species屬性,從而讓派生類重寫返回型別。而且如果你正從一個已定義Symbol.species屬性的類建立派生類,那麼確保使用哪個值而不是使用建構函式。

class MyArray extends Array {
    static get [Symbol.species]() {
        return Array;
    }
}

let items = new MyArray(1, 2, 3, 4),
    subitems = items.slice(1, 3);
    
console.log(items instanceof MyArray); //true
console.log(subitems instanceof Array); //true
console.log(subitems instanceof MyArray); //false
複製程式碼

十、在類的建構函式中使用new.target

在簡單情況下,new.target等於類的建構函式。

因為類必須通過new關鍵字才能呼叫,所以在列的建構函式中,new.target屬性永遠不會是undefined

第十章 改進的陣列功能

一、建立陣列

1.1 ES6之前建立陣列的方法:

  • 呼叫Array建構函式
  • 用陣列字面量語法

1.2 ES6:

  • Array.of();

    作用:幫助開發者們規避通過Array建構函式建立陣列是的怪異行為,因為,Array建構函式表現的與傳入的的引數型別及數量有些不符;

    function createArray(arrayCreator, value){
        return arrayCreator(value);
    }
    
    let items = createArray(Array.of, value)
    複製程式碼
  • Array.from();

    ES5方法將類陣列轉化為真正的陣列:

    function makeArray(arrayLike) {
        var result = [];
        for(var i=0, len = arrayLike.length; i<len; i++) {
            result.push(arrayLike[i]);
        }
        
        return result;
    }
    
    function doSomething() {
        var args = makeArray(arguments);
        
        //使用args
    }
    複製程式碼

    改進方法:

    function makeArray(arrayLike) {
        return Array.prototype.slice.call(arrayLike);
    }
    
    function doSomething() {
        var args = makeArray(arguments);
        
        //使用args
    }
    複製程式碼

    ES6-Array.from():

    function doSomething() {
        var args = Array.from(arguments);
        
        //使用args
    }
    複製程式碼

1.3 對映轉換

如果想要進一步轉化陣列,可以提供一個對映函式作為Array.from()的第二個引數,這個函式用來將類陣列物件中的每一個值轉換成其他形式,最後將這些結果儲存在結果陣列的相應索引中。

function translate() {
    return Array.from(arguments, (value) => value + 1);
}

let numbers = translate(1, 2, 3);
console.log(numbers); //2, 3, 4
複製程式碼

也可以傳入第三個引數來表示對映函式的this值

let helper = {
    diff: 1,
    
    add(value) {
        return value + this.diff;
    }
};

function translate() {
    return Array.from(arguments, helper.add, helper);
}

let numbers = translate(1, 2, 3);
console.log(numbers); //2, 3, 4
複製程式碼

Array.from()轉換可迭代物件

let numbers = {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
};

let numbers = Array.from(numbers, (value) => value + 1);

console.log(numbers2); //2, 3, 4
複製程式碼

如果一個物件既是類陣列又是可迭代的,那麼Array.from()方法會根據迭代器來決定轉換那個值。

二、為所有陣列新增的新方法

2.1 find()方法和findIndex()方法

一旦回撥函式返回truefind()方法和findIndex()方法都會立即停止搜尋陣列剩餘的部分。

適用於根據某個條件查詢匹配的元素,如果只想查詢與某個值匹配的元素,則indexOf()方法和lastIndexOf()方法是更好的選擇。

2.2 fill()方法

  • 傳入一個值,會用這個值重寫陣列中的所有值;
  • 傳入第二個索引引數,表示從該索引位置開始替換;

如果開始索引或結束索引為負值,那麼這些值會與陣列的length屬性相加來作為最終的位置。

2.3copyWith()方法

傳入兩個引數,一個是開始填充值的索引位置,另一個是開始複製值的索引位置。(如果索引存在負值,也會與陣列的length屬性相加作為最終值

三、定型陣列

3.1 定義

定型陣列可以為JavaScript帶來快速的換位運算。ES6採用定型陣列作為語言的正式格式來確保更好的跨JavaScript引擎相容性以及與JavaScript陣列的互操作性。

所謂定型陣列,就是將任何數字轉換為一個包含數字位元的陣列。

定型陣列支援儲存和操作以下8種不同的數值型別:

  • 有符號的8位整數(int8)
  • 無符號的8位整數(uint8)
  • 有符號的16位整數(int16)
  • 無符號的16位整數(uint16)
  • 有符號的32位整數(int32)
  • 無符號的32位整數(uint32)
  • 32位浮點數(float32)
  • 64位浮點數(float64)

所有與定型陣列有關的操作和物件都集中在這8個資料型別上,但是在使用它們之前,需要建立一個 陣列緩衝區 儲存這些資料。

3.2 陣列緩衝區

陣列緩衝區是所有定型陣列的根基,它是一段可以包含特定數量位元組的記憶體地址。(類似c語言malloc()分配記憶體)

let buffer = new ArrayBuffer(10); //分配10位元組
console.log(buffer.byteLength); // 10
複製程式碼

陣列緩衝區包含的實際位元組數量在建立時就已確定,可以修改緩衝區內的資料,但是不能改變緩衝區的尺寸大小。

DataView型別是一種通用的陣列緩衝區檢視,其支援所有8種數值型資料型別。

let buffer = new ArrayBuffer(10),
    view = new DataView(buffer);
複製程式碼

可以基於同一個陣列緩衝區建立多個view, 因而可以為應用申請一整塊獨立的記憶體地址,而不是當需要空間時再動態分配。

  • 獲取讀取試圖資訊

  • 讀寫檢視資訊

    • 檢視是獨立的,無論資料之前是通過何種方式儲存的,你都可在任意時刻讀取或寫入任意格式的資料。
let buffer = new ArrayBuffer(2),
    view = new DataView(buffer); 

view.setInt(0, 5);
view.setInt(1, -1);

console.log(view.getInt16(0)); // 1535
console.log(view.getInt8(0)); //5
console.log(view.getInt8(1)); //-1
複製程式碼
  • 定型陣列是檢視

    • ES6定型陣列實際上是用於陣列緩衝區的特定型別的檢視,你可以強制使用特定的資料型別。而不是使用通用的DataView物件來運算元組緩衝區。
    • 建立定型陣列的三種方法。

3.3 定型陣列與普通陣列的相似之處

可以修改length屬性來改變普通陣列的大小,而定型陣列的length屬性是一個不可寫屬性,所以不能修改定型陣列的大小。

3.4 定型陣列與普通陣列的差別

定型陣列和普通陣列最重要的差別是:定型陣列不是普通陣列。

定型陣列同樣會檢查資料型別的合法性,0被用於代替所以非法值。

  • 附加方法

set(): 將其它陣列複製到已有的定型陣列。

subarray(): 提取已有定型陣列的一部分作為一個新的定型陣列。

第十一章、Promise與非同步程式設計

一、非同步程式設計的背景知識

JavaScript既可以像事件和回撥函式一樣指定稍後執行的程式碼,也可以明確指示程式碼是否成功執行。

JavaScript引擎一次只能執行一個程式碼塊,所以需要跟蹤即將執行的程式碼,那些程式碼被放在一個任務佇列中,每當一段程式碼準備執行時,都會被新增到任務佇列。每當JavaScript引擎中的一段程式碼結束執行,事件迴圈(event loop) 會執行佇列中的下一個任務,它是JavaScript引擎中的一段程式,負責監督程式碼執行並管理任務佇列。

事件模型--->回撥模式--->Promise

二、Promise的基礎知識

Promise相當於非同步操作結果的佔位符,它不會去訂閱一個事件,也不會傳遞一個回撥函式給目標函式,而是讓函式返回一個Promise物件。like:

// readFile承諾將在未來的某個時刻完成
let promise = readFile("example.txt");
複製程式碼

操作完成後,Promise會進入兩個狀態:

  • Fulfilled Promise非同步操作成功完成;
  • Rejected 由於程式錯誤或一些其他原因,Promise非同步操作未能成功完成。

內部屬性[[PromiseState]]被用來表示Promise的三種狀態:"pending"、"fulfilled"、"rejected"。這個屬性不暴露在Promise物件上,所以不能以程式設計的方式檢測Promise的狀態,只有當Promise的狀態改變時,通過then()方法採取特定的行動。

如果一個物件實現了上述的then()方法,那這個物件我們稱之為thenable物件。所有的Promise都是thenable物件,但並非所有thenable物件都是Promise

then方法

catch方法(相當於只給其傳入拒絕處理程式的then()方法)

// 拒絕
promise.catch(function(err)) {
    console.error(err.message);
});
複製程式碼

與下面呼叫相同

promise.then(null, function(err)){
    // 拒絕
    console.error(error.message);
});
複製程式碼

Promise比事件和回撥函式更好用

  • 如果使用事件,在遇到錯誤時不會主動觸發;
  • 如果使用回撥函式,則必須要記得每次都檢查錯誤引數;
  • 不給Promise新增拒絕處理程式,那所有失敗就自動被忽略了,所以一段要新增拒絕處理程式。

如果一個Promise處於已處理狀態,在這之後新增到任務佇列中的處理程式仍將進行。

三、建立未完成的Promise

Promise的執行器會立即執行,然後才執行後續流程中的程式碼:

let promise = new Promise(function(resolve, reject){
    console.log("Promise");
    resolve();
})

console.log("Hi!");

//Promise
//Hi!
複製程式碼

完成處理程式和拒絕處理程式總是在 執行器 完成後被新增到任務佇列的末尾。

四、建立已處理的Promise

  • 使用Promise.resolve()

  • 使用Promise.reject()

    如果向Promise.resolve()方法或Promise.reject()方法傳入一個Promise, 那麼這個Promise會被直接返回。

  • PromiseThenable物件

    Promise.resolve()方法和Promise.reject()方法都可以接受非PromiseThenable物件作為引數。如果傳入一個非PromiseThenable物件,則這些方法會建立一個新的Promise,並在then()函式中被呼叫。

    PromiseThenable物件: 擁有then()方法並且接受resolvereject這兩個引數的普通物件。

    如果不確定某個物件是不是Promise物件,那麼可以根據預期的結果將其傳入Promise.resolve()方法中或Promise.object()方法中,如果它是Promise物件,則不會有任何變化。

五、執行器錯誤

每個執行器都隱含一個try-catch塊,所以錯誤會被捕獲並傳入拒絕處理程式。

六、全域性的Promise拒絕處理

有關Promise的其中一個 最具爭議 的問題是,如果在沒有拒絕處理程式的情況下拒絕一個Promise,那麼不會提示失敗資訊。

6.1 Node.js環境的拒絕處理

  • unhandledRejection

    在一個事件迴圈中,當Promise被拒絕,並且沒有提供拒絕處理程式時,觸發該事件。

    let rejected;
    
    process.on("unhandledRejection", function(reason, promise){
        console.log(reason.message); // "Explosion!"
        console.log(rejected === promise); // true
    });
    
    rejected = Promise.reject(new Error("Explosion!"));
    複製程式碼
  • rejectionHandled

    在一個事件迴圈之後,當Promise被拒絕時,若拒絕處理程式被呼叫,觸發該事件。

    let rejected;
    
    process.on("rejectionHandled", function(promise){
        console.log(rejected === promise); // true
    });
    
    rejected = Promise.reject(new Error("Explosion!"));
    
    //等待新增拒絕處理程式
    setTimeout(function(){
        rejected.catch(function(value){
            console.log(value.message); // "Explosion!"
        });   
    }, 1000);
    複製程式碼

6.2 瀏覽器環境 的拒絕處理

  • unhandledRejection(描述與Node.js相同)

  • rejectionHandled

瀏覽器中的實現與Node.js中的幾乎完全相同,二者都是用同樣的方法將Promise及其拒絕值儲存在Map集合中,然後再進行檢索。唯一的區別是,在事件處理程式中檢索資訊的位置不同。

七、串聯Promise

每次呼叫then()方法或catch()方法時實際上建立並返回了另一個Promise,只有當第一個Promise完成或被拒絕後,第二個才會被解決。

務必在Promise鏈的末尾留有一個拒絕處理程式以確保能夠正確處理所有可能發生的錯誤。

拒絕處理程式中返回的值仍可用在下一個Promise的完成處理程式中,在必要時,即使其中一個Promise失敗也能恢復整條鏈的執行。

八、在Promise中返回Promise

在完成或拒絕處理程式中返回Thenable物件不會改變Promise執行器的執行動機,先定義的Promise的執行器先執行,後定義的後執行。

九、響應多個Promise

  • Promise.All()方法
  • Promise.race()方法

十、自Promise繼承

Promise與其他內建型別一樣,也可以作為基類派生其他類,所以你可以定義自己的Promise變數來擴充套件內建Promise的功能。

十一、基於Promise的非同步任務執行

let fs = require("fs");
function run(taskDef) {

    //建立迭代器
    let task = taskDef();
    
    //開始執行任務
    let result = task.next();
    
    //遞迴函式遍歷
    (function step() {
    
        //如果有更多工要做
        if(!result.done) {
            
            //用一個Promise來解決會簡化問題
            let promise = Promise.resolve(result.value);
            promise.then(function(value) {
                result = task.next(value);
                step();
            }).catch(function(error){
                result = task.throw(error);
                step();
            })
        }
    }());
}

//定義一個可用於任務執行器的函式

function readFile(filename) {
    return new Promise(function(resolve, reject) {
       fs.readFile(filename, function(err, contents){
            if(err){
                reject(err);
            }else{
                resolve(contents);
            }
       }); 
    });
}

//執行一個任務

run(function*(){
    let contents = yield readFile("config.json");
    doSomethingWith(contents);
    console.log("done");
})
複製程式碼

ES2017 await

第十二章、代理(Proxy)和反射(Reflection)API

代理(Proxy)是一種可以攔截並改變底層JavaScript引擎操作的包裝器,在新語言中通過它暴露內部運作的物件。

一、代理和反射

呼叫 new Proxy()可建立代替其他目標物件的代理,它虛擬化了目標,所以二者看起來功能一致。

代理可以攔截 JavaScript 引擎內部目標的底層物件操作,這些底層操作被攔截後會觸發響應特定操作的陷阱函式。

反射APIReflect物件的形式出現,物件中方法的預設特性與相同的底層操作一致,而代理可以覆寫這些操作,每個代理陷阱對應一個命名和引數都相同的Reflect方法。

二、使用set陷阱驗證屬性 / 用get陷阱驗證物件解構(Object Shape)

set代理陷阱可以攔截寫入屬性的操作,get代理陷阱可以攔截讀取屬性的操作。

let target = {
    name: "target"
}

let proxy = new Proxy(target, {
    set(trapTarget, key, value, receiver) {
        
        //忽略不希望受到影響的已有屬性
        if(!trapTarget.hasOwnProperty(key)) {
            if(isNaN(value)) {
                throw new TypeError("屬性必須是數字");
            }
        }
        
        //新增屬性
        return Reflect.set(trapTarget, key, value, receiver);
    }
});

//新增一個新屬性
proxy.count = 1;
console.log(proxy.count); //1
console.log(target.count); //1

//由於目標已有name屬性因而可以給它賦值
proxy.name = "proxy";
console.log(proxy.name); //"proxy"
console.log(target.name); //"proxy"
複製程式碼
let proxy = new Proxy({},{
    get(trapTarget, key, receiver) {
        if (!(key in receiver)) {
            throw new TypeError("屬性" + key + "不存在");
        }
        
        return Reflect.get(trapTarget, key, receiver);
    }
});

//新增一個屬性,程式仍正常執行
proxy.name = "proxy";
console.log(proxy.name); // "proxy"

//如果屬性不存在,則丟擲錯誤
console.log(proxy.nme); // 丟擲錯誤
複製程式碼

三、使用has陷阱隱藏已有屬性

四、使用deleteProperty陷阱防止刪除屬性

五、原型代理陷阱

  • 原型代理陷阱的執行機制

原型代理陷阱有一些限制:

  1. getPrototypeOf陷阱必須返回物件或null,只要返回值必將導致執行時錯誤,返回值檢查可以確保Object.getPropertyOf()返回的總是預期的值;
  2. setPropertyOf陷阱中,如果操作失敗則返回的一定是false,此時Object.setPrototypeOf()會丟擲錯誤,如果setPrototypeOf返回了任何不是false的值,那麼Object.setPrototypeOf()便假設操作成功。
  • 為什麼有兩組方法

Object.getPrototypeOf()Object.setPrototypeOf()是高階操作,建立伊始便給開發者使用的;而Reflect.getPrototypeOf()Reflect.setPrototypeOf()方法則是底層操作,其賦予開發者可以訪問之前只在內部操作的[[GetPrototypeOf]][[SetPrototypeOf]]的許可權。

Object.setPrototypeOf()Reflect.setPrototypeOf()之間在返回值上有微妙的差異,前者返回傳入的物件,後者返回布林值。

六、物件可擴充套件性陷阱

  • preventExtensions(阻止擴充套件)
  • isExtensible(判斷是否可擴充套件)

相比高階功能方法而言,底層的具有更嚴格的錯誤檢查。

七、屬性描述符陷阱

  • defineProperty(定義屬性)
  • getOwnPropertyDescriptor(獲取屬性)

給Object.defineProperty()新增限制

如果讓陷阱返回true並且不呼叫Reflect.defineProperty()方法,則可以讓Object.definePropperty()方法靜默失效,這既消除了錯誤又不會真正定義屬性。

描述符物件限制

defineProperty陷阱被呼叫時,descriptor物件有value屬性卻沒有name屬性,這是因為descriptor不是實際傳入Object.defineProperty()方法的第三個引數的引用,而是一個只包含那些被允許使用的屬性的新物件。Reflect.defineProperty()方法同樣也忽略了描述符上的所有非標準屬性。

八、ownKeys陷阱

ownKeys陷阱通過Reflect.ownKeys()方法實現預設的行為,返回的陣列中包含所有自有屬性的鍵名,字串型別和Symbol型別的都包含在內。

  • Object.getOwnPropertyNames()方法和Object.keys()方法返回的結果將Symbol型別的屬性名排除在外。
  • Object.getOwnPropertySymbols()方法返回的結果將字串型別的屬性名排除在外。
  • Object.assign()方法支援字串和Symbol兩種型別。

九、函式代理中的applyconstruct陷阱

所有的代理陷阱中,只有applyconstruct的代理目標是一個函式。

  • 驗證函式引數
  • 不用new呼叫建構函式

可以通過檢查new target的值來確定函式是否是通過new來呼叫的。

假設Numbers()函式定義在你無法修改的程式碼中,你知道程式碼依賴new target,希望函式避免檢查卻仍想呼叫函式。在這種情況下,用new呼叫時的行為已被設定,所以你只能使用apply陷阱。

  • 覆寫抽象基類建構函式
  • 可呼叫的類建構函式

十、可撤銷代理

十一、解決陣列問題

  • 檢測陣列索引
  • 新增新元素時增加length的值
  • 減少length的值來刪除元素

十二、實現MyArray類

想要建立使用代理的類,最簡單的方法是像往常一樣定義類,然後在建構函式中返回一個代理,那樣的話,當類例項化時返回的物件是代理而不是例項(建構函式中的this是該例項)。

將代理用作原型

雖然從類建構函式返回代理很容易,但這也意味著每建立一個例項都要建立一個新代理。然而有一種方法可以讓所有的例項共享一個代理:將代理用作原型。

  • 在原型上使用get陷阱
  • 在原型上使用set陷阱
  • 在原型上使用has陷阱
  • 將代理用作類的原型

第十三章 用模組封裝程式碼

一、什麼是模組?

模組是自動執行在嚴格模式下並且沒有辦法退出執行的Javascript程式碼。

注:在模組的頂部,this的值是undefined;模組不支援HTML風格的程式碼註釋。

  • 匯出的基本語法
  • 匯入的基本語法
    • 匯入單個繫結
    • 匯入多個繫結匯入繫結的微妙怪異之處
export var name = "xszi";
export function setName(newName) {
    name = newName;
}

//匯入之後
import { name, setName } from "./example.js";

console.log(name);  //xszi
setName("waltz");
console.log(name); //waltz

name = "hahha"; //丟擲錯誤

複製程式碼
  • 匯入和匯出重新命名

  • 模組的預設值

    • 匯出預設值
    • 匯入預設值

    只能為每個模組設定一個預設的匯出值,匯出時多次使用default關鍵字是一個語法錯誤。

    用逗號將預設的本地名稱與大括號包裹的非預設值分隔開,請記住,在import語句中,預設值必須排在非預設值之前。

  • 重新匯出一個繫結

  • 無繫結匯入

即使沒有任何匯出或匯入的操作,這也是一個有效的模組。

無繫結匯入最有可能被應用與建立PilyfillShim

Shim: 是一個庫,它將一個新的API引入到一箇舊的環境中,而且僅靠舊環境中已有的手段實現。

Polyfill: 一個用在瀏覽器API上的Shim,我們通常的做法是先檢查當前瀏覽器是否支援某個API,如果不支援的話就載入對用的polyfill

把舊的瀏覽器想想成一面有裂縫的牆,這些polyfill會幫助我們把這面牆的裂縫填平。

二、載入模組

  1. 在web瀏覽器中使用模組

    //載入一個JavaScript模組檔案
    <script type="module" src="module.js"></script>
    複製程式碼
    //內聯引入模組
    <script type="module">
    import { sum } from "./example.js";
    let result = sum(1, 2)
    </script>
    複製程式碼
    • web瀏覽器中的模組載入順序

    模組與指令碼不同,它是獨一無二的,可以通過import關鍵字來指明其所依賴的其他檔案,並且這些檔案必須被載入進該模組才能正確執行。為了支援該功能,<script type="module">執行時自動應用defer屬性。

    每個模組都可以從一個或多個其他的模組匯入,這會使問題複雜化。因此,首先解析模組以識別所有匯入語句;然後,每個匯入語句都觸發一次獲取過程(從網路或從快取),並且在所有匯入資源都被載入和執行後才會執行當前模組。

    • web瀏覽器中的非同步模組載入
    //無法保證這兩個哪個先執行
    <script type="module" async src="module1.js"></script>
    <script type="module" async src="module2.js"></script>
    複製程式碼

    將模組作為Worker載入

    Worker可以在網頁上下文之外執行JavaScript程式碼。

    //按照指令碼的方式載入script.js
    let worker = new Worker("script.js");
    複製程式碼
    //按照模組的方式載入module.js
    let worker = new Worker("module.js", {type: "module"});
    複製程式碼

A ECMAScript6中較小的改動

一、 安全整數

IEEE 754只能準確的表示-2^53 ~ 2^53之間的整數:

var inside = Number.MAX_SAFE_INTEGER,
    outside = inside + 1;

console.log(Number.isInteger(inside)); //true
console.log(Number.isSafeInteger(inside)); //true

console.log(Number.isInteger(outside)); //true
console.log(Number.isSafeInteger(outside)); //false
複製程式碼

二、 新的Math方法

提高通常的數學計算的速度

三、 Unicode 識別符號

四、正式化_ptoto_屬性

實際上,_proto_Object.getPrototypeOf()方法和Object.setPrototypeOf()方法的早期實現。

B 瞭解ECMAScript 7 (2016)

一、指數運算子

5 ** 2 == Math.pow(5, 2); //true
複製程式碼

求冪運算子在JavaScript所有二進位制運算子中具有最高的優先順序(一元運算子的優先順序高於**)

二、Array.prototype.includes()方法

奇怪之處:

includes()方法認為+0和-0是相等的,Object.is()方法會將+0和-0識別為不同的值。

三、函式作用域嚴格模式的一處改動

ECMAScript 2016 規定在引數被解構或有預設引數的函式中禁止使用"use strict"指令。只有引數為不包含解構或預設值的簡單引數列表才可以在函式體中使用"use strict"

相關文章