JS與ES6高級編程學習筆記(五)——ECMAScript6 代碼組織

一、概述

ES6不僅在語法上有很大的改進,在代碼的組織結構上也有重大升級,ES6中新增加了像Set、WeakSet、Map、WeakMap、ArrayBuffer、TypedArray和DataView等數組結構;原生的模塊化解決了複用、依賴、衝突、代碼組織混亂的問題,讓開發複雜的前端項目變得更加容易;類(class)的加入使JavaScript面向對象更加易於理解。

ES6除了彌補了傳統語言特性的不足外,在許多方面也增強了JavaScript動態語言的特性,可以說是揚長避短。在元編程中增加了Reflect反射對象與Proxy代理構造器,元編程是對編程語言進行編程,元編程的目標使代碼更具描述性、擁有更強的表現力和靈活性。異步流程控制可以更加優雅、方便的編寫異步程序,給用戶帶來更好的體驗與性能。

二、集合

ES6中新增加了多種數據結構,Set可以存放任意不重複的值,Map彌補了對象類型存放key-value對的不足,而WeakSet與WeakMap則解決了Set與Map在GC回收垃圾時存在內存泄漏的風險, ArrayBuffer、TypedArray和DataView的引入是爲了更加方便操作底層二進制數據的視圖。

2.1、Set

在ES6中新增加了Set這種數據結構,通常稱爲集合,Set對象允許你存儲任何類型的唯一值,無論是原始值或者是對象引用,Set中的成員不允許重複。

//創建一個Set對象,使用數組初始化集合,注意2重複了

let numbers=new Set([1,2,2,3,4,5]);

//向Set中添加成員

numbers.add(5);

numbers.add(6);

//輸出集合的大小

console.log("size:"+numbers.size);

//遍歷集合

for(let n of numbers){

console.log(n);

}

輸出結果如圖5-1所示:

圖5-1 Set示例輸出結果

示例中共添加了8個元素,但size的值爲6是因爲有兩個重複的元素。這裏使用數組初始了一個新的Set對象,也可以是實現了iterable 接口的其他數據結構,當然如果不指定此參數或其值爲null,則新的Set爲空。

(1)、Set對象的常用操作

size屬性:返回Set對象的值的個數,屬性的默認值爲0。

add(value)方法:在Set對象尾部添加一個元素。返回該Set對象。

has(value)方法:返回一個布爾值,表示該值在Set中存在與否。

delete(value)方法:移除Set的中與這個值相等的元素,返回has(value)在這個操作前會返回的值(即如果該元素存在,返回true,否則返回false)。has(value)在此後會返回false。

clear()方法:移除Set對象內的所有元素。

<script>

//創建一個空的Set對象

var numbers=new Set();

//添加

numbers.add("hello");

numbers.add("hello");

numbers.add({name:"tom"});

numbers.add({name:"tom"}); //注意對象總是不重複的

//輸出Set的元素個數

console.log("size:"+numbers.size);

//測試元素是否存在

console.log("hello在集合中嗎?"+numbers.has("hello"));

console.log("對象{name:\"tom\"}在集合中嗎?"+numbers.has({name:"tom"}));

//刪除元素

numbers.delete("hello");

numbers.delete({name:"tom"});

//輸出Set的元素個數

console.log("刪除後 size:"+numbers.size);

//清空元素

numbers.clear();

console.log("清空後 size:"+numbers.size);

//創建一個set對象,初始化特殊的重複對象

let set=new Set([NaN,NaN,undefined,undefined,null,null,{},{}]);

//使用...運算展開(spread)集合

var array=[...set];

console.log(array);

輸出結果如圖5-2所示:

圖5-2 Set示例輸出結果

示例中需要特別注意的是因爲Set中的值總是唯一的,所以需要判斷兩個值是否相等,可以參考===操作符的使用;NaN與NaN相等,undefined與undefined相等;對象(含空對象)總是不相等的。

(2)、Set對象的遍歷

keys()方法:返回鍵名的遍歷器

values()方法:返回一個新的迭代器對象,該對象包含Set對象中的按插入順序排列的所有元素的值。

entries():返回鍵值對的遍歷器

forEach(callbackFn[,thisArg])方法:按照插入順序,爲Set對象中的每一個值調用一次callBackFn。如果提供了thisArg參數,回調中的this會是這個參數。

var numbers=new Set([1,2,3]);

//遍歷所有的鍵

for(let n of numbers.keys()){

console.log(n); //輸出1,2,3

}

//遍歷所有的值

for(let n of numbers.values()){

console.log(n); //輸出1,2,3

}

//遍歷所有的鍵值對

for(let obj of numbers.entries()){

console.log(obj); //[1, 1] [2, 2] [3, 3]

}

//調用對象的forEach方法

numbers.forEach((value,key)=>console.log(value,key)); //輸出1 1 2 2 3 3

//給回調函數指定參數

numbers.forEach(function(n){

console.log(n+this); //輸出101 102 103

},100);

輸出結果如圖5-3所示:

圖5-3 Set示例輸出結果

從輸出結果可以看出因爲Set對象並沒有區分鍵與值所以輸出的結果是相同的,另外需要注意的是forEach中的回調函數帶參數時不能使用箭頭函數,因爲此時箭頭函數的this指向Window對象。

(3)、Set的使用技巧

使用Set可以方便的處理數組中的數據去重複、對多個數組進行集合運算操作:

//1、去除數組中的重複元素

var array=[1,1,2,2,3,"3","3","4","5"];

//定義Set對象,清除重複元素

var set=new Set(array);

//將set展開獲得元素唯一的數組

var unique=[...set];

console.log(unique);

var x=new Set([100,200,300]);

var y=new Set([300,400,500]);

//2、並集(合併去重)

var set1=new Set([...x,...y]);

console.log(...set1.values());

//3、交集(共有元素)

var set2=new Set([...x].filter(n=>y.has(n)));

console.log(...set2.values());

//4、補集(x中存在而y中不存在的元素)

var set3=new Set([...x].filter(n=>!y.has(n)));

console.log(...set3.values());

輸出結果如圖5-4所示:

圖5-4 Set示例輸出結果

filter是Array對象中的一個過濾方法,語法如下:

var newArray = array.filter(callback(element[,index[,array]])[,thisArg])

