目錄導航node
是 ES6 提供的一種異步編程解決方案。 語法上是一個狀態機,封裝了多個內部狀態 。執行 Generator 函數會返回一個遍歷器對象。這一點跟promise很像,promise是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果。
Generator 函數是一個普通函數,可是有兩個特徵。git
一、function關鍵字與函數名之間有一個星號(位置不固定);es6
二、函數體內部使用yield表達式,定義不一樣的內部狀態(yield在英語裏的意思就是「產出」)。github
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語句)。shell
yield表達式就是暫停標誌。並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值。yield表達式後面的表達式,只有當調用next方法、內部指針指向該語句時纔會執行。 yield表達式與return語句既有類似之處,也有區別。類似之處在於,都能返回緊跟在語句後面的那個表達式的值。區別在於每次遇到yield,函數暫停執行,下一次再從該位置繼續向後執行,而return語句不具有位置記憶的功能。express
注意:編程
一、 yield表達式只能用在 Generator 函數裏面,用在其餘地方都會報錯。
二、 yield表達式若是用在另外一個表達式之中,必須放在圓括號裏面。json
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
複製代碼
三、 yield表達式用做函數參數或放在賦值表達式的右邊,能夠不加括號。api
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
複製代碼
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'));
複製代碼
輸出結果以下:四次輸出結果如紅線框中所示
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
複製代碼
在函數體外拋出錯誤,而後在 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
複製代碼
返回給定的值,而且終結遍歷 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 }
複製代碼
它們的做用都是讓 Generator 函數恢復執行,而且使用不一樣的語句替換yield表達式。
next()是將yield表達式替換成一個值。
throw()是將yield表達式替換成一個throw語句。
return()是將yield表達式替換成一個return語句。
用到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
複製代碼
let obj = {
* myGeneratorMethod() {
•••
}
};
複製代碼
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),線程(或函數)之間能夠交換執行權。並行執行、交換執行權的線程(或函數),就稱爲協程。
一、 異步操做的同步表達。 經過 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();
}
}
複製代碼
二、 控制流管理
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都必須是同步的,不能有異步操做。 三、 部署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
複製代碼
四、 做爲數據結構
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是一個函數,能夠像回調函數那樣使用它
複製代碼
一、回調函數(耦合性太強)
二、事件監聽
三、發佈/訂閱
四、Promise 對象
五、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);
複製代碼
async函數是Generator 函數的語法糖。async函數就是將 Generator 函數的星號(*)替換成async,將yield替換成await,僅此而已。 async函數對 Generator 函數的改進,體如今如下四點。
- 內置執行器。 調用了asyncReadFile函數,而後它就會自動執行,輸出最後結果。也就是說,async函數的執行,與普通函數如出一轍,只要一行。
- 更好的語義。 async表示函數裏有異步操做,await表示緊跟在後面的表達式須要等待結果。
- 更廣的適用性。 await命令後面,能夠是 Promise 對象和原始類型的值(數值、字符串和布爾值,但這時會自動轉成當即 resolved 的 Promise 對象)。
- 返回值是 Promise。 async函數的返回值是 Promise 對象,進一步說,async函數徹底能夠看做多個異步操做,包裝成的一個 Promise 對象,而await命令就是內部then命令的語法糖。
一、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)
)
複製代碼
二、Promise對象的狀態變化。
async函數返回的 Promise 對象,必須等到內部全部await命令後面的 Promise 對象執行完,纔會發生狀態改變,除非遇到return語句或者拋出錯誤。也就是說,只有async函數內部的異步操做執行完,纔會執行then方法指定的回調函數。
正常狀況下,await命令後面是一個 Promise 對象,返回該對象的結果。若是不是 Promise 對象,就直接返回對應的值。
async function f() {
// 等同於
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
複製代碼
另外一種狀況是,await命令後面是一個thenable對象(即定義then方法的對象),那麼await會將其等同於 Promise 對象。
若是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:出錯了
複製代碼
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()。
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函數內部是繼發執行,外部不受影響。
異步遍歷器的最大的語法特色,就是調用遍歷器的next方法,返回的是一個 Promise 對象。
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
複製代碼
語法上,異步 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命令用於將函數內部的值輸出。
新的class寫法只是讓對象原型的寫法更加清晰、更像面向對象編程的語法 而已。ES6 的類,徹底能夠看做構造函數的另外一種寫法。 事實上,類的全部方法都定義在類的prototype屬性上面。
一、ES6 的類,徹底能夠看做構造函數的另外一種寫法。類自己就指向構造函數。
Point === Point.prototype.constructor // true
複製代碼
二、類的全部方法都定義在類的prototype屬性上面。
三、在類的實例上面調用方法,其實就是調用原型上的方法。
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 的行爲不一致。
constructor方法默認返回實例對象(即this),徹底能夠指定返回另一個對象。類必須使用new調用,不然會報錯。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
複製代碼
與 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
複製代碼
在「類」的內部可使用get和set關鍵字,對某個屬性設置存值函數和取值函數,攔截該屬性的存取行爲。
let methodName = 'getArea';
class Square {
constructor(length) {
// ...
}
[methodName]() {
// ...
}
}
複製代碼
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的注意事項:
一、嚴格模式。類和模塊的內部,默認就是嚴格模式。
二、不存在提高。類不存在變量提高。
三、name屬性老是返回緊跟在class關鍵字後面的類名。
四、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
}
複製代碼
五、 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);
}
// ...
}
複製代碼
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"
複製代碼
這個屬性也能夠定義在類的最頂層,其餘都不變。這種新寫法的好處是,全部實例對象自身的屬性都定義在類的頭部,看上去比較整齊,一眼就能看出這個類有哪些實例屬性。
class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
複製代碼
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
複製代碼
一、 將私有方法移出模塊,由於模塊內部的全部方法都是對外可見的。
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
複製代碼
二、利用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;
}
// ...
};
複製代碼
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
複製代碼
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 調用父類的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 調用父類的toString()
}
}
複製代碼
一、 super關鍵字,它在這裏表示父類的構造函數,用來新建父類的this對象。
二、 子類必須在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);
}
}
複製代碼
三、 ES5 的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面(Parent.apply(this))。ES6 的繼承機制徹底不一樣,實質是先將父類實例對象的屬性和方法,加到this上面(因此必須先調用super方法),而後再用子類的構造函數修改this。
四、 在子類的構造函數中,只有調用super以後,纔可使用this關鍵字,不然會報錯。這是由於子類實例的構建,基於父類實例,只有super方法才能調用父類實例。 5 子類實例對象cp同時是ColorPoint和Point(父類)兩個類的實例,這與 ES5 的行爲徹底一致。
6 父類的靜態方法,也會被子類繼承。
Object.getPrototypeOf(ColorPoint) === Point// true
複製代碼
一、 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
複製代碼
二、 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的實例。
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屬性)的實例。
子類實例的__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"
複製代碼
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)。
export命令用於規定模塊的對外接口,import命令用於輸入其餘模塊提供的功能。 export寫法種類:
一、使用大括號指定所要輸出的一組變量。export {firstName, lastName, year}; 二、直接使用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()
複製代碼
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));
複製代碼
總體加載的寫法: import * from "module"
import * as circle from './circle';
console.log('圓面積:' + circle.area(4));
console.log('圓周長:' + circle.circumference(14));
複製代碼
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
//由於是默認輸出的,因此這時import命令後面,不使用大括號。而且能夠隨意取名。
customName(); // 'foo'
複製代碼
一、下面代碼中,foo函數的函數名foo,在模塊外部是無效的。加載的時候,視同匿名函數加載。
function foo() {
console.log('foo');
}
export default foo;
複製代碼
二、一個模塊只能有一個默認輸出,所以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';
複製代碼
三、由於export default命令的本質是將後面的值,賦給default變量,因此能夠直接將一個值寫在export default以後。
// 正確
export default 42;
// 報錯
export 42;
複製代碼
四、若是想在一條import語句中,同時輸入默認方法(default)和其餘接口,能夠寫成下面這樣。
import _, { each, forEach } from 'lodash';
複製代碼
五、 export default也能夠用來輸出類。
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
複製代碼
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';
複製代碼
// 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方法。
複製代碼
能夠實現動態加載。運行時執行,也就是說,何時運行到這一句,就會加載指定的模塊。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);
});
複製代碼
<script type="module" src="./foo.js"></script>
<!-- 等同於 -->
<script type="module" src="./foo.js" defer></script>
複製代碼
對於外部的模塊腳本(上例是foo.js),有幾點須要注意。
一、 代碼是在模塊做用域之中運行,而不是在全局做用域運行。模塊內部的頂層變量,外部不可見。
二、 模塊腳本自動採用嚴格模式,無論有沒有聲明use strict。
三、 模塊之中,可使用import命令加載其餘模塊(.js後綴不可省略,須要提供絕對 URL 或相對 URL),也可使用export命令輸出對外接口。
四、 模塊之中,頂層的this關鍵字返回undefined,而不是指向window。也就是說,在模塊頂層使用this關鍵字,是無心義的。
五、 同一個模塊若是加載屢次,將只執行一次。
利用頂層的this等於undefined這個語法點,能夠偵測當前代碼是否在 ES6 模塊之中。
const isNotModuleScript = this !== undefined;
複製代碼
一、CommonJS 模塊輸出的是一個值的拷貝,ES6 模塊輸出的是值的引用。
二、 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內部變量的區別:
一、ES6 模塊之中,頂層的this指向undefined;CommonJS 模塊的頂層this指向當前模塊。
二、如下這些頂層變量在 ES6 模塊之中都是不存在的。
// 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();
複製代碼
{
id: '...',
exports: { ... },
loaded: true,
...
}
複製代碼
該對象的id屬性是模塊名,exports屬性是模塊輸出的各個接口,loaded屬性是一個布爾值,表示該模塊的腳本是否執行完畢。其餘還有不少屬性,這裏都省略了。之後須要用到這個模塊的時候,就會到exports屬性上面取值。即便再次執行require命令,也不會再次執行該模塊,而是到緩存之中取值。也就是說,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接着往下執行,直到執行完畢。
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改寫成函數表達式,也會報錯。
- 建議再也不使用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;//對象
複製代碼