目錄導航
17、Generator
是 ES6 提供的一種非同步程式設計解決方案。 語法上是一個狀態機,封裝了多個內部狀態 。執行 Generator 函式會返回一個遍歷器物件。這一點跟promise很像,promise是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。
Generator 函式是一個普通函式,但是有兩個特徵。
1、function關鍵字與函式名之間有一個星號(位置不固定);
2、函式體內部使用yield表示式,定義不同的內部狀態(yield在英語裡的意思就是“產出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next() // { value: 'hello', done: false }
hw.next()// { value: 'world', done: false }
hw.next()// { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }
複製程式碼
該函式有三個狀態:hello,world 和 return 語句(結束執行)。呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,也就是上一章介紹的遍歷器物件(Iterator Object)。下一步,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態(執行yield後面的語句,直到遇到yield或者return語句)。
17.1、 yield表示式
yield表示式就是暫停標誌。並將緊跟在yield後面的那個表示式的值,作為返回的物件的value屬性值。yield表示式後面的表示式,只有當呼叫next方法、內部指標指向該語句時才會執行。 yield表示式與return語句既有相似之處,也有區別。相似之處在於,都能返回緊跟在語句後面的那個表示式的值。區別在於每次遇到yield,函式暫停執行,下一次再從該位置繼續向後執行,而return語句不具備位置記憶的功能。
注意:
1、 yield表示式只能用在 Generator 函式裡面,用在其他地方都會報錯。
2、 yield表示式如果用在另一個表示式之中,必須放在圓括號裡面。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
複製程式碼
3、 yield表示式用作函式引數或放在賦值表示式的右邊,可以不加括號。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
複製程式碼
任意一個物件的Symbol.iterator方法,等於該物件的遍歷器生成函式,呼叫該函式會返回該物件的一個遍歷器物件。
Generator 函式就是遍歷器生成函式,因此可以把 Generator 賦值給物件的Symbol.iterator屬性,從而使得該物件具有 Iterator 介面。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
複製程式碼
Generator 函式執行後,返回一個遍歷器物件。該物件本身也具有Symbol.iterator屬性,執行後返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g // true
複製程式碼
17.2、 next方法的引數
yield表示式本身沒有返回值,或者說總是返回undefined。next方法可以帶一個引數,該引數就會被當作上一個yield表示式的返回值。 從語義上講,第一個next方法用來啟動遍歷器物件,所以不用帶有引數。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
console.log(g.next()) // { value: 0, done: false }
console.log (g.next()) // { value: 1, done: false }
console.log (.next(true) )// { value: 0, done: false } 執行i=-1,然後i++變成了0
複製程式碼
再看下面的一個例子
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
console.log(a.next()) // Object{value:6, done:false}
console.log(a.next()) // Object{value:NaN, done:false},此時的y等於undefined
console.log(a.next()) // Object{value:NaN, done:true}
var b = foo(5);
console.log(b.next()) // { value:6, done:false }
console.log(b.next(12)) // { value:8, done:false } 此時的y=2*12
console.log(b.next(13)) // { value:42, done:true } 5+24+13
複製程式碼
通過next方法的引數,向 Generator 函式內部輸入值的例子。
//例子1
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();// Started。執行了 console.log('Started');和`1. ${yield}`這兩句
genObj.next('a') // 1. a。執行了 console.log(`1. ${yield}`);和`2. ${yield}`這兩句
console.log(genObj.next('b') ) //2.b {value: "result", done: true}。執行了console.log(`2. ${yield}`);和return 'result';這兩句
複製程式碼
上面的console.log(1. ${yield}
);分兩步執行,首先執行yield,等到執行next()時再執行console.log();
//例子2
function* dataConsumer() {
console.log('Started');
yield 1;
yield;
var a=yield;
console.log("1. "+a);
var b=yield;
console.log("2. "+b);
return 'result';
}
let genObj = dataConsumer();
console.log( genObj.next())
console.log(genObj.next());
console.log(genObj.next('a'))
console.log( genObj.next('b'));
複製程式碼
輸出結果如下:四次輸出結果如紅線框中所示
結果分析:第一次呼叫next(),執行到yield 1結束;第二次呼叫next()執行到yield結束;第三次呼叫next("a")執行 var a=yield中的yield;第四次呼叫next("b")方法呼叫var a=yield語句和var b=yield中的yield;17.3、 for…of
for...of迴圈可以自動遍歷 Generator 函式執行時生成的Iterator物件,且此時不再需要呼叫next方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
複製程式碼
一旦next方法的返回物件的done屬性為true,for...of迴圈就會中止,且不包含該返回物件,所以上面程式碼的return語句返回的6,不包括在for...of迴圈之中。
除了for...of迴圈以外,擴充套件運算子(...)、解構賦值和Array.from方法內部呼叫的,都是遍歷器介面。這意味著,它們都可以將 Generator 函式返回的 Iterator 物件,作為引數,並且遇到Generator 函式中的return語句結束。
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 擴充套件運算子
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解構賦值
let [x, y] = numbers();
x // 1
y // 2
// for...of 迴圈
for (let n of numbers()) {
console.log(n)
}
// 1,2
複製程式碼
17.4、 Generator.prototype.throw()
在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲。如果是全域性throw()命令,只能被函式體外的catch語句捕獲。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');//被內部捕獲,所以下面的程式碼還能正常執行
i.throw('b');//被外部捕獲
} catch (e) {
console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b
複製程式碼
如果 Generator 函式內部沒有部署try...catch程式碼塊,那麼throw方法丟擲的錯誤,將被外部try...catch程式碼塊捕獲。
var g = function* () {
while (true) {
yield;
console.log('內部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');//被外部捕獲,所以下面的程式碼不執行了
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 外部捕獲 a
複製程式碼
如果 Generator 函式內部和外部,都沒有部署try...catch程式碼塊,那麼程式將報錯,直接中斷執行。 throw方法丟擲的錯誤要被內部捕獲,前提是必須至少執行過一次next方法。
function* gen() {
try {
yield 1;
} catch (e) {
console.log('內部捕獲');
}
}
var g = gen();
g.throw(1);
// Uncaught 1
複製程式碼
throw方法被捕獲以後,會附帶執行下一條yield表示式。也就是說,會附帶執行一次next方法。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
複製程式碼
另外,throw命令與g.throw方法是無關的,兩者互不影響。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
try {
throw new Error();
} catch (e) {
g.next();
}
// hello
// world
複製程式碼
一旦 Generator 執行過程中丟擲錯誤,且沒有被內部捕獲,就不會再執行下去了。如果此後還呼叫next方法,將返回一個value屬性等於undefined、done屬性等於true的物件,即 JavaScript 引擎認為這個 Generator 已經執行結束了。
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');//中斷函式的執行
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次執行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
try {
v = generator.next();
console.log('第二次執行next方法', v);//因為上面程式碼呼叫時報錯了,所以不會執行該語句
} catch (err) {
console.log('捕捉錯誤', v);
}
try {
v = generator.next();
console.log('第三次執行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次執行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯誤 { value: 1, done: false }
// 第三次執行next方法 { value: undefined, done: true }
// caller done
複製程式碼
17.5、 Generator.prototype.return()
返回給定的值,並且終結遍歷 Generator 函式。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true } //
g.next() // { value: undefined, done: true }
複製程式碼
如果 Generator 函式內部有try...finally程式碼塊,且正在執行try程式碼塊,那麼return方法會推遲到finally程式碼塊執行完再執行。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
g.next() // { value: undefined, done: true }
複製程式碼
17.6、 next()、throw()、return()的共同點及區別
它們的作用都是讓 Generator 函式恢復執行,並且使用不同的語句替換yield表示式。
next()是將yield表示式替換成一個值。
throw()是將yield表示式替換成一個throw語句。
return()是將yield表示式替換成一個return語句。
17.7、 yield* 表示式
用到yield*表示式,用來在一個 Generator 函式裡面執行另一個 Generator 函式。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo(); //
yield 'y';
}
// 等同於
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同於
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x" // "a" // "b" // "y"
function* inner() {
yield 'hello!';
return "test"
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
console.log(gen.next().value) // "open"
var test=gen.next().value // 返回一個遍歷器物件
console.log(test.next().value) //"hello"
console.log(test.next().value)// "test"
console.log(gen.next().value) // "close"
複製程式碼
yield*後面的 Generator 函式(沒有return語句時),等同於在 Generator 函式內部,部署一個for...of迴圈。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同於
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
複製程式碼
如果yield*後面跟著一個陣列,由於陣列原生支援遍歷器,因此就會遍歷陣列成員。
function* gen(){
yield* ["a", "b", "c"];
}
console.log(gen().next()) // { value:"a", done:false }
複製程式碼
實際上,任何資料結構只要有 Iterator 介面,就可以被yield*遍歷。 如果被代理的 Generator 函式有return語句,那麼就可以向代理它的 Generator 函式返回資料。
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a // b // c // d // e
複製程式碼
17.8、 作為物件的屬性的Generator函式
let obj = {
* myGeneratorMethod() {
•••
}
};
複製程式碼
17.9、 Generator函式的this
Generator 函式總是返回一個遍歷器,ES6 規定這個遍歷器是 Generator 函式的例項,也繼承了 Generator 函式的prototype物件上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
複製程式碼
通過生成一個空物件,使用call方法繫結 Generator 函式內部的this。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);//調動F()並且把obj作為this傳進去,這樣給obj新增a、b、c屬性
console.log(f.next()); // Object {value: 2, done: false}
console.log(f.next()); // Object {value: 3, done: false}
console.log(f.next()); // Object {value: undefined, done: true}
console.log(obj.a) // 1
console.log(obj.b) // 2
console.log(obj.c) // 3
複製程式碼
將obj換成F.prototype。將這兩個物件統一起來。再將F改成建構函式,就可以對它執行new命令了。
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
複製程式碼
多個執行緒(單執行緒情況下,即多個函式)可以並行執行,但是隻有一個執行緒(或函式)處於正在執行的狀態,其他執行緒(或函式)都處於暫停態(suspended),執行緒(或函式)之間可以交換執行權。並行執行、交換執行權的執行緒(或函式),就稱為協程。
17.10、 應用
1、 非同步操作的同步表達。 通過 Generator 函式部署 Ajax 操作,可以用同步的方式表達。
function makeAjaxCall(url,callBack){
var xhr;
if (window.XMLHttpRequest)
{
//IE7+, Firefox, Chrome, Opera, Safari 瀏覽器執行程式碼
xhr=new XMLHttpRequest();
}else{
// IE6, IE5 瀏覽器執行程式碼
xhr=new ActiveXObject("Microsoft.XMLHTTP");
}
xhr.open("GET",makeAjaxCall,true);//確保瀏覽器相容性。
xhr.onreadystatechange=function(){
if (xhr.readyState==4 && xhr.status==200)
{
if(xhr.status>=200&&xhr.status<300||xhr.status==304){
callBack(xhr.responseText;);
}
}
}
xmlhttp.send();
}
function* main() {
var result = yield request("https://juejin.im/editor/posts/5cb209e36fb9a068b52fb360");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);//將response作為上一次yield的返回值
});
}
var it = main();
it.next();
複製程式碼
使用yield表示式可以手動逐行讀取檔案。
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
複製程式碼
2、 控制流管理
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
複製程式碼
使用Promise
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
複製程式碼
使用Generator
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函式未結束,就繼續呼叫
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
function step1(value){
return value*2;
}
function step2(value){
return value*2;
}
function step3(value){
return value*2;
}
function step4(value){
return value*2;
}
複製程式碼
注意,上面這種做法,只適合同步操作,即所有的task都必須是同步的,不能有非同步操作。 3、 部署iterator介面
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
複製程式碼
4、 作為資料結構
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
for (task of doStuff()) {}
// task是一個函式,可以像回撥函式那樣使用它
複製程式碼
17.11、 Generator函式的非同步呼叫(**需要好好理解弄懂**)
非同步程式設計的方法主要有這幾種:1、回撥函式(耦合性太強)
2、事件監聽
3、釋出/訂閱
4、Promise 物件
5、generator
1. 使用Generator來封裝非同步函式
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
複製程式碼
首先執行 Generator 函式,獲取遍歷器物件,然後使用next方法(第二行),執行非同步任務的第一階段。由於Fetch模組返回的是一個 Promise 物件,因此要用then方法呼叫下一個next方法。
2. Thunk函式
編譯器的“傳名呼叫”實現,往往是將引數放到一個臨時函式之中,再將這個臨時函式傳入函式體。這個臨時函式就叫做 Thunk 函式。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同於
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
f(thunk)
// 正常版本的readFile(多引數版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(單引數版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
複製程式碼
3. 基於 Promise 物件的自動執行
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
複製程式碼
然後,手動執行上面的 Generator 函式。
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
});
複製程式碼
自動執行器寫法:
function run(gen){
var g = gen();
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen);
複製程式碼
18、async函式
async函式是Generator 函式的語法糖。async函式就是將 Generator 函式的星號(*)替換成async,將yield替換成await,僅此而已。 async函式對 Generator 函式的改進,體現在以下四點。
- 內建執行器。 呼叫了asyncReadFile函式,然後它就會自動執行,輸出最後結果。也就是說,async函式的執行,與普通函式一模一樣,只要一行。
- 更好的語義。 async表示函式裡有非同步操作,await表示緊跟在後面的表示式需要等待結果。
- 更廣的適用性。 await命令後面,可以是 Promise 物件和原始型別的值(數值、字串和布林值,但這時會自動轉成立即 resolved 的 Promise 物件)。
- 返回值是 Promise。 async函式的返回值是 Promise 物件,進一步說,async函式完全可以看作多個非同步操作,包裝成的一個 Promise 物件,而await命令就是內部then命令的語法糖。
18.1、 Async的語法
1、async函式返回一個 Promise 物件。
async函式內部return語句返回的值,會成為then方法回撥函式的引數。async函式內部丟擲錯誤,會導致返回的 Promise 物件變為reject狀態。丟擲的錯誤物件會被catch方法回撥函式接收到。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
async function f() {
throw new Error('出錯了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
複製程式碼
2、Promise物件的狀態變化。
async函式返回的 Promise 物件,必須等到內部所有await命令後面的 Promise 物件執行完,才會發生狀態改變,除非遇到return語句或者丟擲錯誤。也就是說,只有async函式內部的非同步操作執行完,才會執行then方法指定的回撥函式。
18.2、 Await命令
正常情況下,await命令後面是一個 Promise 物件,返回該物件的結果。如果不是 Promise 物件,就直接返回對應的值。
async function f() {
// 等同於
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
複製程式碼
另一種情況是,await命令後面是一個thenable物件(即定義then方法的物件),那麼await會將其等同於 Promise 物件。
18.3、 錯誤處理
如果await後面的非同步操作出錯,那麼等同於async函式返回的 Promise 物件被reject。
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出錯了');
});
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出錯了
複製程式碼
18.4、 使用注意點
1) await命令後面的Promise物件,執行結果可能是rejected,所以最好把await命令放在try...catch程式碼塊中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一種寫法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
複製程式碼
2) 多個await命令後面的非同步操作,如果不存在繼發關係,最好讓它們同時觸發。
// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;//直接返回
let bar = await barPromise;
複製程式碼
3) await命令只能用在async函式之中,如果用在普通函式,就會報錯。
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 報錯
docs.forEach(function (doc) {
await db.post(doc);
});
}
複製程式碼
如果確實希望多個請求併發執行,可以使用Promise.all方法。
async function dbFuc(db) {
let docs = [{}, {}, {}];
let promises = docs.map((doc) => db.post(doc));
let results = await Promise.all(promises);
console.log(results);
}
複製程式碼
4) async 函式可以保留執行堆疊。
const a = () => {
b().then(() => c());
};
複製程式碼
當b()執行的時候,函式a()不會中斷,而是繼續執行。等到b()執行結束,可能a()早就執行結束了,b()所在的上下文環境已經消失了。如果b()或c()報錯,錯誤堆疊將不包括a()。
const a = async () => {
await b();
c();
};
複製程式碼
b()執行的時候,a()是暫停執行,上下文環境都儲存著。一旦b()或c()報錯,錯誤堆疊將包括a()。
18.5、 例項:按順序完成非同步操作
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
複製程式碼
上面程式碼的問題是所有遠端操作都是繼發。只有前一個 URL 返回結果,才會去讀取下一個 URL,這樣做效率很差,非常浪費時間。
async function logInOrder(urls) {
// 併發讀取遠端URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序輸出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
複製程式碼
雖然map方法的引數是async函式,但它是併發執行的,因為只有async函式內部是繼發執行,外部不受影響。
18.6、 非同步遍歷器
非同步遍歷器的最大的語法特點,就是呼叫遍歷器的next方法,返回的是一個 Promise 物件。
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
複製程式碼
18.7、 非同步 Generator 函式
語法上,非同步 Generator 函式就是async函式與 Generator 函式的結合。
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
複製程式碼
非同步 Generator 函式內部,能夠同時使用await和yield命令。可以這樣理解,await命令用於將外部操作產生的值輸入函式內部,yield命令用於將函式內部的值輸出。
19、Class
19.1、class的基本語法
新的class寫法只是讓物件原型的寫法更加清晰、更像物件導向程式設計的語法 而已。ES6 的類,完全可以看作建構函式的另一種寫法。 事實上,類的所有方法都定義在類的prototype屬性上面。
1、ES6 的類,完全可以看作建構函式的另一種寫法。類本身就指向建構函式。
Point === Point.prototype.constructor // true
複製程式碼
2、類的所有方法都定義在類的prototype屬性上面。
3、在類的例項上面呼叫方法,其實就是呼叫原型上的方法。
p1.constructor === Point.prototype.constructor // true
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
//改成類的寫法
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
typeof Point // "function"
Point === Point.prototype.constructor // true 類本身就指向建構函式。
var p1=new Point(2,4);
p1.constructor === Point.prototype.constructor // true
Point.prototype.constructor === Point // true
Object.keys(Point.prototype)// []
複製程式碼
上面程式碼中,toString方法是Point類內部定義的方法,它是不可列舉的。這一點與 ES5 的行為不一致。
19.1、 constructor方法
constructor方法預設返回例項物件(即this),完全可以指定返回另外一個物件。類必須使用new呼叫,否則會報錯。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
複製程式碼
19.2、 類的例項
與 ES5 一樣,例項的屬性除非顯式定義在其本身(即定義在this物件上),否則都是定義在原型上(即定義在class上)。
//定義類
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
//toString是原型上的方法,構造方法中的才是例項屬性
複製程式碼
與 ES5 一樣,類的所有例項共享一個原型物件。
var p1 = new Point(2,3);
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
複製程式碼
19.3、取值函式(getter)和存值函式(setter)
在“類”的內部可以使用get和set關鍵字,對某個屬性設定存值函式和取值函式,攔截該屬性的存取行為。
19.4、 屬性表示式
let methodName = 'getArea';
class Square {
constructor(length) {
// ...
}
[methodName]() {
// ...
}
}
複製程式碼
19.5、 Class表示式
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
複製程式碼
這個類的名字是Me,但是Me只在 Class 的內部可用,指代當前類。在 Class 外部,這個類只能用MyClass引用。
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
複製程式碼
如果類的內部沒用到的話,可以省略Me。
const MyClass = class { /* ... */ };
複製程式碼
採用 Class 表示式,可以寫出立即執行的 Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('張三');
person.sayName(); // "張三"
複製程式碼
class的注意事項:
1、嚴格模式。類和模組的內部,預設就是嚴格模式。
2、不存在提升。類不存在變數提升。
3、name屬性總是返回緊跟在class關鍵字後面的類名。
4、Generator 方法。Symbol.iterator方法返回一個Foo類的預設遍歷器,for...of迴圈會自動呼叫這個遍歷器。
class Foo {
constructor(...args) {
this.args = args;
}
* [Symbol.iterator]() {
for (let arg of this.args) {
yield arg;
}
}
}
for (let x of new Foo('hello', 'world')) {
console.log(x); // hello,world
}
複製程式碼
5、 This的指向。 類的方法內部如果含有this,它預設指向類的例項。 但是,必須非常小心,一旦單獨使用該方法,很可能報錯。this會指向該方法執行時所在的環境(由於 class 內部是嚴格模式,所以 this 實際指向的是undefined)
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined 本來是例項的方法,但是此時printName()不是例項呼叫的,所以this指向不明,預設為undefined
複製程式碼
一個比較簡單的解決方法是,在構造方法中繫結this,這樣就不會找不到print方法了。
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
複製程式碼
19.6、 靜態方法
如果在一個方法前,加上static關鍵字,就表示該方法不會被例項繼承,而是直接通過類來呼叫,這就稱為“靜態方法”。 如果靜態方法包含this關鍵字,這個this指的是類,而不是例項。靜態方法可以與非靜態方法重名。 class Foo {
static bar() {
this.baz();
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}
Foo.bar() // hello
複製程式碼
父類的靜態方法,可以被子類繼承。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
}
Bar.classMethod() // 'hello'
複製程式碼
靜態方法也是可以從super物件上呼叫的。
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
複製程式碼
19.7、 實力屬性的新寫法
這個屬性也可以定義在類的最頂層,其他都不變。這種新寫法的好處是,所有例項物件自身的屬性都定義在類的頭部,看上去比較整齊,一眼就能看出這個類有哪些例項屬性。
class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
複製程式碼
19.8、 靜態屬性
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
複製程式碼
19.9、 私有方法和私有屬性
1、 將私有方法移出模組,因為模組內部的所有方法都是對外可見的。
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
複製程式碼
2、利用Symbol值的唯一性,將私有方法的名字命名為一個Symbol值。一般情況下無法獲取到它們,因此達到了私有方法和私有屬性的效果。但是也不是絕對不行,Reflect.ownKeys()依然可以拿到它們。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
複製程式碼
19.10、new.target()
ES6 為new命令引入了一個new.target屬性,該屬性一般用在建構函式之中,返回new命令作用於的那個建構函式 。如果建構函式不是通過new命令或Reflect.construct()呼叫的,new.target會返回undefined,因此這個屬性可以用來確定建構函式是怎麼呼叫的。 Class 內部呼叫new.target,返回當前Class。在函式外部,使用new.target會報錯。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必須使用 new 命令生成例項');
}
}
// 另一種寫法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必須使用 new 命令生成例項');
}
}
var person = new Person('張三'); // 正確
var notAPerson = Person.call(person, '張三'); // 報錯
複製程式碼
子類繼承父類時,new.target會返回子類。主要是看new後面的類是哪個
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}
class Square extends Rectangle {
constructor(length,width) {
super(length, width);
}
}
var c=new Rectangle(1,2);
var obj = new Square(3); // 輸出 false
複製程式碼
19.11、 類的繼承
Class 可以通過extends關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。 class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 呼叫父類的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 呼叫父類的toString()
}
}
複製程式碼
1、 super關鍵字,它在這裡表示父類的建構函式,用來新建父類的this物件。
2、 子類必須在constructor方法中呼叫super方法,否則新建例項時會報錯。這是因為子類自己的this物件,必須先通過父類的建構函式完成塑造,得到與父類同樣的例項屬性和方法,然後再對其進行加工,加上子類自己的例項屬性和方法。如果不呼叫super方法,子類就得不到this物件。 或者是不寫constructor(){},寫了必須寫super()。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
}
}
let cp = new ColorPoint(); // ReferenceError
————————————————————————————————————————————————————————————
class ColorPoint extends Point {
}
// 等同於
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
複製程式碼
3、 ES5 的繼承,實質是先創造子類的例項物件this,然後再將父類的方法新增到this上面(Parent.apply(this))。ES6 的繼承機制完全不同,實質是先將父類例項物件的屬性和方法,加到this上面(所以必須先呼叫super方法),然後再用子類的建構函式修改this。
4、 在子類的建構函式中,只有呼叫super之後,才可以使用this關鍵字,否則會報錯。這是因為子類例項的構建,基於父類例項,只有super方法才能呼叫父類例項。 5 子類例項物件cp同時是ColorPoint和Point(父類)兩個類的例項,這與 ES5 的行為完全一致。
6 父類的靜態方法,也會被子類繼承。
19.12、 Object.getPrototypeOf()
Object.getPrototypeOf方法可以用來從子類上獲取父類。可以使用這個方法判斷,一個類是否繼承了另一個類。 Object.getPrototypeOf(ColorPoint) === Point// true
複製程式碼
19.13、 Super關鍵字
1、 super作為函式呼叫時,代表父類的建構函式 。ES6 要求,子類的建構函式必須執行一次super函式。 super雖然代表了父類A的建構函式,但是返回的是子類B的例項。 作為函式時,super()只能用在子類的建構函式之中,用在其他地方就會報錯。
class A {
constructor() {
console.log(new.target.name);//new.targe建構函式
}
}
class B extends A {
constructor() {
super();
}
}
new A() // A
new B() // B
複製程式碼
2、 super作為物件時,在普通方法中,指向父類的原型物件;在靜態方法中,指向父類。所以定義在父類例項上的方法或屬性,是無法通過super呼叫的。
lass A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
複製程式碼
在子類普通方法中通過super呼叫父類的方法時,方法內部的this指向當前的子類例項。
class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}
let b = new B();
b.m() // 2
複製程式碼
由於this指向子類例項,所以如果通過super對某個屬性賦值,這時super就是this,賦值的屬性會變成子類例項的屬性。
class A {
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;//此時的super相當於this
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
複製程式碼
而當讀取super.x的時候,讀的是A.prototype.x,所以返回undefined。
class A {
constructor() {
this.x = 1;
}
static print() {
console.log(this.x);
}
}
class B extends A {
constructor() {
super();
this.x = 2;
}
static m() {
super.print();
}
}
B.x = 3;
B.m() // 3
複製程式碼
靜態方法B.m裡面,super.print指向父類的靜態方法。這個方法裡面的this指向的是B,而不是B的例項。
19.14、 類的 prototype 屬性和__proto__屬性
ES5 實現之中,每一個物件都有__proto__屬性,指向對應的建構函式的prototype屬性。
instance.__proto__===A.prototype//instance是A的例項
複製程式碼
Class作為建構函式的語法糖,同時有prototype屬性和__proto__屬性,因此同時存在兩條繼承鏈。
(1)子類的__proto__屬性,表示建構函式的繼承, 總是指向父類。
(2)子類prototype屬性的__proto__屬性,**表示方法的繼承,**總是指向父類的prototype屬性。
class A {
}
class B extends A {
}
console.log(B.__proto__ === A) // true,
console.log(B.prototype.__proto__ === A.prototype )// true,
// 等同於
Object.create(A.prototype);
複製程式碼
作為一個物件,子類(B)的原型(__proto__屬性)是父類(A);作為一個建構函式,子類(B)的原型物件(prototype屬性)是父類的原型物件(prototype屬性)的例項。
19.15、例項的 __proto__ 屬性
子類例項的__proto__屬性的__proto__屬性,指向父類例項的__proto__屬性。也就是說,子類的原型的原型,是父類的原型。(p2是子類,p1是父類)
p2.__proto__.__proto__ === p1.__proto__ // true
解析:
p2.__proto__===p2的類.prototype;
p2的類.prototype.__proto__===p2的類的父類的.prototype
p1.__proto__===p2的類的父類的.prototype。
複製程式碼
因此,通過子類例項的__proto__.__proto__屬性,可以修改父類例項的行為。
p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"
複製程式碼
20、Module
20、1 嚴格模式
ES6 的模組自動採用嚴格模式,不管你有沒有在模組頭部加上"use strict";。 嚴格模式主要有以下限制。
- 變數必須宣告後再使用。
- 函式的引數不能有同名屬性,否則報錯。
- 不能使用with語句。
- 不能對只讀屬性賦值,否則報錯。
- 不能使用字首 0 表示八進位制數,否則報錯。
- 不能刪除不可刪除的屬性,否則報錯。
- 不能刪除變數delete prop,會報錯,只能刪除屬性delete global[prop]。
- eval不會在它的外層作用域引入變數(沒懂)。
- eval和arguments不能被重新賦值。
- arguments不會自動反映函式引數的變化。
- 不能使用arguments.callee。(指向用於arguments物件的函式)
- 不能使用arguments.caller,值為undefined。(caller屬性儲存著調動當前函式的函式的引用)
- 禁止this指向全域性物件。
- 不能使用fn.caller和fn.arguments獲取函式呼叫的堆疊。
- 增加了保留字(比如protected、static和interface)。
20、2 export的用法
export命令用於規定模組的對外介面,import命令用於輸入其他模組提供的功能。 export寫法種類:
1、使用大括號指定所要輸出的一組變數。export {firstName, lastName, year}; 2、直接使用export關鍵字輸出該變數。export var year = 1958;
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
等同於下面這中寫法
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
複製程式碼
通常情況下,export輸出的變數就是本來的名字,但是可以使用as關鍵字重新命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
複製程式碼
注意1:export命令規定的是對外的介面,必須與模組內部的變數建立一一對應關係。
// 報錯
export 1;
// 報錯
var m = 1;
export m;
// 報錯
function f() {}
export f;
複製程式碼
注意2:export語句輸出的介面,與其對應的值是動態繫結關係 ,即通過該介面,可以取到模組內部實時的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
複製程式碼
注意3:export命令可以出現在模組的任何位置,只要處於模組頂層就可以。
function foo() {
export default 'bar' // SyntaxError
}
foo()
複製程式碼
20、3 import的用法
import命令輸入的變數都是隻讀的,因為它的本質是輸入介面。也就是說,不允許在載入模組的指令碼里面,改寫介面。 import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
但是,如果a是一個物件,改寫a的屬性是允許的。
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作
複製程式碼
import後面的from指定模組檔案的位置,可以是相對路徑,也可以是絕對路徑,.js字尾可以省略。如果只是模組名,不帶有路徑,那麼必須有配置檔案,告訴 JavaScript 引擎該模組的位置。
import {myMethod} from 'util';
//util是模組檔名,由於不帶有路徑,必須通過配置,告訴引擎怎麼取到這個模組。
複製程式碼
注意,import命令具有提升效果,會提升到整個模組的頭部,首先執行。import是靜態執行,所以不能使用表示式和變數 ,這些只有在執行時才能得到結果的語法結構。
// 報錯
import { 'f' + 'oo' } from 'my_module';
// 報錯
let module = 'my_module';
import { foo } from module;
// 報錯
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
複製程式碼
逐一指定要載入的方法:
import { area, circumference } from './circle';
console.log('圓面積:' + area(4));
console.log('圓周長:' + circumference(14));
複製程式碼
20、4 模組的整體載入 import *
整體載入的寫法: import * from "module"
import * as circle from './circle';
console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));
複製程式碼
20、5 export default
用到export default命令,為模組指定預設輸出。 // export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
//因為是預設輸出的,所以這時import命令後面,不使用大括號。並且可以隨意取名。
customName(); // 'foo'
複製程式碼
1、下面程式碼中,foo函式的函式名foo,在模組外部是無效的。載入的時候,視同匿名函式載入。
function foo() {
console.log('foo');
}
export default foo;
複製程式碼
2、一個模組只能有一個預設輸出,因此export default命令只能使用一次。所以,import命令後面才不用加大括號,因為只可能唯一對應export default命令。 本質上,export default就是輸出一個叫做default的變數或方法,然後系統允許你為它取任意名字。但是建議import時還是用default後面的名字。
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同於
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同於
// import foo from 'modules';
複製程式碼
3、因為export default命令的本質是將後面的值,賦給default變數,所以可以直接將一個值寫在export default之後。
// 正確
export default 42;
// 報錯
export 42;
複製程式碼
4、如果想在一條import語句中,同時輸入預設方法(default)和其他介面,可以寫成下面這樣。
import _, { each, forEach } from 'lodash';
複製程式碼
5、 export default也可以用來輸出類。
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
複製程式碼
20、5 export和import的複合寫法
export { foo, bar } from 'my_module';
// 可以簡單理解為
import { foo, bar } from 'my_module';
export { foo, bar };
複製程式碼
寫成一行以後,foo和bar實際上並沒有被匯入當前模組,只是相當於對外轉發了這兩個介面,導致當前模組不能直接使用foo和bar。 預設介面的寫法如下。
export { default } from 'foo';
複製程式碼
具名介面改為預設介面的寫法如下。
export { es6 as default } from './someModule';
// 等同於
import { es6 } from './someModule';
export default es6;
複製程式碼
同樣地,預設介面也可以改名為具名介面。
export { default as es6 } from './someModule';
複製程式碼
20、6 模組的繼承
// circleplus.js
export * from 'circle';
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
複製程式碼
上面程式碼中的export*,表示再輸出circle模組的所有屬性和方法。*注意,export 命令會忽略circle模組的default方法。
// main.js
import * as math from 'circleplus';//整體載入的寫法
import exp from 'circleplus';
console.log(exp(math.e));
import exp表示,將circleplus模組的預設方法載入為exp方法。
複製程式碼
20、7 Import()
可以實現動態載入。執行時執行,也就是說,什麼時候執行到這一句,就會載入指定的模組。import()返回一個 Promise 物件。
注意:import()載入模組成功以後,這個模組會作為一個物件,當作then方法的引數。因此,可以使用物件解構賦值的語法,獲取輸出介面。
import('./myModule.js')
.then(({export1, export2}) => {
// ...•
});
複製程式碼
上面程式碼中,export1和export2都是myModule.js的輸出介面,可以解構獲得。 如果模組有default輸出介面,可以用引數直接獲得。
import('./myModule.js')
.then(myModule => {
console.log(myModule.default);
});
複製程式碼
上面的程式碼也可以使用具名輸入的形式。
import('./myModule.js')
.then(({default: theDefault}) => {
console.log(theDefault);
});
複製程式碼
20、8 module的載入實現
瀏覽器載入 ES6 模組,也使用script標籤,但是要加入type="module"屬性。 <script type="module" src="./foo.js"></script>
<!-- 等同於 -->
<script type="module" src="./foo.js" defer></script>
複製程式碼
對於外部的模組指令碼(上例是foo.js),有幾點需要注意。
1、 程式碼是在模組作用域之中執行,而不是在全域性作用域執行。模組內部的頂層變數,外部不可見。
2、 模組指令碼自動採用嚴格模式,不管有沒有宣告use strict。
3、 模組之中,可以使用import命令載入其他模組(.js字尾不可省略,需要提供絕對 URL 或相對 URL),也可以使用export命令輸出對外介面。
4、 模組之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模組頂層使用this關鍵字,是無意義的。
5、 同一個模組如果載入多次,將只執行一次。
利用頂層的this等於undefined這個語法點,可以偵測當前程式碼是否在 ES6 模組之中。
const isNotModuleScript = this !== undefined;
複製程式碼
20、9 ES6 模組與 CommonJS 模組
ES6 模組與 CommonJS 模組完全不同。 它們有兩個重大差異。1、CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。
2、 CommonJS 模組是執行時載入。 ,ES6 模組是編譯時輸出介面。 。
第二個差異是因為 CommonJS 載入的是一個物件(即module.exports屬性),該物件只有在指令碼執行完才會生成。而 ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。
第一個差異是因為CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。ES6模組是動態引用,並且不會快取值,模組裡面的變數繫結其所在的模組。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
複製程式碼
這是因為mod.counter是一個原始型別的值 ,會被快取。除非寫成一個函式,才能得到內部變動後的值。
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
get counter() {
return counter
},
incCounter: incCounter,
};
// main.js
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 4
複製程式碼
可以對obj新增屬性,但是重新賦值就會報錯。 因為變數obj指向的地址是隻讀的,不能重新賦值,這就好比main.js創造了一個名為obj的const變數。
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK
obj = {}; // TypeError
複製程式碼
commonJS和ES6內部變數的區別:
1、ES6 模組之中,頂層的this指向undefined;CommonJS 模組的頂層this指向當前模組。
2、以下這些頂層變數在 ES6 模組之中都是不存在的。
- arguments
- require
- module
- exports
- __filename
- __dirname
20.10、 ES6載入CommonJS模組(整體輸入)
Node 會自動將module.exports屬性,當作模組的預設輸出,即等同於export default xxx。 // a.js
module.exports = {
foo: 'hello',
bar: 'world'
};
// 等同於
export default {
foo: 'hello',
bar: 'world'
};
複製程式碼
由於 ES6 模組是編譯時確定輸出介面,CommonJS 模組是執行時確定輸出介面,所以採用import命令載入 CommonJS 模組時,不允許採用下面的寫法。
// 不正確
import { readFile } from 'fs';
複製程式碼
因為fs是 CommonJS格式,只有在執行時才能確定readFile介面,而import命令要求編譯時就確定這個介面。解決方法就是改為整體輸入。
// 正確的寫法一
import * as express from 'express';
const app = express.default();
// 正確的寫法二
import express from 'express';
const app = express();
複製程式碼
20.11、 CommonJS載入ES6模組(import()函式)
CommonJS 模組載入 ES6 模組,不能使用require命令,而要使用import()函式。ES6 模組的所有輸出介面,會成為輸入物件的屬性。20.12、 CommonJS 模組的載入原理。
require命令第一次載入該指令碼,就會執行整個指令碼,然後在記憶體生成一個物件。 {
id: '...',
exports: { ... },
loaded: true,
...
}
複製程式碼
該物件的id屬性是模組名,exports屬性是模組輸出的各個介面,loaded屬性是一個布林值,表示該模組的指令碼是否執行完畢。其他還有很多屬性,這裡都省略了。以後需要用到這個模組的時候,就會到exports屬性上面取值。即使再次執行require命令,也不會再次執行該模組,而是到快取之中取值。也就是說,CommonJS 模組無論載入多少次,都只會在第一次載入時執行一次,以後再載入,就返回第一次執行的結果,除非手動清除系統快取。
20.13、 CommonJS的迴圈載入
一旦出現某個模組被"迴圈載入",就只輸出已經執行的部分,還未執行的部分不會輸出。 //a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 執行完畢');
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 執行完畢');
//main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
$ node main.js
複製程式碼
執行結果如下:
在main.js中的詳細執行過程如下:
a.js指令碼先輸出一個done變數,然後載入另一個指令碼檔案b.js。注意,此時a.js程式碼就停在這裡,等待b.js執行完畢,再往下執行。 b.js執行到第二行,就會去載入a.js,這時,就發生了“迴圈載入”。系統會去a.js模組對應物件的exports屬性取值,可是因為a.js還沒有執行完,從exports屬性只能取回已經執行的部分,而不是最後的值。(a.js已經執行的部分,只有一行。)然後,b.js接著往下執行,等到全部執行完畢,再把執行權交還給a.js。於是,a.js接著往下執行,直到執行完畢。
20.14、 ES6模組的迴圈載入
ES6 模組是動態引用,如果使用import從一個模組載入變數(即import foo from 'foo'),那些變數不會被快取,而是成為一個指向被載入模組的引用
// a.mjs
import {bar} from './b';
console.log('a.mjs');
console.log(bar);
export let foo = 'foo';
//function foo() { return 'foo' }
//export {foo};
// b.mjs
import {foo} from './a';
console.log('b.mjs');
console.log(foo);
export let bar = 'bar';
//function bar() { return 'bar' }
//export {bar};
$ node --experimental-modules a.mjs
b.mjs
ReferenceError: foo is not defined
複製程式碼
上述程式碼的詳細執行過程如下:
首先,執行a.mjs以後,引擎發現它載入了b.mjs,因此會優先執行b.mjs,然後再執行a.mjs。接著,執行b.mjs的時候,已知它從a.mjs輸入了foo介面,這時不會去執行a.mjs,而是認為這個介面已經存在了,繼續往下執行。執行到第三行console.log(foo)的時候,才發現這個介面根本沒定義,因此報錯。這可以通過將foo寫成函式來解決這個問題。 這是因為函式具有提升作用(提升到頂部),在執行import {bar} from './b'時,函式foo就已經有定義了,所以b.mjs載入的時候不會報錯。這也意味著,如果把函式foo改寫成函式表示式,也會報錯。
21、程式設計風格(效能優化)
- 建議不再使用var命令,而是使用let命令取代。
- 在let和const之間,建議優先使用const,尤其是在全域性環境,不應該設定變數,只應設定常量。 原因:一個是const可以提醒閱讀程式的人,這個變數不應該改變;另一個是const比較符合函數語言程式設計思想,運算不改變值,只是新建值,而且這樣也有利於將來的分散式運算;最後一個原因是 JavaScript 編譯器會對const進行優化,所以多使用const,有利於提高程式的執行效率,也就是說let和const的本質區別,其實是編譯器內部的處理不同。
- 靜態字串一律使用單引號或反引號,不使用雙引號。動態字串使用反引號。
// bad
const a = "foobar";
const b = 'foo' + a + 'bar';
// good
const a = 'foobar';
const b = `foo${a}bar`;
複製程式碼
- 解構賦值 使用陣列成員對變數賦值時,優先使用解構賦值。
const arr = [1, 2, 3, 4];
// bad
const first = arr[0];
const second = arr[1];
// good
const [first, second] = arr;
複製程式碼
函式的引數如果是物件的成員,優先使用解構賦值。
// bad
function getFullName(user) {
const firstName = user.firstName;
const lastName = user.lastName;
}
// good
function getFullName(obj) {
const { firstName, lastName } = obj;
}
// best
function getFullName({ firstName, lastName }) {
}
複製程式碼
- 物件
單行定義的物件,最後一個成員不以逗號結尾。多行定義的物件,最後一個成員以逗號結尾。
// bad
const a = { k1: v1, k2: v2, };
const b = {
k1: v1,
k2: v2
};
// good
const a = { k1: v1, k2: v2 };
const b = {
k1: v1,
k2: v2,
};
複製程式碼
物件儘量靜態化,一旦定義,就不得隨意新增新的屬性。如果新增屬性不可避免,要使用Object.assign方法。
// bad
const a = {};
a.x = 3;
// if reshape unavoidable
const a = {};
Object.assign(a, { x: 3 });
// good
const a = { x: null };
a.x = 3;
複製程式碼
- 使用擴充套件運算子(...)拷貝陣列。使用 Array.from 方法,將類似陣列的物件轉為陣列。
const itemsCopy = [...items];
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);
複製程式碼
- 簡單的、單行的、不會複用的函式,建議採用箭頭函式。如果函式體較為複雜,行數較多,還是應該採用傳統的函式寫法。
- 不要在函式體內使用 arguments 變數,使用 rest 運算子(...)代替。
// bad
function concatenateAll() {
const args = Array.prototype.slice.call(arguments);
return args.join('');
}
// good
function concatenateAll(...args) {
return args.join('');
}
複製程式碼
- 使用預設值語法設定函式引數的預設值。
// bad
function handleThings(opts) {
opts = opts || {};
}
// good
function handleThings(opts = {}) {
// ...
}
複製程式碼
- 注意區分 Object 和 Map,只有模擬現實世界的實體物件時,才使用 Object。如果只是需要key: value的資料結構,使用 Map 結構。因為 Map 有內建的遍歷機制。
- 總是用 Class,取代需要 prototype 的操作。因為 Class 的寫法更簡潔,更易於理解。
// bad
function Queue(contents = []) {
this._queue = [...contents];
}
Queue.prototype.pop = function() {
const value = this._queue[0];
this._queue.splice(0, 1);
return value;
}
// good
class Queue {
constructor(contents = []) {
this._queue = [...contents];
}
pop() {
const value = this._queue[0];
this._queue.splice(0, 1);
return value;
}
}
複製程式碼
- 使用extends實現繼承,因為這樣更簡單,不會有破壞instanceof運算的危險。
- 如果模組只有一個輸出值,就使用export default,如果模組有多個輸出值,就不使用export default。export default與普通的export不要同時使用。
- 不要在模組輸入中使用萬用字元。因為這樣可以確保你的模組之中,有一個預設輸出(export default)。
// bad
import * as myObject from './importModule';
// good
import myObject from './importModule';
複製程式碼
- 如果模組預設輸出一個函式,函式名的首字母應該小寫。如果模組預設輸出一個物件,物件名的首字母應該大寫。
function makeStyleGuide() {
}
export default makeStyleGuide;//函式
const StyleGuide = {
es6: {
}
};
export default StyleGuide;//物件
複製程式碼