callback:篩選數組中每個元素的函數。返回true表示該元素保留,false則不保留。

element:數組中當前正在處理的元素。

index可選參數,正在處理的元素在數組中的索引。

array可選參數,數組本身。

2.2、WeakSet

ES6中新增加的WeakSet對象的作用是可以將弱引用對象保存在集合中,該對象的使用方法與Set基本一樣,但有如下幾點不同:

(1)、WeakSet只允許添加對象類型,不允許添加原生類型值,因爲沒有引用,而Set都可以。

(2)、WeakSet對象中存儲的對象值都是被弱引用的,如果沒有其他的變量或屬性引用這個對象值,則這個對象值會被當成垃圾回收掉.正因爲這樣,WeakSet對象是無法被枚舉的,沒有辦法拿到它包含的所有元素,而Set則不然。

(3)、WeakSet比Set更適合(和執行)跟蹤對象引用,尤其是在涉及大量對象時,可以避免一些性能問題,如內存泄漏。

下面是Stack Overflow中的一段腳本,可以用於更好的理解WeakSet:

        const requests = new WeakSet();

        class ApiRequest {
            constructor() {
                requests.add(this);
            }

            makeRequest() {
                if (!request.has(this)) throw new Error("Invalid access");
                // do work
            }
        }

從上面的代碼中可以看出這裏集合並不想控制對象的生命週期但又需要判斷對象是否存在使用WeakSet比Set要更加合適。

用於存儲DOM節點,而不用擔心這些節點從文檔移除時會引發內存泄露,即可以用來避免內存泄露的情況。

  const foos = new WeakSet()
  class Foo {
  constructor() {
    foos.add(this)
  }
  method() {
    if(!foos.has(this)) {
      throw new TypeError("Foo.prototype..method 只能在Foo的實例上調用")
    }
  }
}
// 這段代碼的作用是保證了Foo 的實例方法只能在Foo的實例上調用。
// 這裏用WeakSet的好處:數組foos對實例的引用不會被計入內存回收機制,所以刪除實例時無需考慮foos, 也不會出現內存泄露

2.3、Map

鍵值對集合是非常常用的散列數據結構(Hash),ES6之前常常使用Object當作鍵值對集合使用,但Object只能是String與Symbol作爲鍵,而ES6中新增加的Map的鍵可以是任意值,包括函數、對象或任意基本類型;Map中的key是有序的。

//定義用戶對象

var jack={name:"jack"};

var mark={name:"mark"};

//定義一個Object字面量對象,當着Key-value集合使用

var objectMap={};

objectMap[jack]=jack.name; //向對象中添加元素,使用對象作爲key

objectMap[mark]=mark.name;

console.log(objectMap[jack],objectMap[mark]);

console.log(objectMap["[object Object]"]);

輸出結果如圖5-5所示:

圖5-5 Object作Map使用示例輸出結果

當使用對象類型作爲鍵向對象中添加成員時會自動轉換爲字符串,這裏的jack與mark都轉換成了" [object Object]",所以看到的輸出結果都是mark,這並沒有達到我們的預期,使用Map可以做到。

//定義用戶對象

var jack={name:"jack"};

var mark={name:"mark"};

//創建Map對象

var map=new Map();

//向集合中添加key爲jack對象,值爲字符類型的key-value對

map.set(jack,jack.name);

map.set(mark,mark.name);

console.log(map.get(jack),map.get(mark));

輸出結果如圖5-6所示:

圖5-6 Map示例輸出結果

(1)、Map對象的常用操作

set(key,value)方法:向Map對象中設置鍵爲key的值。

size屬性:獲得Map對象的鍵值對總數。

get(key)方法:獲取鍵對應的值,如果不存在,則獲取undefined。

has(key)方法:獲取一個布爾值,表示Map實例是否包含鍵對應的值。

delete(key)方法:根據key刪除集合中的對象,成功刪除返回true,否則返回false。

clear()方法:移除Map對象的所有鍵/值對。

//定義一個空的Map對象

let users=new Map();

//設置成員

users.set("mark",{name:"mark",height:195});

//添加鍵爲jack,值爲{name:"jack",height:173}對象

users.set("jack",{name:"jack",height:173});

users.set("rose",{name:"玫瑰",height:188});

users.set("rose",{name:"rose",height:168}); //重複添加key爲rose的對象

//獲得成員個數

console.log("size:"+users.size);

//獲取成員

console.log(users.get("rose"));

console.log(users.get("tom"));

//刪除對象

users.delete("jack"); //返回true

users.delete("jack"); //返回false

//判斷成員是否存在

console.log("jack是否存在:"+users.has("jack"));

//刪除所有成員

users.clear();

console.log("size:"+users.size);

輸出結果如圖5-7所示:

圖5-7 Map示例輸出結果

示例中有幾處需要注意的地方:重複添加key爲rose的對象會覆蓋原有對象,類似修改;刪除成功時回返回true,如果key不存在則刪除失敗,返回false。

(2)、Map對象的遍歷

keys()方法:獲取迭代(Iterator)對象,含每個元素的key的數組。

values()方法:獲取迭代(Iterator)對象,含每個元素的value的數組。

entries()方法:獲取迭代(Iterator)對象,含每個元素的 [key, value] 數組。

forEach(callbackFn[, thisArg])方法:遍歷集合,如果爲forEach提供了thisArg,它將在每次回調中作爲this值。

//定義一個Map對象,使用數組初始化

let users=new Map([

["mark",{name:"mark",height:195}],

["jack",{name:"jack",height:173}],

["rose",{name:"rose",height:168}]

]);

//1、使用for同時獲取鍵與值,等同於users.entries()

for(let [key,value] of users){

console.log(key,value);

}

//2、獲取所有的鍵

for(var key of users.keys()){

console.log(key);

}

//3、獲取所有的值

for(var value of users.values()){

console.log(value);

}

//4、獲取所有的鍵值對,等同於直接users

for(var entity of users.entries()){

console.log(entity[0],entity[1]);

}

//5、使用forEach遍歷,注意value與key的順序

users.forEach((value,key)=>console.log(value,key));

//帶參數

users.forEach(function (v,k) {

console.log(this+k,v);

},'user:');

輸出結果如圖5-8所示:

圖5-8 Map示例輸出結果

(3)、Map與其它對象的轉換

