作者:自成
原文地址:凹凸實(shí)驗(yàn)室(http://aotu.io/notes/2016/03/31/readable/)
編程是一門藝術(shù)活闪金,好的代碼應(yīng)該就像住的房子一樣捣染,有整體的框架,有門买鸽,有窗戶描沟,相互獨(dú)立又完美組合。你覺(jué)得門不夠結(jié)實(shí)钾虐,就拆下來(lái)?yè)Q個(gè)實(shí)心的读跷;你覺(jué)得窗戶不夠明亮就換個(gè)全玻璃的,總之對(duì)房子的其他部位沒(méi)有任何影響禾唁。所以說(shuō)每一個(gè)程序員都應(yīng)該有一顆設(shè)計(jì)師的心效览。本文主要從編碼、變量荡短、處理錯(cuò)誤丐枉、對(duì)象等基礎(chǔ)方面進(jìn)行簡(jiǎn)單的探討,希望能對(duì)大家的工作有所幫助掘托。
1 編碼風(fēng)格
老生常談瘦锹,我們先從最基礎(chǔ)的編碼說(shuō)起吧!好的編碼規(guī)范不僅僅能夠提升代碼的可讀性與可維護(hù)性闪盔,提高團(tuán)隊(duì)的工作效率弯院,也能夠避開(kāi)一些低級(jí)的錯(cuò)誤,減少bug的隱患泪掀,提升程序員的自我修養(yǎng)听绳。編碼雖小,但卻是萬(wàn)丈高樓的基礎(chǔ)异赫,對(duì)于編寫清晰連貫的代碼來(lái)說(shuō)椅挣,每一個(gè)字符都是非常重要的。以下部分編碼規(guī)范參考自凹凸實(shí)驗(yàn)室塔拳。
1.1 縮進(jìn)
通常使用四個(gè)空格進(jìn)行代碼縮進(jìn)鼠证,有些也用tab來(lái)縮進(jìn),這主要根據(jù)團(tuán)隊(duì)的風(fēng)格跟個(gè)人喜好靠抑。
1.2 空格
- 左括號(hào)與類名之間一個(gè)空格
- 冒號(hào)與屬性值之間一個(gè)空格
- 操作符前后
- 匿名函數(shù)表達(dá)式之后等
1.3 空行
這是一個(gè)容易被大家忽略的點(diǎn)量九,但它所帶來(lái)的效果是毋庸置疑的!通常一段代碼的語(yǔ)義和另一段代碼不相關(guān)颂碧,就應(yīng)該用空行隔開(kāi)荠列,避免一大段的代碼揉在一起,比如:
- 在方法之間稚伍;
- 方法中的局部變量和第一條語(yǔ)句之間弯予;
- 注釋之前;
- 方法內(nèi)的邏輯片段之間个曙。
1.4 命名約定
有一位大師曾說(shuō)過(guò)锈嫩,計(jì)算機(jī)科學(xué)只存在兩個(gè)難題:緩存和命名受楼。由此可見(jiàn)命名不僅是一門科學(xué),也是一門技術(shù)呼寸。通常情況下艳汽,變量與函數(shù)一般使用駝峰大小寫命名法,其中為了區(qū)分變量與函數(shù)对雪,變量命名前綴應(yīng)當(dāng)是名詞河狐,函數(shù)前綴應(yīng)當(dāng)是動(dòng)詞,也就是說(shuō)我們應(yīng)當(dāng)讓命名承載一定的含義瑟捣,因此要避免使用沒(méi)有意義的命名馋艺。
1.4 注釋
通常我們?cè)诰帉懲暌欢未a的短時(shí)間內(nèi),會(huì)清楚這段代碼的工作原理迈套。但是當(dāng)過(guò)一段時(shí)間再次回到代碼中捐祠,可能會(huì)花很長(zhǎng)的時(shí)間才能讀懂。這種情況下桑李,編寫注釋就變得尤為重要了踱蛀。
2 變量
首先說(shuō)一說(shuō)全局變量存在哪些的問(wèn)題吧!命名沖突贵白、測(cè)試難度大率拒、深耦合等等。在創(chuàng)建變量的時(shí)候禁荒,我們應(yīng)該注意以下幾個(gè)方面:
2.1 避免隱性的創(chuàng)建全局變量
什么是隱性的全局變量呢猬膨?官方的回答是:任何變量,如果未經(jīng)聲明圈浇,就為全局對(duì)象所有寥掐。啥意思呢?其實(shí)就是沒(méi)有加var
聲明的磷蜀,請(qǐng)看下面的例子:
function obj() {
name = "aotu";
return name;
}
另外一種容易創(chuàng)建隱形全局變量的情況就是var
聲明的鏈?zhǔn)劫x值,如下代碼所示:
function person() {
var a = b = 1;
}
以上這段代碼的執(zhí)行結(jié)果是:a
是局部變量百炬,b
是全局變量褐隆,主要原因是從右至左的操作符優(yōu)先級(jí),它實(shí)際執(zhí)行的結(jié)果等同于:
var a = ( b = 0 );
綜上所述剖踊,隱式全局變量并不是我們平時(shí)用var聲明的變量庶弃,而是全局對(duì)象的屬性,既然是屬性德澈,那么它可以通過(guò)delete
操作符刪除歇攻,但變量不可以,且在ES5 strict以上會(huì)拋出錯(cuò)誤梆造。
2.2 在函數(shù)頂部聲明變量
在javascript中缴守,聲明變量有一個(gè)“提升”的概念,即無(wú)論在函數(shù)哪里聲明,效果都等同于在函數(shù)頂部進(jìn)行聲明屡穗。所以我們統(tǒng)一把變量在函數(shù)頂部聲明贴捡,既有利于可讀性與可維護(hù)行,也不易出錯(cuò)村砂。
2.3 使用單一var模式
var a = 1,
b = 1,
c = 1;
這樣聲明的變量不僅可讀性好烂斋,而且可以防止變量在定義前就被使用的邏輯錯(cuò)誤,且編碼更少础废。
2.4 單全局變量方式
雖然全局變量的容易污染命名空間汛骂,但有些功能的需要,難以避免使用评腺,關(guān)鍵是我們應(yīng)該做到避免全局變量超出我們的掌控香缺,最佳的方法是依賴盡可能少的全局變量。我們可以使用單全局變量的方式來(lái)開(kāi)啟我們的項(xiàng)目歇僧,這種方式在許多的javascript類庫(kù)中都有這樣使用图张。如jQuery,它定義了兩個(gè)全局變量$
和jQuery
诈悍。
3 UI松耦合
什么是松耦合祸轮?當(dāng)修改一個(gè)組件的邏輯,而對(duì)另一個(gè)組件沒(méi)有影響侥钳,就說(shuō)這叫松耦合适袜。通常一個(gè)大型的web應(yīng)用,都是由多人共同開(kāi)發(fā)維護(hù)舷夺,這時(shí)候松耦合顯得至關(guān)重要苦酱,假如你修改了某一處的代碼而影響了團(tuán)隊(duì)其他人的功能,這是非常不友好的给猾。通常我們主要注意以下幾點(diǎn):
- 將javascript從css中抽離疫萤,如:避免使用css表達(dá)式。
- 將css從javascript中抽離敢伸,如:避免使用javascript直接修改css扯饶,最佳的方法是操作css的
className
; - 將javascript從HTML中抽離,如:避免將函數(shù)直接嵌入到html執(zhí)行池颈,我們應(yīng)該盡量做到將所有的js代碼都放入外置文件中尾序,確保html中不會(huì)有內(nèi)聯(lián)的js代碼。
- 將html從javascript中抽離躯砰,如避免在js中拼接html結(jié)構(gòu)每币,我們可以用模板引擎,也可以使用Vue琢歇、React等兰怠。
4 錯(cuò)誤處理
4.1 為什么要拋出錯(cuò)誤梦鉴?
在javascript開(kāi)發(fā)中,總是會(huì)悄無(wú)聲息的出現(xiàn)一些超出我們預(yù)期的痕慢,攜帶的信息稀少的尚揣,隱晦含糊的bug,讓我們措手不及掖举,大大增加了我們調(diào)試錯(cuò)誤快骗、定位錯(cuò)誤的難度,影響開(kāi)發(fā)效率塔次。假設(shè)錯(cuò)誤中包含這樣的信息:“由于某某情況方篮,導(dǎo)致某某函數(shù)執(zhí)行錯(cuò)誤”,那么励负,是不是馬上就可以開(kāi)始調(diào)試藕溅,而不用花大量的時(shí)候去定位錯(cuò)誤?
4.2 何時(shí)拋出錯(cuò)誤?
主要是辨識(shí)代碼中哪些部分继榆,在特定的情況下最后可能導(dǎo)致錯(cuò)誤巾表。這里的錯(cuò)誤,通常都是我們?cè)谒伎嫉倪^(guò)程中的一些可預(yù)期的錯(cuò)誤略吨。
4.3 怎樣拋出錯(cuò)誤集币?
4.3.1 使用try-catch
將可能引發(fā)錯(cuò)誤的代碼放在try塊中,處理錯(cuò)誤的代碼放在catch中翠忠,如:
try {
someMethod();
} catch (ex) {
catchError(ex);
}
也可以增加一個(gè)finally塊鞠苟,這里需注意的是:finally塊中的代碼塊不管是否有錯(cuò)誤發(fā)生,最后都會(huì)被執(zhí)行秽之。
4.3.2 throw
當(dāng)我們能清晰的捕捉到錯(cuò)誤的時(shí)候当娱,最好的做法就是拋出這個(gè)錯(cuò)誤店雅,避免在不經(jīng)意的時(shí)候又遇到它疑苫,讓大家尷尬航唆。這里需注意的是割择,當(dāng)遇到throw操作符時(shí),代碼會(huì)立即停止執(zhí)行:
throw new Error("method(): descdescdesc");
也可以自定義一個(gè)錯(cuò)誤類型捂敌⌒费荩總之阴颖,就是盡可能用最短的字符描述清楚:
throw {
name: "myErrorType",
message: "arguments must be a DOM element",
errorMethod: errorMethod
}
5 創(chuàng)建對(duì)象
5.1 對(duì)象字面量
所謂的對(duì)象字面量其實(shí)就是我們通常所說(shuō)的鍵值對(duì)哈希表愤诱,這種方式不僅富有表現(xiàn)力,可讀性好捐友,且字符更短淫半,沒(méi)有作用域解析。它的語(yǔ)法規(guī)則如下:
- 對(duì)象包裝在大括號(hào)中
- 逗號(hào)分隔屬性和方法
- 用冒號(hào)分隔屬性名稱和屬性的值
var obj = {
name: "aotu",
job: "farmer",
getName: function () {
return this.name;
}
}
//調(diào)用方式
obj.getName();
實(shí)現(xiàn)私有屬性
以上例子的name
匣砖、job
屬性都是可直接訪問(wèn)的科吭。有些時(shí)候我們可能想實(shí)現(xiàn)一些私有的屬性昏滴,然后提供一個(gè)公有的接口來(lái)對(duì)外訪問(wèn)。雖然javascript并沒(méi)有特殊的語(yǔ)法來(lái)表示私有对人、公共屬性和方法谣殊,但是可以通過(guò)匿名閉包來(lái)實(shí)現(xiàn),內(nèi)部的任意變量都不會(huì)暴露牺弄,來(lái)看以下代碼:
var obj;
(function () {
//這樣就能實(shí)現(xiàn)私有成員
var name = "aotu",
job = "farmer";
obj = {
getName: function () {
return name;
}
}
}())
更優(yōu)雅的寫法:
var obj = (function () {
var name = "aotu",
job = "farmer";
return {
getName: function () {
return name;
}
}
}());
這種寫法也是模塊模式的基礎(chǔ)框架姻几,后續(xù)會(huì)有詳細(xì)介紹。
熟悉了這種模式之后它還有很多種玩法势告,比如可以像jQuery這樣鏈?zhǔn)秸{(diào)用:“$(‘#id’).siblings(‘ul’).find(“l(fā)i”).addClass();
var obj = {
num: 0,
add: function (arg) {
this.num += arg;
return this;
},
red: function (arg) {
this.num -= arg;
return this;
},
setTotal: function () {
console.log(this.num);
}
};
//調(diào)用方式
obj.add(5).red(2).setTotal(); //3
5.2 構(gòu)造函數(shù)
我們先來(lái)看看構(gòu)造函數(shù)的基礎(chǔ)框架:
function Obj() {
//公有屬性
this.name = "aotu";
this.job = "farmer";
//公有方法
this.getName = function () {
console.log(this.name);
}
}
//調(diào)用方式
var obj = new Obj();
obj.getName();
在使用new方式實(shí)例化構(gòu)造函數(shù)蛇捌,通常會(huì)經(jīng)歷以下幾個(gè)步驟:
- 創(chuàng)建一個(gè)對(duì)象并且this變量引用了該對(duì)象,且繼承了該對(duì)象的原型咱台。
- 屬性和方法被加入到this引用的對(duì)象中络拌。
- 隱式的返回新對(duì)象。
忘記使用NEW的情況
當(dāng)然回溺,我們有時(shí)候會(huì)忘記使用new操作符實(shí)例化的情況春贸,然而這并不會(huì)導(dǎo)致語(yǔ)法錯(cuò)誤,但構(gòu)造函數(shù)的this指向了全局對(duì)象遗遵,可能會(huì)發(fā)生邏輯錯(cuò)誤或者意外萍恕,來(lái)看下面執(zhí)行的結(jié)果:
var obj = Obj();
obj.getName(); //Cannot read property 'getInfo' of undefined
為了避免這種意外發(fā)生,我們也可以在構(gòu)造函數(shù)中檢查this是否為構(gòu)造函數(shù)的一個(gè)實(shí)例瓮恭,強(qiáng)制使用new操作符雄坪,繼續(xù)看下面的例子:
function Obj() {
if(!(this instanceof Obj)){
return new Obj();
}
this.name = "aotu";
this.age = 25;
this.getName = function () {
console.log(this.name);
}
}
再看執(zhí)行的結(jié)果:
var obj = Obj();
obj.getName(); //"aotu"
靜態(tài)成員
在javascript中,并沒(méi)有特殊的語(yǔ)法來(lái)表示靜態(tài)成員屯蹦,但我們可以為構(gòu)造函數(shù)添加屬性這種方式來(lái)實(shí)現(xiàn)這種語(yǔ)法维哈,請(qǐng)看下面的例子:
//構(gòu)造函數(shù)
function Obj() {}
//添加靜態(tài)方法
Obj.getAge = function () {
console.log(25);
}
//注意這里的調(diào)用方式
Obj.getAge(); //25
//如果使用實(shí)例對(duì)象調(diào)用
obj.getAge(); //Object #<Obj> has no method 'getAge'
這里大家需要注意調(diào)用靜態(tài)方法的方式,若以實(shí)例對(duì)象調(diào)用一個(gè)靜態(tài)方法是無(wú)法正常運(yùn)行的登澜,反之同理阔挠。
私有屬性與方法
在以上例子中,構(gòu)造函數(shù)的屬性與方法都屬于公有方法脑蠕,我們也可以給構(gòu)造函數(shù)添加私有方法與私有屬性:
function Obj() {
this.name = "auto";
this.age = 25;
//私有屬性
var address = "sz",
that = this;
//私有方法
function getAddress() {
console.log(that.address);
}
this.getName = function () {
console.log(this.name);
}
}
構(gòu)造函數(shù)存在的問(wèn)題
構(gòu)造函數(shù)的主要問(wèn)題就是當(dāng)多次實(shí)例化這個(gè)構(gòu)造函數(shù)的時(shí)候购撼,每個(gè)方法都會(huì)重新創(chuàng)建一遍,這樣就等于在內(nèi)存中的拷貝谴仙。解決問(wèn)題的第一種思路迂求,就是將函數(shù)中的方法通過(guò)函數(shù)定義轉(zhuǎn)移到函數(shù)外面,并將指針傳遞給構(gòu)造函數(shù)晃跺,來(lái)看下面的例子:
function Obj() {
this.name = "aotu";
this.age = 25;
//將指針賦給getName
this.getName = getName;
}
function getName () {
console.log(this.name);
}
var obj1 = new Obj();
var obj2 = new Obj()
雖然也解決了以上的問(wèn)題揩局,但并沒(méi)有達(dá)到封裝的效果。接下來(lái)我們引入原型prototype的概念掀虎。
5.3 原型模式
每一個(gè)構(gòu)造函數(shù)都有一個(gè)原型prototype凌盯,原型對(duì)象包含一個(gè)指向構(gòu)造函數(shù)的指針付枫,這個(gè)指針指向一個(gè)可以由特定類型的所有實(shí)例共享的屬性和方法,所以使用原型對(duì)象可以讓所有對(duì)象實(shí)例共享它的屬性和方法驰怎,來(lái)看下面的例子:
function Obj() {}
Obj.prototype.name = "aotu";
Obj.prototype.age = 25;
Obj.prototype.getName = function () {
console.log(this.name);
}
//調(diào)用方式
var obj1 = new Obj();
obj1.getName() //"aotu"
var obj2 = new Obj();
obj2.getName() //"aotu"
alert(obj1.getName == obj2.getName); //true
由此可見(jiàn)obj1
和obj2
訪問(wèn)的是同一個(gè)getName
函數(shù)阐滩。
更好的寫法
我們可以將所有的原型都寫在一個(gè)對(duì)象字面量里,這樣整個(gè)代碼看起來(lái)更加簡(jiǎn)潔清晰县忌,繼續(xù)往下看:
function Obj() {}
Obj.prototype = {
name: "aotu",
age: 25,
getName: function () {
return this.name;
}
}
使用字面量的方式需注意的問(wèn)題
在使用這種字面量的方式的時(shí)候掂榔,需注意以下兩點(diǎn):
1.將prototype設(shè)置為等于一個(gè)對(duì)象字面量形式創(chuàng)建的對(duì)象,它本質(zhì)上已經(jīng)完全重寫了默認(rèn)的prototype
對(duì)象芹枷,最終結(jié)果雖然相同衅疙,但是其constructor
屬性不再指向該對(duì)象。
constructor是個(gè)什么鬼鸳慈?在默認(rèn)情況下饱溢,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)constructor,它指向prototype
屬性所在函數(shù)的指針走芋,換句話說(shuō)這個(gè)constructor就是指這個(gè)構(gòu)造函數(shù)绩郎。以上代碼執(zhí)行結(jié)果如下所示:
var obj= new Obj();
alert(obj.cnstructor == Obj) //false;
我們可以在重寫prototype的時(shí)候給constructor指定構(gòu)造函數(shù),接著往下看:
function Obj(){}
Obj.prototype = {
constructor: Obj,
name: "aotu",
age: 25,
getName: function () {
return this.name;
}
}
var obj= new Obj();
alert(obj.cnstructor == Obj) //true;
2.當(dāng)我們重寫整個(gè)原型的時(shí)候如果先創(chuàng)建了實(shí)例翁逞,就會(huì)切斷構(gòu)造函數(shù)與原型之間的聯(lián)系肋杖,因?yàn)?strong>實(shí)例的指針僅僅指向原型,而不是構(gòu)造函數(shù)挖函,在實(shí)際的操作過(guò)程中状植,應(yīng)該盡量避免這種錯(cuò)誤。
function Obj() { }
var obj = new Obj();
Obj.prototype = {
constructor: Obj,
name: "aotu",
age: 25,
getName: function () {
return this.name;
}
}
obj.getName(); //error
組合使用二者
在我們的具體應(yīng)用中怨喘,通常比較多的是組合使用構(gòu)造函數(shù)模式與原型模式津畸。構(gòu)造函數(shù)用于定義實(shí)例屬性,原型用于定于共享的屬性和方法必怜,這樣能夠最大限度的節(jié)省內(nèi)存肉拓。以下是一個(gè)基本的組合使用構(gòu)造函數(shù)與原型的例子:
function Obj(){
if(!(this instanceof Obj)){
return new Obj();
}
this.name = "aotu";
this.age = 25;
}
Obj.prototype = {
constructor: Obj,
getName: function () {
return this.name;
}
}
var obj = Obj();
obj.getName();
5.4 模塊模式
模塊模式是一種非常通用的模式,也是使用頻率比較高的模式梳庆,它具有以下幾個(gè)特點(diǎn):
- 模塊化
- 可復(fù)用
- 松耦合
- 區(qū)分了私有方法與公共方法
我們先看看模塊模式的基礎(chǔ)框架:
var testModule = function () {
//私有成員
var testNode = document.getElementById("test");
//也可在此定義私有方法
function privateMethod() {
console.log("this is Private method!");
}
return {
//對(duì)外公開(kāi)的方法
setHtml: function (txt) {
testNode.innerHTML = txt;
}
}
}
//調(diào)用方式
var testModule = new testModule();
testModule.setHtml("Hello");
這種方式看起來(lái)比較清晰暖途、簡(jiǎn)潔。但就是每次調(diào)用的時(shí)候都需要用new來(lái)實(shí)例化膏执。我們知道每個(gè)實(shí)例在內(nèi)存里都是一份拷貝驻售,如何解決這個(gè)問(wèn)題呢?我們可以采用一個(gè)匿名閉包來(lái)完美的解決這個(gè)問(wèn)題更米。
(function () {
//將所有的變量和function放在這里聲明芋浮,其作用域也只能在這個(gè)匿名閉包里面,既達(dá)到了封裝的目的,也能防止命名沖突
}())
接下來(lái)我們將它應(yīng)用到具體的實(shí)例中壳快,以下就是一個(gè)基本的Module模式:
var testModule =(function () {
var my = {},
testNode = document.getElementById("test");
my.setHtml = function(txt) {
testNode.innerHTML = txt;
}
return my;
} ())
//調(diào)用方式
testModule.setHtml("Hello");
通常在一個(gè)大型的項(xiàng)目中纸巷,會(huì)有多人共同開(kāi)發(fā)一個(gè)功能的情況,這個(gè)時(shí)候我們可以運(yùn)用這種模式將全局變量當(dāng)作參數(shù)傳遞眶痰,然后通過(guò)變量返回瘤旨,從而達(dá)到多人協(xié)作的目的。
var testModule =(function (my) {
var testNode = document.getElementById("test");
my.setHtml = function(txt) {
testNode.innerHTML = txt;
}
return my;
} (testModule || {}))
我們也可以通過(guò)這個(gè)模式將私有的對(duì)象或者屬性保護(hù)起來(lái)竖伯,然后設(shè)置一些公共接口對(duì)外訪問(wèn)存哲,繼續(xù)來(lái)看下面的代碼:
var testModule =(function () {
var testNode = document.getElementById("test"),
setHtml = function(txt) {
testNode.innerHTML = txt;
};
//設(shè)置公共調(diào)用方法
return {
setHtml: setHtml
}
} ())
以上幾種方式僅僅只是一些創(chuàng)建對(duì)象的基礎(chǔ),通過(guò)靈活運(yùn)用這些基礎(chǔ)七婴,可以變換出傳說(shuō)中各種各樣的模式祟偷,如迭代器模式、工廠模式打厘、裝飾者模式等修肠。對(duì)于后續(xù)學(xué)習(xí)其他的技術(shù)也是極有幫助的。如React:
var MyTitle = React.createClass({
getDefaultProps : function () {
return {
title : 'Hello World'
};
},
render: function() {
return <h1> {this.props.title} </h1>;
}
});
Vue:
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!'
},
methods: {
reverseMessage: function () {
this.message = this.message.split('').reverse().join('')
}
}
})
以上就是本期的所有內(nèi)容户盯,如有錯(cuò)漏嵌施,懇請(qǐng)指正,大家共同進(jìn)步莽鸭!在下一期中吗伤,會(huì)繼續(xù)跟大家探討更多好玩的東西,敬請(qǐng)期待硫眨。
6 參考資料
- 《編寫可維護(hù)的JavaScript》[美] Nicholas C. Zakas 著
- 《JavaScript設(shè)計(jì)模式》[美] Addy Osmani 著
- 《JavaScript高級(jí)程序設(shè)計(jì)(第3版)》
- 博文:深入理解JavaScript系列