模塊
The old way
傳統(tǒng)JavaScript有相當多的模塊化方案(AMD,CommonJS,UMD等)。大體都是通過一個外部函數(shù)返回一個攜帶各種閉包的對象,達到將公用的API暴露出來的目的。類似這樣
function Hello(name) {
function greeting() {
console.log( "Hello " + name + "!" );
}
// public API
return {
greeting: greeting
};
}
var me = Hello( "Kyle" );
me.greeting(); // Hello Kyle!
之所以會誕生出這么多方案,無疑是需求跑在了前面,而語法不直接支持這些。于是乎ES6吸收了一些C系語言的思想,直接支持模塊化。
Before the new way
在具體介紹語法之前,來看看ES6模塊化和以往有何不同
- ES6模塊化是基于文件的,一個文件只能有一個模塊;
ES6 modules are file-based, meaning one module per file. At this time, there is no standardized way of combining multiple modules into a single file.(ES6模塊化是基于文件的,意味著每一個文件只有一個模塊。與此同時,沒有一個將多個模塊合并在同一個文件內(nèi)的標準化的方式)
- ES6模塊化是單例的;
Every time you import that module into another module, you get a reference to the one centralized instance. If you want to be able to produce multiple module instances, your module will need to provide some sort of factory to do it.(每次引用一個模塊,實際上只是獲取了一個中心實例的引用。如果你想生成多個模塊實例,你的模塊需要提供某種工廠模式來實現(xiàn))
The new way
exports & import
一個模塊包含多個獨立的子功能,如果需要公開相應(yīng)的功能模塊,直接用export
關(guān)鍵字即可,來看一個例子:
// exports.js
export function foo(canvas, options) {
//可以導出function
}
export class bar {
//可以導出class
}
export {foo,bar} //可以是Object
導入模塊
import {foo, bar} from "kittydar.js";
//重命名
import {foo as fooRename} from "kittydar.js";
import {bar as barRename} from "kittydar.js";
export default
ES6完全支持
import
語句從歷史項目AMD或CommonJS模塊化方案中導入模塊。
來看代碼
//傳統(tǒng)CommonJS寫法
module.export = {
field1: value1,
field2: function(){
//implements
}
}
//ES6寫法
//exportDefault.js
export default {
field1: value1,
field2: function(){
//implements
}
};
兩者幾乎等價,這意味著export輸出模塊本質(zhì)上和CommonJS模塊化方案沒什么區(qū)別,ES6完全可以從歷史代碼引入模塊,減少代碼遷移的痛苦。
import expDef from 'exportDefault.js';
import $ from 'jQuery.js';
import _ from 'lodash'
循環(huán)依賴
A依賴B,B依賴A,怎么做到不出問題呢?
《You Don't Know JS ES6》書中給出了一個例子:
//Module A
import bar from "B";
export default function foo(x) {
if(x>10)return bar(x-1);
return x * 2;
}
//Module B
import foo from "A";
export default function bar(y) {
if(y>5)return foo(y/2);
return y * 3;
}
我們來試想一下,foo
和bar
兩個函數(shù)在同一個函數(shù)作用域內(nèi),運行起來自然是沒問題的,然而在模塊化環(huán)境下,ES6則需要做一些額外的工作才能使得如此循環(huán)依賴生效。但是,怎么做到呢?
In essence, the mutual imports, along with the static verification that’s done to validate both import statements, virtually composes the two separate module scopes (via the bindings), such that foo(..) can call bar(..) and vice versa. This is symmetric to if they had originally been declared in the same scope.
大概意思是本質(zhì)來說,實際上這樣的循環(huán)依賴將兩個模塊(A和B)的函數(shù)作用域虛擬地聯(lián)合在了一起,如此一來foo可以調(diào)用bar,bar也可以調(diào)用foo。這和將foo和bar兩個函數(shù)聲明在同一個函數(shù)作用域的實際效果是一樣的。
模塊對象 & 聚合模塊
使用import *
實際上導出的是模塊命名空間對象,將所有模塊的屬性全部導出。
import * as cows from "cows";
cows.moon();
聚合模塊又是什么呢?很好理解,將其他細小模塊整理起來,在同一個模塊里聚合,并導出。類似于先import...from了模塊,然后再將其export,這樣的操作無非就是把兩步合成一步來做。
// 導入"sri-lanka"并將它導出的內(nèi)容的一部分重新導出
export {Tea, Cinnamon} from "sri-lanka";
// 導入"equatorial-guinea"并將它導出的內(nèi)容的一部分重新導出
export {Coffee, Cocoa} from "equatorial-guinea";
// 導入"singapore"并將它導出的內(nèi)容全部導出
export * from "singapore";
Classes 類
墻裂聲明:此類非彼類,JavaScript中的Classes和其他語言Classes雖然類似,但有本質(zhì)的區(qū)別,而JS中所謂的Classes只不過是語法糖。
實現(xiàn)原理
At the heart of the new ES6 class mechanism is the class keyword, which identifies a block where the contents define the members of a function’s prototype.(class機制核心在于class關(guān)鍵字,這標識了一個定義了函數(shù)原型上面的成員的語句塊)
有點拗口,直接來個例子:
class Foo {
constructor(a,b) {
this.x = a;
this.y = b;
}
gimmeXY() {
return this.x * this.y;
}
}
//等價于
function Foo(a,b) {
this.x = a;
this.y = b;
}
Foo.prototype.gimmeXY = function() {
return this.x * this.y;
}
可以這么說,你所看到的class的內(nèi)容,實際上就是該函數(shù)的原型對象(Prototype Object)本身。
我們趁機來復習一下JavaScript的Prototype和Constructor吧。
每一個函數(shù)都包含一個prototype屬性,這個屬性是指向一個對象的引用,這個對象稱之為“原型對象”(prototype object)。每一個函數(shù)包含不同的原型對象。當將函數(shù)用作構(gòu)造函數(shù)的時候,新創(chuàng)建的對象會從原型對象上繼承屬性。
在JavaScript中,類的實現(xiàn)是基于其原型繼承機制的。如果兩個實例都從同一個原型對象上繼承了屬性,我們說他們是同一個類的實例。
如果兩個對象繼承自同一個原型,往往意味著(但不是絕對)他們是由同一個構(gòu)造函數(shù)創(chuàng)建并初始化的
構(gòu)造函數(shù)是用來初始化新創(chuàng)建的對象的。使用new調(diào)用構(gòu)造函數(shù)會自動創(chuàng)建一個新對象,因此構(gòu)造函數(shù)本身只需初始化這個新對象的狀態(tài)即可。調(diào)用構(gòu)造函數(shù)的一個重要特征是,構(gòu)造函數(shù)的prototye屬性被用作新對象的原型。這意味著通過同一個構(gòu)造函數(shù)創(chuàng)建的所有對象都繼承自一個相同的對象,因此他們是同一個類的成員。
---引自《大犀牛》
認真看看下面的Demo
function Foo(a,b) {
this.x = a;
this.y = b;
console.log('Foo is constructor');
return 'ok';
}
function Bar(a,b) {
this.x = a;
this.y = b;
console.log('Bar is constructor');
return 'ok';
}
Foo.prototype.getXYmutiply = function() {
return this.x * this.y;
}
//通過原型對象傳遞實現(xiàn)方法和屬性的繼承
Bar.prototype = Foo.prototype;//原型對象傳遞(繼承)
var bar = new Bar(5,4);//Bar is constructor
var foo = new Foo(5,6);//Foo is constructor
/** 此時 bar 和 foo 是同一個原型對象 **/
console.log(Bar.prototype === Foo.prototype);//true
console.log(Bar.prototype.constructor);//function Foo(){[....]}
Bar.prototype.constructor = Bar;
bar = new Bar(5,4);//Bar is constructor
console.log(Bar.prototype.constructor);//function Bar(){[....]}
console.log(bar.getXYmutiply());//20
所以我們很清楚,ES6所謂的Classes特性只是在ES5基礎(chǔ)上做了美化,用貼近其他語言語法(Java)的形式,模擬出其他語言有的面向?qū)ο蟮奶匦裕ɡ^承,多態(tài),封裝),使得代碼看起來更加明確易懂。
extends & super
接著上面的Foo Bar例子
class Foo {
constructor(a,b) {
this.x = a;
this.y = b;
}
gimmeXY() {
return this.x * this.y;
}
}
class Bar extends Foo {
constructor(a,b,c) {
super( a, b );
this.z = c;
}
gimmeXYZ() {
return super.gimmeXY() * this.z;
}
}
var b = new Bar(5,15,25);
b.x; // 5
b.y; // 15
b.z; // 25
b.gimmeXY();//75
b.gimmeXYZ(); // 1875
仔細想想,在pre-ES6的時代,要模擬面向?qū)ο笫且患艿疤鄣氖虑椤J紫任覀兪熘嫦驅(qū)ο笕齻€重要特征,繼承、多態(tài)、封裝。尤其是繼承,傳統(tǒng)的做法都是通過父類將原型對象賦值給子類原型對象實現(xiàn)的,與此同時還要注意,在原型對象賦值以后,需要更正子類原型對象中的構(gòu)造方法,blabla...特別啰嗦。ES6這次索性把以上我們說的一整套流程固定成了幾個語法糖,在明確語義的同時,減少了重復啰嗦的代碼實現(xiàn)。
再來看一組代碼對照,一切都會很清晰了。
function Foo() {
this.a = 1;
}
function Bar() {
this.b = 2;
Foo.call( this );
}
// `Bar` "extends" `Foo`
Bar.prototype = Object.create( Foo.prototype );
//等價于
class Foo {
constructor() {
this.a = 1;
}
}
class Bar extends Foo {
constructor() {
this.b = 2;
super();
}
}
new.target & static
老規(guī)矩,先看代碼
class Foo {
static answer = 42;
static cool() {
console.log( "cool" );
}
// ..
}
class Bar extends Foo {
constructor() {
console.log( new.target.answer );
}
}
Foo.answer;// 42
Bar.answer;// 42
var b = new Bar();// 42
b.cool();// "cool"
b.answer;// undefined -- `answer` is static on `Foo`
Be careful not to get confused that static members are on the class’s prototype chain. They’re actually on the dual/parallel chain between the function constructors.(靜態(tài)成員并不在類的原型鏈里,而是存在于兩個函數(shù)間的構(gòu)造方法內(nèi))
如上所說,離開了構(gòu)造方法,使用new.target
是無效的。
模塊化編程部分暫時介紹到這里,如果遇到什么沒介紹到的,我會及時補充進來。