Map可以與數組、對象、JSON等其它類型進行相互轉換,部分轉換示例如下:

//1、數組轉map

var array1=[[1,'a'],[2,'b']];

var map1=new Map(array1);

console.log(map1);

//2、map轉數組

var array21=[...map1.entries()]; //[[1,'a'],[2,'b']];

var array22=[...map1]; //[[1,'a'],[2,'b']];

var array23=[...map1.values()]; //['a','b'];

console.log(array21,array22,array23);

//3、對象轉成map

var user={name:"mark",height:195};

console.log(Object.entries(user)); //[["name", "mark"],["height", 195]]

var map3=new Map(Object.entries(user));

輸出結果如圖5-9所示:

圖5-9 Map示例輸出結果

注意展開運算符"…"的使用,Object.entries()的作用是獲取對象自身可枚舉屬性的鍵值對數組。

2.4、WeakMap

ES6中新增加的WeakMap與WeakSet類似也是一個弱引用的數據結構,使用方法也與Map基本相同但兩者的區別主要是內存分配與回收。

Map可能會導致內存泄漏因爲Map內部數組會一直引用着每個鍵和值(強引用),如果在使用Map時只想引用對象而不想管理其生命週期則可以考慮使用WeakMap,注意只有key是弱引用。

<body>

<div id="div1"></div>

<div id="div2"></div>

<script>

var div1=document.querySelector("#div1");

var div2=document.querySelector("#div2");

let elements=[

[div1,"文章管理"],

[div2,"商品管理"]

];

var map=new Map(elements);

</script>

</body>

示例中Map使用div1與div2作爲key,map對這兩個對象是強引用的,如果不再需要使用則需要手動釋放,否則可能會引起內存泄漏。

elements[0]=null;

elements[1]=null;

當然如果將上面的代碼修改爲WeakMap則不需要手動來管理對象的釋放了。

WeakMap只接受對象作爲鍵名,不支持clear方法,不支持遍歷,也就沒有了keys、values、entries、forEach這4個方法,也沒有屬性size;WeakMap 鍵名中的引用類型是弱引使用,假如這個引使用類型的值被垃圾機制回收了,WeakMap實例中的對應鍵值對也會消失;WeakMap中的key不計入垃圾回收,即若只有WeakMap中的key對某個對象有引用,那麼此時執行垃圾回收時就會回收該對象。

WeakMap對象只允許使用對象作爲key而Map可以是任意類型。而這些作爲鍵的對象是弱引用的,值非弱引用,如果作爲key的對象被GC回收則WeakMap中對應的對象也將被刪除,因爲不能確保key是否存在,所以key不可以枚舉。

在我們的開發過程中,如果我們想要讓垃圾回收器回收某一對象,就將對象的引用直接設置爲 null

var a = {}; // {} 可訪問,a 是其引用
a = null; // 引用設置爲 null
// {} 將會被從內存裏清理出去

但如果一個對象被多次引用時,例如作爲另一對象的鍵、值或子元素時,將該對象引用設置爲 null 時,該對象是不會被回收的,依然存在

var a = {};
var arr = [a];
a = null;
console.log(arr)  // [{}]

如果作爲 Map 的鍵:

var a = {};
var map = new Map();
map.set(a, 'hello map')

a = null;
console.log(map.keys()) // MapIterator {{}}
console.log(map.values()) // MapIterator {"hello map"}

如果想讓 a 置爲 null 時,該對象被回收,該怎麼做?

ES6 考慮到了這一點,推出了: WeakMap 。它對於值的引用都是不計入垃圾回收機制的,所以名字裏面纔會有一個"Weak",表示這是弱引用(對對象的弱引用是指當該對象應該被GC回收時不會阻止GC的回收行爲)。

Map 相對於 WeakMap :

Map 的鍵可以是任意類型,WeakMap 只接受對象作爲鍵(null除外),不接受其他類型的值作爲鍵

Map 的鍵實際上是跟內存地址綁定的,只要內存地址不一樣,就視爲兩個鍵; WeakMap 的鍵是弱引用,鍵所指向的對象可以被垃圾回收,此時鍵是無效的
Map 可以被遍歷, WeakMap 不能被遍歷

下面以 WeakMap 爲例,看看它是怎麼上面問題的:

var a = {};
var map = new WeakMap();
map.set(a, 'hello map')
map.get(a)
a = null;

2.5、ArrayBuffer、TypedArray和DataView

ES6中引入了ArrayBuffer、TypedArray和DataView,方便操作底層二進制數據的視圖,如在Canvas、Fetch API、File API、WebSockets、XMLHttpRequest等對象的API操作中會使用到。

(1)ArrayBuffer操作內存中的一段原始二進制數據。

(2)TypedArray共有 9 種類型的視圖:

Int8Array();

Uint8Array();

Uint8ClampedArray();

Int16Array();

Uint16Array();

Int32Array();

Uint32Array();

Float32Array();

Float64Array();

基本構成是"類型+位數+Array",U表示無符號,如Uint16Array()表示無符16位整數視圖。用來讀寫簡單類型的二進制數據。

(3)DataView可以自定義複合格式的視圖,用來讀寫複雜類型的二進制數據。

new DataView(buffer [,byteOffset[,byteLength]])

buffer:一個已經存在的ArrayBuffer或SharedArrayBuffer對象,DataView對象的數據源。

byteOffset:第一個字節在 buffer 中的字節偏移,默認從第1個字節開始。

byteLength:此DataView對象的字節長度。

//1、定義一個長度爲16個字節的buffer

var buffer=new ArrayBuffer(16);

console.log(buffer,buffer.byteLength);

//2、定義可以存放2個16位的整型數據視圖

var int16 = new Int16Array(2);

int16[0] = 13;

console.log(int16[0]); // 13

//3、定義一個DataView,從第10個字節開始的3個長度

var dataview1=new DataView(buffer,10,3);

console.log(dataview1,dataview1.byteLength);

輸出結果如圖5-10所示:

圖5-10 二進制數組示例輸出結果

2.6、Iterator 迭代器 △

Iterator(迭代器)是一個接口,實現該接口的對象擁有可迭代的功能,迭代器對象可以通過重複調用next()方法迭代。常見可迭代的內置對象有Array、String、Map、Set、TypedArray、Generator等,使用for…of循環可以直接迭代一個符合規範的iterator迭代器。

