JavaScript是橫亙在所有學(xué)習(xí)web前端開發(fā)人員面前的一座大山。很多童鞋一直在要求我能不能講解一季js的零基礎(chǔ)視頻教程,推脫了許久,終于得空開始講解。喜歡的話請努力轉(zhuǎn)發(fā)出去,讓更多的人看到,JavaScript其實一點(diǎn)都不難!
javascript!是一門非常強(qiáng)大的腳本語言,應(yīng)用的范圍非常廣泛,每一個web開發(fā)者學(xué)好javascript也是必須的,本套視頻教程詳細(xì)的講解了javascript各個知識點(diǎn)、關(guān)鍵點(diǎn),其中涉及到高深的函數(shù)概念、原型概念、接口概念、單體概念、更是詳細(xì)的講解了javascript設(shè)計模式。
Part 1:你能再次解釋模塊是什么嗎?
優(yōu)秀的作者會將他的書分為章和節(jié)。同理,優(yōu)秀的程序員能將他的程序劃分為各個模塊。
就像書的章節(jié),模塊就是詞(或代碼,視情況而定)的集群。
好的模塊擁有以下特點(diǎn):不同功能是高度獨(dú)立的,并且它們允許被打亂、移除或在必要時進(jìn)行補(bǔ)充,而不會擾亂系統(tǒng)作為一個整體。
為什么使用模塊?
使用模塊有諸多好處,如利于建立一個擴(kuò)展性強(qiáng)的、互相依賴的代碼庫。而在我看來,其最重要是:
1)可維護(hù)性:根據(jù)定義,模塊是獨(dú)立的。一個設(shè)計良好的模塊意在盡可能減少對代碼庫的依賴,所以它才能單獨(dú)地擴(kuò)展與完善。更新一個從其它代碼段解耦出來的獨(dú)立模塊顯然來得更簡單。
回到書的案例,如果書的某個章節(jié)需要進(jìn)行小改動,而該改動會牽涉到其它所有章節(jié),這無疑是個夢魘。相反,如果每章節(jié)都以某種良好方式進(jìn)行編寫,那么改動某章節(jié),則不會影響其它章節(jié)。
2)命名空間:在 JavaScript 中,如果變量聲明在頂級函數(shù)的作用域外,那么這些變量都是全局的(意味著任何地方都能讀寫它)。因此,造成了常見的“命名空間污染”,從而導(dǎo)致完全無關(guān)的代碼卻共享著全局變量。
無關(guān)的代碼間共享著全局變量是一個嚴(yán)重的 編程禁忌。
我們將在本文后面看到,模塊通過為變量創(chuàng)建一個私有空間,從而避免了命名空間的污染。
3)可重用性:坦誠地講:我們都試過復(fù)制舊項目的代碼到新項目上。例如,我們復(fù)制以前項目的某些功能方法到當(dāng)前項目中。
該做法看似可行,但如果發(fā)現(xiàn)那段代碼有更好的實現(xiàn)方式(即需要改動),那么你就不得不去追溯并更新任何你所粘貼到的任何地方。
這無疑會浪費(fèi)大量的時間。因此可復(fù)用的模塊顯然讓你編碼輕松。
如何整合為模塊?
整合為模塊的方式有很多。下面就看看其中的一些方法:
模塊模式(Module pattern)
模塊模式用于模仿類(由于 JavaScript 并不支持原生的類),以致我們能在單個對象中存儲公有和私有變量與方法——類似于其它編程語言(如 Java 或 Python )中的類的用法。模塊模式不僅允許我們創(chuàng)建公用接口 API(如果我們需要暴露方法時),而且也能在閉包作用域中封裝私有變量和方法。
下面有幾種方式能實現(xiàn)模塊模式(module pattern)。第一個案例中,我將會使用匿名閉包。只需將所有代碼放進(jìn)匿名函數(shù)中,就能幫助我們實現(xiàn)目標(biāo)(記住:在 JavaScript 中,函數(shù)是唯一創(chuàng)建新作用域的方式)。
Example 1:匿名閉包(Anonymous closure)
(function () {
// We keep these variables private inside this closure scope
// 讓這些變量在閉包作用域內(nèi)變?yōu)樗接校ㄍ饨缭L問不到這些變量)。
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return 'Your average grade is ' + total / myGrades.length + '.';
}
var failing = function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;});
return 'You failed ' + failingGrades.length + ' times.';
}
console.log(failing());
}());
// ‘You failed 2 times.’
通過這種結(jié)構(gòu),匿名函數(shù)擁有自身的求值環(huán)境或”閉包“,并立即執(zhí)行它。這就實現(xiàn)了對上級(全局)命名空間的隱藏。
這種方法的好處是:能在函數(shù)內(nèi)使用本地變量,而不會意外地重寫已存在的全局變量。當(dāng)然,你也能獲取全局變量,如:
var global = 'Hello, I am a global variable :)';
(function () {
// We keep these variables private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item}, 0);
return 'Your average grade is ' + total / myGrades.length + '.';
}
var failing = function(){
var failingGrades = myGrades.filter(function(item) {
return item < 70;});
return 'You failed ' + failingGrades.length + ' times.';
}
console.log(failing());
console.log(global);
}());
// 'You failed 2 times.'
// 'Hello, I am a global variable :)'
這里需要注意的是,匿名函數(shù)必須被小括號包裹住,這是因為當(dāng)語句以關(guān)鍵字 function 開頭時,它會被認(rèn)為是一個函數(shù)的聲明語句(記住,JavaScript 中不能擁有未命名的函數(shù)聲明語句)。因此,該括號會創(chuàng)建一個函數(shù)表達(dá)式代替它。欲知詳情,可點(diǎn)擊 這里。
Example 2:全局導(dǎo)入(Global import )
另一個常見的方式是類似于 jQuery 的全局導(dǎo)入(global import)。該方式與上述的匿名閉包相似,特別之處是傳入了一個全局變量作為參數(shù):
(function (globalVariable) {
// Keep this variables private inside this closure scope
var privateFunction = function() {
console.log('Shhhh, this is private!');
}
// Expose the below methods via the globalVariable interface while
// hiding the implementation of the method within the
// function() block
// 通過 globalVariable 接口暴露下面的方法。當(dāng)然,這些方法的實現(xiàn)則隱藏在 function() 塊內(nèi)
globalVariable.each = function(collection, iterator) {
if (Array.isArray(collection)) {
for (var i = 0; i < collection.length; i++) {
iterator(collection[i], i, collection);
}
} else {
for (var key in collection) {
iterator(collection[key], key, collection);
}
}
};
globalVariable.filter = function(collection, test) {
var filtered = [];
globalVariable.each(collection, function(item) {
if (test(item)) {
filtered.push(item);
}
});
return filtered;
};
globalVariable.map = function(collection, iterator) {
var mapped = [];
globalUtils.each(collection, function(value, key, collection) {
mapped.push(iterator(value));
});
return mapped;
};
globalVariable.reduce = function(collection, iterator, accumulator) {
var startingValueMissing = accumulator === undefined;
globalVariable.each(collection, function(item) {
if(startingValueMissing) {
accumulator = item;
startingValueMissing = false;
} else {
accumulator = iterator(accumulator, item);
}
});
return accumulator;
};
}(globalVariable));
在該案例中,globalVariable 是唯一的全局變量。這個相對于匿名閉包的優(yōu)勢是:提前聲明了全局變量,能讓別人更清晰地閱讀你的代碼。
var myGradesCalculate = (function () {
// Keep this variable private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];
// Expose these functions via an interface while hiding
// the implementation of the module within the function() block
return {
average: function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'Your average grade is ' + total / myGrades.length + '.';
},
failing: function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return 'You failed ' + failingGrades.length + ' times.';
}
}
})();
myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
正如你所看到的,該方式讓你決定哪個變量/方法是私有的(如 myGrades),哪個變量/方法是需要暴露出來的(通過將需要暴露出來的變量/方法放在 return 語句中,如 average & failing)。
Example 4: 暴露模塊模式(Revealing module pattern)
這與上一個方法非常類似,只不過該方法確保所有變量和方法都是私有的,除非顯式暴露它們:
var myGradesCalculate = (function () {
// Keep this variable private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];
var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);
return'Your average grade is ' + total / myGrades.length + '.';
};
var failing = function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});
return 'You failed ' + failingGrades.length + ' times.';
};
// Explicitly reveal public pointers to the private functions
// that we want to reveal publicly
return {
average: average,
failing: failing
}
})();
myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'
看似有許多知識需要我們吸收,但這只是模塊模式(module patterns)的冰山一角。在我學(xué)習(xí)這方面知識時,發(fā)現(xiàn)了下面這些有用的資源:
Learning JavaScript Design Patterns: 出自 Addy Osmani,他以極其簡潔的方式對模塊模式進(jìn)行詳細(xì)分析。
Adequately Good by Ben Cherry:一篇通過案例對模塊模式的高級用法進(jìn)行概述的文章。
Blog of Carl Danley:一篇對模塊模式進(jìn)行概述并擁有其它 JavaScript 模式資源的文章。
CommonJS and AMD
上述所有方法都有一個共同點(diǎn):使用一個全局變量將其代碼封裝在一個函數(shù)中,從而利用閉包作用域為自身創(chuàng)建一個私有的命名空間。
雖每種方式都有效,但他們也有消極的一面。
舉個例子說,作為一名開發(fā)者,需要以正確的依賴順序去加載你的文件。更直接地說,假如你在項目中使用 Backbone,那么你需要在文件中用 script 標(biāo)簽引入 Backbone 的源代碼。
然而,由于 Backbone 重度依賴于 Underscore.js,因此 Backbone 的 script 標(biāo)簽不能放在 Underscore 的 script 標(biāo)簽前。
作為一名開發(fā)者,有時會為了正確處理并管理好這種依賴關(guān)系而感到頭痛。
另一個消極部分是:他們?nèi)詴䦟?dǎo)致命名空間污染。例如,兩個模塊擁有同樣的名字,或者一個模塊擁有兩個版本,而且你同時需要他們倆。
所以,你可能會想到:我們能不能設(shè)計一種方法,無須通過全局作用域去請求一個模塊接口呢?
答案是能!
有兩種流行且實現(xiàn)良好的方法:CommonJS 和 AMD。
CommonJS
CommonJS 是一個志愿工作組設(shè)計并實現(xiàn)的 JavaScript 聲明模塊 APIs。
CommonJS 模塊本質(zhì)上是一片可重用的 JavaScript 代碼段,將其以特定對象導(dǎo)出后,其它模塊即可引用它。如果你接觸過 Node.js,那么你應(yīng)該非常熟悉這種格式。
通過 CommonJS,每個 JavaScript 文件保存的模塊都擁有其獨(dú)一無二的模塊上下文(就像封裝在閉包內(nèi))。在此作用域中,我們使用 module.exports 對象導(dǎo)出模塊,然后通過 require 導(dǎo)入它們。
當(dāng)你定義一個 CommonJS 模塊時,代碼類似:
function myModule() {
this.hello = function() {
return 'hello!';
}
this.goodbye = function() {
return 'goodbye!';
}
}
module.exports = myModule;
我們使用特定對象模塊,并將 module.exports 指向我們的函數(shù)。這讓 CommonJS 模塊系統(tǒng)知道我們想導(dǎo)出什么,并讓其它文件能訪問到它。
然后,當(dāng)有人想使用 myModule 時,他們可在文件內(nèi)將其 require 進(jìn)來,如:
var myModule = require('myModule');
var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'
該方法相對于我們先前討論的模塊模式有兩個顯而易見的好處:
避免了全局命名空間的污染
讓依賴關(guān)系更明確
此外,該語法非常緊湊簡單,我個人非常喜歡。
另外需要注意的一點(diǎn)是:CommonJS 采用服務(wù)器優(yōu)先的方式,并采用同步的方式加載模塊。這點(diǎn)很重要,因為如果我們有其它三個模塊需要 require 進(jìn)來的話,這些模塊會被一個接一個地加載。
這種工作方式很適合應(yīng)用在服務(wù)器上。但不幸的是,當(dāng)你將這種方式應(yīng)用在瀏覽器端時,就會出現(xiàn)問題。因為相對于硬盤,從 web 上讀取模塊更耗時(網(wǎng)絡(luò)傳輸?shù)纫蛩兀6遥灰K正在加載,就會阻塞瀏覽器運(yùn)行其它任務(wù)。這是由于 JavaScript 線程會在代碼加載完成前被停止。(在 Part 2 的模塊打包部分,我會告訴你如何解決此問題。而現(xiàn)在,只需了解到這)。
AMD
CommonJS 非常不錯,但如果我們想異步加載模塊呢?答案是異步模塊定義(Asynchronous Module Definition),或簡稱 AMD。
使用 AMD 加載模塊的代碼類似:
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});
define 函數(shù)的第一個參數(shù)是一個包含本模塊所依賴的模塊數(shù)組。這些依賴都在后臺加載(以不阻塞的方式)。加載完成后,define 會調(diào)用其指定的回調(diào)函數(shù)。
接著,回調(diào)函數(shù)會將加載完成后的依賴作為其參數(shù)(一一對應(yīng))——在該案例中,是 myModule 和 myOtherModule。因此,回調(diào)函數(shù)就能使用這些依賴。當(dāng)然,這些依賴本身也需要通過 define 關(guān)鍵字定義。
例如,myModule 類似:
define([], function() {
return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});
與 CommonJS相反,AMD 采取瀏覽器優(yōu)先的方式,通過異步加載的方式完成任務(wù)。(注意,有很多人并不贊成此方式,因為他們堅信在代碼開始運(yùn)行時動態(tài)且逐個地加載文件是不好的。我將會在下一節(jié)的模塊構(gòu)建(module-building)中探討更多相關(guān)信息)。
除了異步外,AMD 的另一個好處是:模塊可以是一個對象、函數(shù)、構(gòu)造函數(shù)、字符串、JSON 或其它各種類型,而 CommonJS 僅支持對象作為模塊。
話雖如此,AMD 不兼容 io、文件系統(tǒng)(filesystem)和其它通過 CommonJS 實現(xiàn)的面向服務(wù)器的功能,而且其通過函數(shù)封裝的語法與簡單的 require 語句相比顯得有點(diǎn)啰嗦。