程式設計是一門藝術活,好的程式碼應該就像住的房子一樣,有整體的框架,有門,有窗戶,相互獨立又完美組合。你覺得門不夠結實,就拆下來換個實心的;你覺得窗戶不夠明亮就換個全玻璃的,總之對房子的其他部位沒有任何影響。所以說每一個程式設計師都應該有一顆設計師的心。本文主要從編碼、變數、處理錯誤、物件等基礎方面進行簡單的探討,希望能對大家的工作有所幫助~~
1 編碼風格
老生常談,我們先從最基礎的編碼說起吧!好的編碼規範不僅僅能夠提升程式碼的可讀性與可維護性,提高團隊的工作效率,也能夠避開一些低階的錯誤,減少bug的隱患,提升程式設計師的自我修養。編碼雖小,但卻是萬丈高樓的基礎,對於編寫清晰連貫的程式碼來說,每一個字元都是非常重要的。以下部分編碼規範參考自凹凸實驗室。
1.1 縮排
通常使用四個空格進行程式碼縮排,有些也用tab來縮排,這主要根據團隊的風格跟個人喜好
1.2 空格
- 左括號與類名之間一個空格
- 冒號與屬性值之間一個空格
- 操作符前後
- 匿名函式表示式之後等
1.3 空行
這是一個容易被大家忽略的點,但它所帶來的效果是毋庸置疑的!通常一段程式碼的語義和另一段程式碼不相關,就應該用空行隔開,避免一大段的程式碼揉在一起,比如
- 在方法之間;
- 方法中的區域性變數和第一條語句之間;
- 註釋之前
- 方法內的邏輯片段之間
1.4 命名約定
有一位大師曾說過,電腦科學只存在兩個難題:快取和命名。由此可見命名不僅是一門科學,也是一門技術。
通常情況下,變數與函式一般使用駝峰大小寫命名法,其中為了區分變數與函式,變數命名字首應當是名詞,函式字首應當是動詞,也就是說我們應當讓命名承載一定的含義,因此要避免使用沒有意義的命名。
1.4 註釋
通常我們在編寫完一段程式碼的短時間內,會清楚這段程式碼的工作原理。但是當過一段時間再次回到程式碼中,可能會花很長的時間才能讀懂。這種情況下,編寫註釋就變得尤為重要了。
2 變數
首先說一說全域性變數存在哪些的問題吧!命名衝突、測試難度大、深耦合等等。在建立變數的時候,我們應該注意以下幾個方面
2.1 避免隱性的建立全域性變數
什麼是隱性的全域性變數呢?官方的回答是:任何變數,如果未經宣告,就為全域性物件所有。啥意思呢?其實就是沒有加var宣告的,請看下面的例子
1 2 3 4 |
function obj() { name = "aotu"; return name; } |
另外一種容易建立隱形全域性變數的情況就是var宣告的鏈式賦值,如下程式碼所示
1 2 3 |
function person() { var a = b = 1; } |
以上這段程式碼的執行結果是:a是區域性變數,b是全域性變數,主要原因是從右至左的操作符優先順序,它實際執行的結果等同於
1 |
var a = ( b = 0 ); |
綜上所述,隱式全域性變數並不是我們平時用var宣告的變數,而是全域性物件的屬性,既然是屬性,那麼它可以通過delete操作符刪除,但變數不可以,且在ES5 strict以上會丟擲錯誤。
2.2 在函式頂部宣告變數
在javascript中,宣告變數有一個“提升”的概念,即無論在函式哪裡宣告,效果都等同於在函式頂部進行宣告。所以我們統一把變數在函式頂部宣告,既有利於可讀性與可維護行,也不易出錯。
2.3 使用單一var模式
1 2 3 |
var a = 1, b = 1, c = 1; |
這樣宣告的變數不僅可讀性好,而且可以防止變數在定義前就被使用的邏輯錯誤,且編碼更少。
2.4 單全域性變數方式
雖然全域性變數的容易汙染名稱空間,但有些功能的需要,難以避免使用,關鍵是我們應該做到避免全域性變數超出我們的掌控,最佳的方法是依賴儘可能少的全域性變數。我們可以使用單全域性變數的方式來開啟我們的專案,這種方式在許多的javascript類庫中都有這樣使用。如jQuery,它定義了兩個全域性變數$和jQuery。
3 UI鬆耦合
什麼是鬆耦合?當修改一個元件的邏輯,而對另一個元件沒有影響,就說這叫鬆耦合。通常一個大型的web應用,都是由多人共同開發維護,這時候鬆耦合顯得至關重要,假如你修改了某一處的程式碼而影響了團隊其他人的功能,這是非常不友好的。通常我們主要注意以下幾點
- 將javascript從css中抽離,如避免使用css表示式
- 將csst從javascrip中抽離,如避免使用javascript直接修改css,最佳的方法是操作css的className;
- 將javascript從HTML中抽離,如避免將函式直接嵌入到html執行,我們應該儘量做到將所有的js程式碼都放入外接檔案中,確保
html中不會有內聯的js程式碼。 - 將html從javascript中抽離,如避免在js中拼接html結構,我們可以用模板引擎,也可以使用Vue、React等。
4 錯誤處理
4.1 為什麼要丟擲錯誤?
在javascript開發中,總是會悄無聲息的出現一些超出我們預期的,攜帶的資訊稀少的,隱晦含糊的bug,讓我們措手不及,大大增加了我們除錯錯誤、定位錯誤的難度,影響開發效率。假設錯誤中包含這樣的資訊:“由於某某情況,導致某某函式執行錯誤”,那麼是不是馬上就可以開始除錯而不用花大量的時候去定位錯誤?
4.2 何時丟擲錯誤?
主要是辨識程式碼中哪些部分在特定的情況下最後可能導致錯誤,這裡的錯誤通常都是我們在思考的過程中的一些可預期的錯誤。
4.3 怎樣丟擲錯誤?
4.3.1 使用try-catch
將可能引發錯誤的程式碼放在try塊中,處理錯誤的程式碼放在catch中,如
1 2 3 4 5 |
try { someMethod(); } catch (ex) { catchError(ex); } |
也可以增加一個finally塊,這裡需注意的是finally塊中的程式碼塊不管是否有錯誤發生,最後都會被執行。
4.3.2 throw
當我們能清晰的捕捉到錯誤的時候,最好的做法就是丟擲這個錯誤,避免在不經意的時候又遇到它,讓大家尷尬。這裡需注意的是當遇到throw操作符時,程式碼會立即停止執行
1 |
throw new Error("method(): descdescdesc"); |
也可以自定義一個錯誤型別,總之就是儘可能用最短的字元描述清楚
1 2 3 4 5 |
throw { name: "myErrorType", message: "arguments must be a DOM element", errorMethod: errorMethod } |
5 建立物件
5.1 物件字面量
所謂的物件字面量其實就是我們通常所說的鍵值對雜湊表,這種方式不僅富有表現力,可讀性好,且字元更短,沒有作用域解析。它的語法規則如下
- 物件包裝在大括號中
- 逗號分隔屬性和方法
- 用冒號分隔屬性名稱和屬性的值
123456789var obj = {name: "aotu",job: "farmer",getName: function () {return this.name;}}//呼叫方式obj.getName();
實現私有屬性
以上例子的name、job屬性都是可直接訪問的。有些時候我們可能想實現一些私有的屬性,然後提供一個公有的介面來對外訪問。雖然javascript並沒有特殊的語法來表示私有、公共屬性和方法,但是可以通過匿名閉包來實現,內部的任意變數都不會暴露,來看以下程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 |
var obj; (function () { //這樣就能實現私有成員 var name = "aotu", job = "farmer"; obj = { getName: function () { return name; } } }()) |
更優雅的寫法
1 2 3 4 5 6 7 8 9 |
var obj = (function () { var name = "aotu", job = "farmer"; return { getName: function () { return name; } } }()); |
這種寫法也是模組模式的基礎框架,後續會有詳細介紹。
熟悉了這種模式之後它還有很多種玩法,比如可以像jQuery這樣鏈式呼叫:“$(‘#id’).siblings(‘ul’).find(“li”).addClass();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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); } }; //呼叫方式 obj.add(5).red(2).setTotal(); //3 |
5.2 建構函式
我們先來看看建構函式的基礎框架
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function Obj() { //公有屬性 this.name = "aotu"; this.job = "farmer"; //公有方法 this.getName = function () { console.log(this.name); } } //呼叫方式 var obj = new Obj(); obj.getName(); |
在使用new方式例項化建構函式通常會經歷以下幾個步驟
- 建立一個物件並且this變數引用了該物件,且繼承了該物件的原型
- 屬性和方法被加入到this引用的物件中
- 隱式的返回新物件
忘記使用NEW的情況
當然我們有時候會忘記使用new操作符的例項化的情況,然而這並不會導致語法錯誤,但建構函式的this指向了全域性物件,可能會發生邏輯錯誤或者意外,來看下面執行的結果
1 2 |
var obj = Obj(); obj.getName(); //Cannot read property 'getInfo' of undefined |
為了避免這種意外發生,我們也可以在建構函式中檢查this是否為建構函式的一個例項,強制使用new操作符,繼續看下面的例子
1 2 3 4 5 6 7 8 9 10 |
function Obj() { if(!(this instanceof Obj)){ return new Obj(); } this.name = "aotu"; this.age = 25; this.getName = function () { console.log(this.name); } } |
再看執行的結果
1 2 |
var obj = Obj(); obj.getName(); //"aotu" |
靜態成員
在javascript中,並沒有特殊的語法來表示靜態成員,但我們可以為建構函式新增屬性這種方式來實現這種語法,請看下面的例子
1 2 3 4 5 6 7 8 9 10 |
//建構函式 function Obj() {} //新增靜態方法 Obj.getAge = function () { console.log(25); } //注意這裡的呼叫方式 Obj.getAge(); //25 //如果使用例項物件呼叫 obj.getAge(); //Object #<Obj> has no method 'getAge' |
這裡大家需要注意呼叫靜態方法的方式,若以例項物件呼叫一個靜態方法是無法正常執行的,反之同理。
私有屬性與方法
在以上例子中建構函式的屬性與方法都屬於公有方法,我們也可以給建構函式新增私有方法與私有屬性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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); } } |
建構函式存在的問題
建構函式的主要問題就是當多次例項化這個建構函式的時候,每個方法都會重新建立一遍,這樣就等於在記憶體中的拷貝。解決問題的第一種思路就是將函式中的方法通過函式定義轉移到函式外面,並將指標傳遞給建構函式,來看下面的例子
1 2 3 4 5 6 7 8 9 10 11 12 |
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() |
雖然也解決了以上的問題,但並沒有達到封裝的效果。接下來我們引入原型prototype的概念。
5.3 原型模式
每一個建構函式都有一個原型prototype,原型物件包含一個指向建構函式的指標,這個指標指向一個可以由特定型別的所有例項共享的屬性和方法,所以使用原型物件可以讓所有物件例項共享它的屬性和方法,來看下面的例子
1 2 3 4 5 6 7 8 9 10 11 12 |
function Obj() {} Obj.prototype.name = "aotu"; Obj.prototype.age = 25; Obj.prototype.getName = function () { console.log(this.name); } //呼叫方式 var obj1 = new Obj(); obj1.getName() //"aotu" var obj2 = new Obj(); obj2.getName() //"aotu" alert(obj1.getName == obj2.getName); //true |
由此可見obj1 和 obj2 訪問的是同一個getName函式
更好的寫法
我們可以將所有的原型都寫在一個物件字面量裡,這樣整個程式碼看起來更加簡潔清晰,繼續往下看
1 2 3 4 5 6 7 8 |
function Obj() {} Obj.prototype = { name: "aotu", age: 25, getName: function () { return this.name; } } |
使用字面量的方式需注意的問題
在使用這種字面量的方式的時候需注意以下兩點
1.將prototype設定為等於一個物件字面量形式建立的物件,它本質上已經完全重寫了預設的prototype物件,最終結果雖然相同但是其constructor屬性不再指向該物件。
constructor是個什麼鬼?在預設情況下,所有原型物件都會自動獲得一個constructor,它指向prototype屬性所在函式的指標,換句話說這個constructor就是指這個建構函式。以上程式碼執行結果如下所示
1 2 |
var obj= new Obj(); alert(obj.cnstructor == Obj) //false; |
我們可以在重寫prototype的時候給constructor指定建構函式,接著往下看
1 2 3 4 5 6 7 8 9 10 11 |
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.當我們重寫整個原型的時候如果先建立了例項,就會切斷建構函式與原型之間的聯絡,因為例項的指標僅僅指向原型,而不是建構函式,在實際的操作過程中,應該儘量避免這種錯誤
1 2 3 4 5 6 7 8 9 10 11 |
function Obj() { } var obj = new Obj(); Obj.prototype = { constructor: Obj, name: "aotu", age: 25, getName: function () { return this.name; } } obj.getName(); //error |
組合使用二者
在我們的具體應用中,通常比較多的是組合使用建構函式模式與原型模式。建構函式用於定義例項屬性,原型用於定於共享的屬性和方法,這樣能夠最大限度的節省記憶體。以下是一個基本的組合使用建構函式與原型的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 模組模式
模組模式是一種非常通用的模式,也是使用頻率比較高的模式,它具有以下幾個特點
- 模組化
- 可複用
- 鬆耦合
- 區分了私有方法與公共方法
我們先看看模組模式的基礎框架
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var testModule = function () { //私有成員 var testNode = document.getElementById("test"); //也可在此定義私有方法 function privateMethod() { console.log("this is Private method!"); } return { //對外公開的方法 setHtml: function (txt) { testNode.innerHTML = txt; } } } //呼叫方式 var testModule = new testModule(); testModule.setHtml("Hello"); |
這種方式看起來比較清晰、簡潔,但就是每次呼叫的時候都需要用new來例項化,我們知道每個例項在記憶體裡都是一份拷貝。如何解決這個問題呢?…我們可以採用一個匿名閉包來完美的解決這個問題。
1 2 3 |
(function () { //將所有的變數和function放在這裡宣告,其作用域也只能在這個匿名閉包裡面,既達到了封裝的目的,也能防止命名衝突 }()) |
接下來我們將它應用到具體的例項中,以下就是一個基本的Module模式
1 2 3 4 5 6 7 8 9 10 11 12 |
var testModule =(function () { var my = {}, testNode = document.getElementById("test"); my.setHtml = function(txt) { testNode.innerHTML = txt; } return my; } ()) //呼叫方式 testModule.setHtml("Hello"); |
通常在一個大型的專案中,會有多人共同開發一個功能的情況,這個時候我們可以運用這種模式將全域性變數當作引數傳遞,然後通過變數返回,從而達到多人協作的目的。
1 2 3 4 5 6 7 8 9 |
var testModule =(function (my) { var testNode = document.getElementById("test"); my.setHtml = function(txt) { testNode.innerHTML = txt; } return my; } (testModule || {})) |
我們也可以通過這個模式將私有的物件或者屬性保護起來,然後設定一些公共介面對外訪問,繼續來看下面的程式碼
1 2 3 4 5 6 7 8 9 10 11 |
var testModule =(function () { var testNode = document.getElementById("test"), setHtml = function(txt) { testNode.innerHTML = txt; }; //設定公共呼叫方法 return { setHtml: setHtml } } ()) |
以上幾種方式僅僅只是一些建立物件的基礎,通過靈活運用這些基礎,可以變換出傳說中各種各樣的模式,如迭代器模式、工廠模式、裝飾者模式等,對於後續學習其他的技術也是極有幫助的,如React:
1 2 3 4 5 6 7 8 9 10 |
var MyTitle = React.createClass({ getDefaultProps : function () { return { title : 'Hello World' }; }, render: function() { return <h1> {this.props.title} </h1>; } }); |
Vue:
1 2 3 4 5 6 7 8 9 10 11 |
new Vue({ el: '#app', data: { message: 'Hello Vue.js!' }, methods: { reverseMessage: function () { this.message = this.message.split('').reverse().join('') } } }) |
以上就是本期的所有內容,如有錯漏,懇請指正,大家共同進步!在下一期中,會繼續跟大家探討更多好玩的東西,敬請期待~~~
6 參考資料
《編寫可維護的JavaScript》[美] Nicholas C. Zakas 著
《JavaScript設計模式》[美] Addy Osmani 著
《JavaScript高階程式設計(第3版)》