獲得內置對象的迭代器對象。

var greeting="Hi World!";

for(let c of greeting){

console.log(c);

}

//獲取字符串對象上的迭代器對象

var itor=greeting[Symbol.iterator]();

console.log(itor.next()); //輸出:{value: "H", done: false}

console.log(itor.next()); //輸出:{value: "i", done: false}

輸出結果如圖5-18所示:

圖5-18 Iterator示例輸出結果

當然,除了可以獲取內置對象的迭代器之外也可以自定義迭代器,自定義迭代器需要遵循接口約束。

var MyIterator = {

content: "MyIterator",

[Symbol.iterator]() {

const that = this;

let currentIndex = 0;

return {

next() {

if (currentIndex < that.content.length) {

return {done: false, value: that.content[currentIndex++]}

} else {

return {done: true, value: undefined}

}

}

}

}

}

for(let c of MyIterator){

console.log(c); //輸出:MyIterator

}

方法[Symbol.iterator]()在被調用時返回帶有next()和return()方法的迭代器對象。

三、模塊(module)

  1. Java中的包,C#中的命名空間可以很好的組織代碼,但早期的JavaScript版本沒有塊級作用域、沒有類、沒有包、也沒有模塊,這樣會帶來一些問題,如複用、依賴、衝突、代碼組織混亂等,隨着前端的膨脹,模塊化顯得非常迫切。

    通過許多開發者的努力創建了很多JavaScript模塊化規範與框架如圖5-11所示,但本質上這些都只是替代方案,並非原生的模塊化,而ES6新增加的模塊化功能改變了這些。

    圖5-10 前端模塊規範

3.1、第一個模塊

  1. 爲了讓大家快速瞭解ES6中的模塊化,現在我們在項目的js文件夾下定義第一個模塊,並引用該模塊,使用模塊中的成員。

    (1)、定義模塊,module1.js文件的內容如下:

    export let number = 100;

    export function add(x,y) {

    console.log(x+y);

    }

    (2)、引用模塊,頁面內容如下:

    <script type="module">

    import {add,number} from './js/module1.js';

    add(number,200);

    </script>

    輸出結果如圖5-11所示:

    圖5-11 第一個ES6模塊輸出結果

    在引用模塊時需要注意聲明type="module";當前並非所有的瀏覽器都支持原生的模塊,請注意兼容性,本示例運行的瀏覽器版本是:Chrome 79.0.3945.13(正式版本) (64 位),至少版本61以後。

3.2、ES6中模塊的特點

  1. (1)、模塊代碼強制運行在嚴格模式下,並且沒有任何辦法退出嚴格模式,不管是否聲明"use strict",在前面的章節中關於嚴格模式的要求已詳細說明。

    (2)、一個模塊一個文件,文件名可以是*.mjs或*.js,爲了區別模塊與其它js腳本V8推薦使用*.mjs,但考慮到兼容性問題暫時我們建議還是使用*.js。示例中module1.js就是一個獨立的文件。

    (3)、模塊間可以相互依賴。A模塊可以引用B模塊,B模塊也可依賴A模塊。

    (4)、模塊的API是靜態的,頂層內容導出之後不能被動態修改。

    (5)、模塊都是單例,每一個模塊只加載一次,只執行一次,如果下次再去加載相同文件,直接從內存中讀取。

    (6)、每個模塊內聲明的變量都是局部變量,不會污染全局作用域。在模塊頂層作用域創建的變量,不會被自動添加到共享的全局作用域,他們只會在模塊的頂層作用域內部存在,模塊的頂層作用域this值爲undefined;

3.3、export導出

  1. 從"第一個模塊"的示例中我們看到了兩個指令"export和import",export用於導出,import用於導入。

    在模塊中使用export可以導出模塊想暴露給外部使用的接口信息,這些對象可以是變量、對象、函數、類或其它模塊的內容,比如你想外部能夠訪問add這個函數,在模塊中就需要導出這個函數,否則外部不可見。

    export完成導出功能時可以放在成員的聲明前。

    //導出變量i

    export let i=100;

    //導出常量PI

    export const PI=3.14;

    //導出函數add

    export function add(m,n) {

    return m+n;

    }

    也可以先定義然後再集中導出。

    let i=100;

    const PI=3.14;

    function add(m,n) {

    return m+n;

    }

    //集中導出i,PI與add

    export {i,PI,add};

    也允許兩種方法混合,導出時可以使用as重新命名,也可以將同一個對象重命名後導出多次。

    //導出變量i

    export let i=100;

    const PI=3.14;

    function add(m,n) {

    return m+n;

    }

    //導出PI,導出函數add並重命名對外暴露的接口名稱爲plus

    export {PI,add as plus}

    導出值以修改後的爲準,如下模塊中導出的i最終的值爲200。

    var i=100;

    export {i};

    i=200;

    直接導出值是不正確的,因爲沒有接口外部不能訪問;集中導出時的大括號不能省略。

    //直接導出值是錯誤的

    export 3.14;

    var i=100;

    //這裏會被認爲是導出聲明,但i沒正確聲明,如果想以集中方式導出則這裏需要加大括號

    export i;

    正確的導出方式應該如下腳本所示。

    //導出聲明的成員pi

    export var pi=3.14;

    var i=100;

    //集中導出已定義的成員i

    export {i};

3.4、import導入

  1. 使用import指令可以加載模塊並將export導出的成員導入到使用模塊的上下文。假如已定義好的模塊module8.js如下:

    export let i=100;

    const N=200;

    function add(m,n) {

    console.log(m+'+'+n+'=',m+n);

    }

    export {N,add as plus}

    在另一個模塊或頁面中導入該模塊的代碼如下:

    //加載模塊module8.js,並指定導入成員i,N,plus

    import {i,N,plus} from './js/module8.js';

    plus(i,N); //輸出100+200= 300

    需要注意的是這裏路徑如果是相對路徑則必須以"/"、"./"、或"../"開始;不需要將所有成員導入,但導入的成員必須在導出模塊中定義且名稱一致,否則將報語法錯誤。當然可以使用as將導入的成員重命名。

    //加載模塊module8.js,並指定導入成員N,plus,並將plus重命名爲plus

    import {N,plus as sum} from './js/module8.js';

    sum(100,N); //輸出100+200= 300

    使用*號可以將所有導入的成員綁定到一個特定的對象,使用時可以通過"對象名.成員"的方式訪問,我們常常把這種導入方式稱爲命名空間導入(namespace import)。

    //導入模塊module8.js中所有成員到m8這個對象中

    import * as m8 from './js/module8.js';

    //訪問m8對象中的成員

    m8.plus(m8.i,m8.N); //輸出100+200= 300

    上面的代碼將module8中所有的對象都導出給了m8這個對象,使用時需要使加對象名訪問,可以理解爲m8就是命名空間。

    模塊允許多次導入,但因爲是單例所以實際只會執行一次;導出的頂層對象是隻讀的,不允許修改,但對象中的成員允許修改。

    模塊文件module9.js的內容如下:

    //導出變量i

    export let i=100;

    //導出對象math

    export var math={

    j:200,

    add(m,n){

    console.log(m+'+'+n+'=',m+n);

    }

    };

    console.log("module9.js 被加載!");

    導入並使用該模塊的內容如下:

    //加載模塊9

    import {i,math} from './js/module9.js';

    //再次加載並重命名對象,爲了解決衝突

    import {i as m,math as calculator} from './js/module9.js';

    //將對象的成員重新賦值,允許

    math.j=300;

    //調用add方法

    math.add(i,math.j);

    calculator.add(m,calculator.j);

    //查看math與calculator是否爲同一個對象

    console.log(math===calculator);

    //直接修改導入成員的值,不允許

    i=200; //錯誤

    math={}; //錯誤

    輸出結果如圖5-12所示:

    圖5-12 ES6模塊示例輸出結果

    從錯誤提示可以知道i被視爲常量,所以不允許修改;雖然加載了兩次模塊,但控制檯只輸出了一次"module9.js被加載",可見module9.js只執行了一次;另外math與calculator相等可以看出導出的對象是單例的。

3.5、默認導出與導入

  1. 每個模塊允許默認導出一個成員,導入時可以自定義對象名稱,而不需要使用者過多關注導入模塊的細節,解決了命名對象導出時使用該模塊必須清楚的知道每個導出成員的名稱的問題,簡單說默認導出使模塊的使用更加方便。

    //定義math對象

    let math={

    add(m,n){ //加法方法

    console.log(m+'+'+n+'=',m+n);

    },

    sub(m,n){ //減法方法

    console.log(m+'-'+n+'=',m-n);

    }

    };

    //默認導出math對象

    export default math;

    導入上面定義的模塊:

    //導入module10模塊,注意這裏沒有使用{}

    import calculator from './js/module10.js';

    //調用calculator對象中的方法

    calculator.add(200,100); //輸出:200+100= 300

    calculator.sub(200,100); //輸出:200-100= 100

    默認導出允許使用匿名對象、匿名函數或匿名變量。

    //匿名對象

    export default {price:100};

    //匿名函數

    export default function () {

    }

    //匿名變量

    export default 900;

    默認導出可以與命名導出混合使用。

    export let math={};

    export var i=100;

    var j=200;

    var k=300;

    //j作爲默認導出成員,k爲命名導出成員

    export {j as default,k};

    導入時同樣可以將命名與默認成員混合導入。

    //導出模塊名的成員,默認導出成員重命名爲j

    import {default as j,i,k} from './js/module12.js';

    console.log(j,i,k); //輸出:200 100 300

    導入其它模塊時允許將導入的內容再次導出。

    //導入模塊module12的成員,重命名後導出

    export {i as n1,k} from './js/module12.js';

    //導入模塊module12的所有成員並重新導出

    export * from './js/module12.js';

    通過上面的方法可以實現模塊間的"繼承"。

四、類(class)

面向對象編程中class是非常重要的,如果你熟悉像Java、C#、C++這樣的面向對象編程語言,你想用其中的面向對象思維來理解JavaScript是非常難的,因爲JavaScript並非真正的面嚮對象語言,所以這給開發者帶來了較大的障礙,ES6中增加了類(class),這樣可以讓JavaScript更加接近傳統面嚮對象語言。

4.1、第一個類

假定我們現在要定義一個"形狀(方、圓、五角形…)"類,該類擁有"顏色"屬性,與"顯示"顏色的方法。

傳統定義如下:

//定義形狀類(構造器)

function Shape(color) {

this.color=color;

}

//在構造器的原型對象中添加show方法

Shape.prototype.show=function () {

console.log("形狀的顏色:"+this.color);

}

//創建對象,並調用show方法

var shape=new Shape("藍色");

shape.show();

控制檯輸出結果:形狀的顏色:藍色

ES6定義如下:

//定義Shape類

class Shape{

//帶參構造函數

constructor(color){

this.color=color;

}

//show方法

show(){

console.log("形狀的顏色:"+this.color);

}

}

//創建對象,並調用show方法

let shape=new Shape("藍色");

shape.show();

控制檯輸出結果:形狀的顏色:藍色

可以看出輸出結果是完全一樣的,但ES6定義類的方法明顯更加接近傳統OOP的方式。

4.2、ES6中類的特點

(1)、class只是語法糖,class定義的類本質還是一個構造函數,但這種寫法更加清晰,更加接近經典面向對象的寫法。

(2)、類的所有實例方法定義在類的prototype屬性中,類中定義的方法默認爲原型中所有對象共享的方法但ES5中定義在構造器中的方法屬於對象或構造器如圖5-13所示:

圖5-13 ES6 class示例輸出結果

(3)、使用class定義的類不具有提升特性,而構造函數具有提升特性。真正執行聲明語句之前,會一直存在於臨時死區中。

(4)、類中的代碼強制運行在嚴格模式下,並且沒有任何辦法退出嚴格模式,不管是否聲明"use strict",在前面的章節中關於嚴格模式的要求已詳細說明。

(5)、在類中定義的方法不可枚舉。

(6)、類默認都擁有Constructor內部方法。

4.3、字段

類中可以定義多種成員,包含字段、構造方法、屬性、公共實例方法、靜態方法。

(1)、實例字段,字段可以分爲實例字段與靜態字段,實例字段是每個對象獨有的,相互間不會影響,定義時不需要使用關鍵字聲明,如果不指定值則默認爲undefined。

//定義Shape類

class Shape{

//公有實例字段

size=0;

name={};

width;

}

let rect1=new Shape();

rect1.width=100;

let rect2=new Shape();

console.log(rect1.name,rect1.size,rect1.width); //輸出:{} 0 100

console.log(rect2.name,rect2.size,rect2.width); //輸出:{} 0 undefined

console.log(rect1.name===rect2.name); //輸出:false

(2)、靜態字段,實例字段是每個實例獨享的,如果需要共享則可以定義成靜態字段,在字段聲明前加上關鍵字static,靜態成員屬於類這點與傳統面向對象一致。

class Shape{

//定義靜態字段

static width=100;

}

let s1=new Shape();

console.log(s1.width); //輸出:undefined

console.log(Shape.width); //輸出:100

因爲靜態字段屬於類,訪問時只能用類名訪問,所以s1中並沒有width字段而需要使用Shape訪問。靜態字段可用於存放緩存數據、固定結構數據或者其他你不想在所有實例都複製一份的數據。

4.4、方法

方法也可以分爲實例方法、靜態方法與構造方法。實例方法屬於實例,通過實例名訪問;靜態方法通過類名訪問;在實例方法中可以通過類名訪問靜態字段,但是在靜態方法中不能直接通過this訪問實例成員。

class Shape {

//實例字段

width="100";

//靜態字段

static PI=3.14;

//實例方法

getWidth(){

console.log("寬:"+this.width);

}

//靜態方法

static getPI(){

console.log("PI:"+Shape.PI);

}

}

var shape=new Shape();

shape.getWidth(); //輸出:寬:100

Shape.getPI(); //輸出:PI:3.14

構造方法是通過new關鍵字創建對象時調用的特殊方法,ES6中class的構造方法具有如下特性:

(1)、方法名爲constructor,這與經典的面向對象爲類名的區別較大;

(2)、每個類都有一個默認的空構造方法;

(3)、構造方法默認會返回this,不建議指定返回對象;

(4)、一個類只能定義一個構造方法,沒有重載;

class Shape {

constructor(width){ //構造方法

this.width=width;

}

}

4.5、屬性

在class中通過get與set關鍵字可以聲明屬性,get方法用於取值,set方法用於設置值。

class Shape {

//獲取寬度

get width(){

return this._width;

}

//設置寬度

set width(value){

//約束屬性值

if(value>=0) {

this._width = value;

}else{

throw "寬度必須大於等於0";

}

}

}

let shape=new Shape();

shape.width=100; //設置正確的值

console.log(shape.width); //獲取值

shape.width=-100; //設置不合理的值

輸出結果如圖5-14所示:

圖5-14 ES6 class示例輸出結果

類中的成員還有一些,比如Generator生成器、私有成員等;私有成員暫時沒有統一的解決方法,可以通過"_名稱"的方式命名約束,通過Symbol隱藏,私有成員一般使用#names方式聲明,即爲識別符加一個前綴"#"。"#"是名稱的一部分,也用於訪問和聲明。私有成員僅能在類的內部訪問。

4.6、繼承

(1)、extends與super。繼承是面向對象最重要的特性之一,ES5中的繼承相對麻煩,在ES6中使用關鍵字extends可以很方便的實現類之間的繼承,但本質上還是基於原型鏈實現的。通過super可以訪問父類成員。

//形狀,父類

class Shape {

constructor(){

this.width=100;

}

draw(){

console.log("寬:"+this.width);

}

}

//圓,繼承形狀

class Circle extends Shape{

height=200;

draw() { //類似重寫父類方法

//調用父類方法

super.draw();

console.log("高:"+this.height);

}

}

//實例化子類對象

let circle=new Circle();

circle.draw();

console.log(circle instanceof Shape,circle instanceof Object);

輸出結果如圖5-15所示:

圖5-15 ES6 class示例輸出結果

從輸出結果可以看出circle對象是Shape、Object類型的實例。

(2)、構造方法與this。子類必須調用父類的構造方法,如果不顯式調用將自動調用,只有調用super後,才允許用this關鍵字,否則將出錯,因爲子類實例是基於父類實例的,子類實例在獲得父類實例後再新增自己的方法與屬性。super調用父類構造方法時this指向的是子類實例。

//形狀,父類

class Shape {

constructor(type="形狀"){ //構造方法

this.type=type;

console.log("調用父類構造方法");

}

draw(){

console.log("這是一個"+this.type);

}

}

//圓,繼承形狀

class Circle extends Shape{

constructor(radius){ //子類構造方法

super("圓形"); //調用父類構造函數

this.radius=radius;

}

draw() {

super.draw(); //調用父類中的draw()方法,該方法在原型中

console.log(this.type+"的半徑是"+this.radius);

}

}

let circle=new Circle(75); //輸出:調用父類構造方法

circle.draw();

輸出結果如圖5-16所示:

圖5-16 ES6 class示例輸出結果

在構造函數中定義的屬性和方法相當於定義在父類實例上,而不是原型對象上。super作爲對象時,在實例方法中,指向父類的原型對象;在靜態方法中,指向父類。

(3)、靜態成員繼承。父類的靜態成員也將被子類繼承,這可能與經典的面向對象有些區別。

//形狀,父類

class Shape {

static width = 100; //靜態字段

static show() { //靜態方法

console.log("寬度:" + Shape.width);

}

}

//圓,繼承形狀

class Circle extends Shape {

}

Circle.show(); //輸出:寬度:100

console.log(Circle.width); //輸出:100

(4)、擴展原生類。使用繼承不僅可以擴展自定的類,也可以擴展系統中內置的類型,如:Boolean、Number、String、Array、Date、Function、RegExp、Error、Object等。

//定義類ArrayPro,繼承自內置類型Array

class ArrayPro extends Array{

getData(index){ //自定義獲得數據的方法

return this[index];

}

get size(){ //自定義屬性,獲得數組長度

return this.length;

}

get last(){ //自定義屬性,獲得最後一個元素

return this[this.size-1];

}

}

let arraypro=new ArrayPro(1,2,3,4,5,6);

console.log(arraypro.getData(1)); //輸出:2

console.log(arraypro.size); //輸出:6

console.log(arraypro.last); //輸出:6

當然ES5也可以擴展內置類型,但方法相對複雜且並不支持真正array的性質,ES6可以非常自然的完成內置類型的擴展功能。

五、元編程 △

5.1、Reflect 反射

Reflect是ES6中新增加的一個對象,並非構造器,該對象中含有多個可完成"元編程(對編程語言進行編程)"功能的靜態函數,能方便的對對象進行操作,也可以結合Proxy實現攔截功能,共計13個函數,TypeScript定義如下:

apply(target: Function, thisArgument: any, argumentsList: ArrayLike<any>): any;

construct(target: Function, argumentsList: ArrayLike<any>, newTarget?: any): any;

defineProperty(target: object, propertyKey: PropertyKey, attributes: PropertyDescriptor): boolean;

deleteProperty(target: object, propertyKey: PropertyKey): boolean;

get(target: object, propertyKey: PropertyKey, receiver?: any): any;

getOwnPropertyDescriptor(target: object, propertyKey: PropertyKey): PropertyDescriptor | undefined;

getPrototypeOf(target: object): object;

has(target: object, propertyKey: PropertyKey): boolean;

isExtensible(target: object): boolean;

ownKeys(target: object): PropertyKey[];

preventExtensions(target: object): boolean;

set(target: object, propertyKey: PropertyKey, value: any, receiver?: any): boolean;

setPrototypeOf(target: object, proto: any): boolean;

這裏演示一下get與set方法的使用:

//要反射的對象

var shape = {

width: 100, height: 200, get area() {

return this.width * this.height;

}

};

//獲取shape對象中的width屬性值

console.log(Reflect.get(shape,"width")); //輸出:100

//設置shape對象中的height屬性值爲300

Reflect.set(shape,"height",300); //true

console.log(Reflect.get(shape,"area")); //輸出:30000

//獲取shape對象中area的屬性值,area中的this使用指定的對象替代

console.log(Reflect.get(shape,"area",{width:200,height:300}));//輸出:60000

這裏需要注意的是get與set方法的最後一個參數receiver是可選參數,默認爲當前操作對象,如果指定後則this將指向該對象。

5.2、Proxy 代理

Proxy是ES6中新增加的"元編程(對編程語言進行編程)"內容,使用Proxy可以對被代理的對象進行攔截,當被代理對象被訪問時可以實現統一的處理。

//定義被代理的對象

var shape={width:100};

//定義代理代理

let proxy=new Proxy(shape,{

get:function (target, key, receiver) {

//輸出被代理的目標對象,屬性名稱,receiver爲getter調用時的this值(當前對象)

console.log(target, key, receiver);

//使用get方法從目標對象中獲取值,把取得的值加100

return Reflect.get(target, key, receiver)+100;

},

set:function (target, key, value, receiver) {

//輸出被代理的目標對象,屬性名稱,值,receiver爲getter調用時的this值(當前對象)

console.log(target, key, value, receiver);

//在目標對象上設置屬性值,設置值時將值加100

return Reflect.set(target, key, value+100, receiver);

}

});

proxy.width=101;

console.log(proxy.width);

輸出結果如圖5-19所示:

圖5-19 Proxy示例輸出結果

示例中我們的被代理對象是shape,當對該對象執行讀取操作時將自動執行get方法,攔截後將值增加了100,當對該對象執行設置值操作時將自動執行set方法,攔截後將值也增加了100,所以最後輸出301。

六、異步編程 △

6.1、Generator 生成器

Generator生成器是一種帶"*"號的特殊函數,是ES6中提供的一種異步編程解決方案。一個Generator可以在運行期間暫停,可以立即或稍後再繼續執行。

function *SendDataGenerator() {

yield "建立連接"; //產出一個狀態,暫停點

console.log("1");

yield "傳輸數據";

console.log("2");

return "斷開連接"; //完成

}

//調用生成器創建一個生成器實例

var sender=SendDataGenerator();

console.log(sender.next()); //輸出:{value: "建立連接",done: false}

console.log(sender.next()); //輸出:{value: "傳輸數據",done: false}

console.log(sender.next()); //輸出:{value: "斷開連接",done: true}

console.log(sender.next()); //輸出:{value: undefined,done: true}

輸出結果如圖5-17所示:

圖5-17 ES6 生成器示例輸出結果

從輸出結果可以看出函數並沒有一次執行完成,每當調用一次next方法後獲得一個狀態,向下執行一步,直到return後完成的狀態值爲true。

6.2、Promise 異步控制流

Promise提供一種異步編程解決方案,比傳統的回調函數和事件解決方案更合理、強大、簡潔。讓回調函數變成了鏈式調用,避免了層層嵌套,使程序流程變得清晰,併爲一個或者多個回調函數拋出的錯誤通過catch方法進行統一處理。

//複雜計算

function complexCompute(millseconds) {

//返回一個Promise對象

return new Promise(function (resolve, reject) {

if(millseconds<0){

throw new Error("毫秒數必須大於0"); //異常

}else if(millseconds<1000){

reject("毫秒數必須大於1000"); //失敗時回調

}

//在指定的毫秒數millseconds結束後返回一個隨機數

setTimeout(() => {

resolve(Math.random() * 1000); //成功時回調

}, millseconds);

});

}

//延遲時間爲3000毫秒,執行成功,指定成功時的處理函數

complexCompute(3000).then(v => console.log(v))

//延遲時間爲200毫秒,執行失敗,指定成功與失敗時的處理方法

complexCompute(200).then(v => console.log(v),r=>console.log(r));

//延遲時間爲-10毫秒,拋出異常,指定成功與異常時的處理函數

complexCompute(-10).then(v => console.log(v)).catch(r=>console.log(r));

輸出結果如圖5-20所示:

圖5-20 Promise示例輸出結果

6.3、async-await函數

async-await是promise和generator的語法糖,使用async-await,搭配promise,可以通過編寫形似同步的代碼實現異步編程,提高代碼的可讀性,且使代碼變得更加簡潔。

async用於聲明一個函數是異步的,當函數聲明爲async執行時將不再同步執行,函數執行完成後將返回一個promise對象,對promise對象的處理可以參考上一節。

//定義異步函數

async function getAge(age) {

if(age>0){

return age; //成功 resolve

}

else{

throw "年齡必須大於0"; //失敗 reject

}

}

//執行getAge獲得promise,指定成功時的處理方法

getAge(28).then(v=>console.log(v));

//執行getAge獲得promise,指定失敗時的處理方法

getAge(-10).catch(r=>console.log(r));

console.log("異步函數getAge後的代碼"); //先輸出

輸出結果如圖5-21所示:

圖5-21 asnyc示例輸出結果

從輸出結果可以看出來"異步函數getAge後的代碼"這一句雖然在最後但是是先輸出的,而兩次調用getAge雖然在前面但是後輸出結果的,可以看出getAge是異步的。

而await用於等待一個異步方法執行完成,await必須定義在異步方法中。

//定義函數

function getAge(age) {

//1秒後返回結果

return new Promise((resolve, reject) =>{

setTimeout(()=>resolve(age),1000);

});

}

//定義異步函數

async function client() {

//等待getAge執行成功後返回結果,未返回結果前不向下執行

let age=await getAge(18);

console.log(age);

}

//執行

console.log(client());

1-1000毫秒時輸出結果如圖5-22所示:

圖5-22 1-1000毫秒時await示例輸出結果

因爲client是異步方法,所以先輸出了一個promise對象,而此時沒有值,所有結果爲undefined,當1000毫秒後輸出的結果如圖5-23所示:

圖5-23 1000毫秒後await示例輸出結果

七、課後作業

7.1、上機任務一(90分鐘內完成)

上機目的

1、掌握ES6中集合Set與Map的應用。

2、鞏固DOM操作。

上機要求

1、定義一個app對象,在該對象中封裝好產品管理的業務邏輯,完成產品管理功能,如圖5-24所示:

圖5-24 產品管理原型

2、使用Set集合封裝所有的數據。

3、完成產品的展示、添加、編輯、刪除功能,刪除時需要提示用戶是否刪除,添加時需要校驗字段是否爲空,嘗試添加重複數據到Set集合中。

4、先用Set完成所有功能,複製頁面後將Set替換成Map,實現相同的功能,試比較兩者的區別。

推薦實現步驟

步驟1:創建好app對象,根據業務設計出對象的結構,參考結構如下,可以根據自己的思路調整。

var app = {

data: new Set([{...}, {...}, {...}...]),

current:null,

init() {

//初始化

},

query() {

//搜索與展示

},

delete() {

//刪除

},

findById(id) {

//根據編號獲得產品對象

},

edit() {

//編輯

},

save() {

//保存

}

};

步驟2:根據不同的方法完成相應的功能,先不需要考慮界面,在控制檯完成所有的方法測試,通過後再根據需要渲染界面,完成其它功能。

步驟3:反覆測試運行效果,優化代碼,關鍵位置書寫註釋,必要位置進行異常處理。

7.2、上機任務二(90分鐘內完成)

上機目的

1、掌握ES6中模塊的定義、導入與導出。

2、掌握ES6中模塊間的引用與應用。

上機要求

1、使用模塊改進本章上機任務一,完成一個升級版本的產品管理功能,效果如圖5-24所示:

2、定義5個模塊,模塊間的依賴關係與基本功能如圖5-25所示,模塊中的成員僅供參考,可以根據自己的實現思路進行調整。

圖5-25 產品管理模塊間依賴關係

3、頁面最終只允許使用app.js主模塊與utils.js工具模塊。

4、所有功能要求請參照本章的上機任務一。

5、必須使用到import、export、默認導入與導出技術。

推薦實現步驟

步驟1:根據依賴關係逐個創建好每個模塊,先創建沒有依賴任何模塊的模塊,控制檯測試各模塊功能。

步驟2:保證模塊的正確性後按要求完成每個功能。

步驟3:反覆測試運行效果,優化代碼,關鍵位置書寫註釋,必要位置進行異常處理。

7.3、上機任務三(60分鐘內完成)

上機目的

1、掌握ES6中模塊與類的定義。

2、掌握類的繼承。

3、瞭解Canvas繪畫技術。

上機要求

  1. 定義好一個模塊shapeModule.js,該模塊向外暴露3個類。
  2. 如圖5-26所示創建3個class(類),定義好屬性與方法,父類中draw方法向控制檯輸出當前形狀的基本信息,不需要實現繪圖功能,area方法計算形狀的面積,PI是靜態字段。

圖5-26 繼承關係圖

2、實現形狀間的繼承關係,構造方法要求可以初始化所有參數,子類構造方法要求調用父類構造方法,如圖5-26所示。

3、分別創建不同類型的測試對象,定義對象時傳入參數,調用對象中的方法。

4、重寫draw方法,通過Canvas實現繪圖功能,參考代碼如下所示:

<canvas id="canvas1" width="500" height="500"></canvas>

<script>

var c=document.getElementById("canvas1");

var cxt=c.getContext("2d");

cxt.fillStyle="dodgerblue";

//fillRect(x: number, y: number, w: number, h: number): void;

cxt.fillRect(200,200,100,200);

cxt.beginPath();

//arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void;

cxt.arc(100,100,100,0,Math.PI*2,true);

cxt.closePath();

cxt.fillStyle="orangered";

cxt.fill();

</script>

圖5-27 Canvas繪圖參考示例

5、定義一個drawHandler方法,接受不同的形狀實例,調用繪圖方法,在頁面上繪出不同的圖形,請使用多態的方式。

推薦實現步驟

步驟1:創建模塊與頁面,按要求定義好三個類並export,並實現其繼承關係,測試效果。

步驟2:學會HTML5中使用Canvas繪畫的基本技巧後,重寫draw方法

步驟3:在頁面中導入模塊,創建測試對象,調用方法實現繪圖功能。

步驟4:反覆測試運行效果,優化代碼,關鍵位置書寫註釋,必要位置進行異常處理。

7.4、代碼題

1、使用XMLHttpRequest第2版XHR2從服務器獲取任意一張圖片的二進制數據,顯示在頁面中如圖5-28所示。

圖5-28 AJAX獲得圖片數據顯示在頁面中

2、在第1題的基礎上將請求到的圖片進行水平翻轉,如下圖5-29所示。

圖5-29 客戶端翻轉圖片效果

八、源代碼

https://gitee.com/zhangguo5/JS_ES6Demos.git

九、教學視頻

https://www.bilibili.com/video/BV1bY411u7ky?share_source=copy_web
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章