1. 程式人生 > >精讀JavaScript模式(一)

精讀JavaScript模式(一)

一、前言

為什麼讀這本書?

其實做前端開發,一個需求給不同工作經驗的人去做,只要完工時間不算苛刻,大家都是能實現的。功能實現雖然大致相同,但當我們迴歸程式碼去看實現方式,程式碼書寫的美觀程度,以及實現的方法其實是不盡相同的。畢竟經驗豐富的人,拿到一個需求,可能腦海裡就浮現了多個可供選擇的方案,而經驗較淺的人,就更偏向於如何實現基本需求了。
例如說到過濾一個數組,第一想到使用for迴圈,不會想到filter方法;再如做條件判斷,首先想到if else,忽略掉了還有which case或Boolean?true:false三元運算子之類的其它選擇。說這些不是說後者畢竟比前者要好,畢竟對於不同的使用場景,合適的才是最佳的,但能舉一反三,從三種甚至多種方法中做出選擇,是肯定要比一招鮮要更好的。


經驗的積累不是一天兩天的事情,這點我也明白,那能不能先從基本做起,比如瞭解更好的程式碼書寫規範,掌握好基本概念,知道一些實用的js模式甚至說套路,那這就是我讀這本書的原因了。
從這本書,你會知道比常規for迴圈更優的寫法,知道new一個建構函式時究竟發生了什麼,知道為什麼setTimeout('fun()',1000),setTimeout(fun,1000)兩種寫法,為什麼前者加引號都能執行,知道更優秀的編碼方式以及更多有趣的東西。
這個系列只是作為讀書筆記,挑出一些重要或者我覺得有趣的的概念,如果覺得有趣,推薦閱讀原書。

二、JavaScript概念 

1.面向物件
JavaScript(以下簡稱js)是一門面向物件的程式語言,我們總說,萬物皆物件,這點是沒錯的。但需要注意的是,js中的六種基本(原始)資料型別

不是物件,它們分別是,String(字串)Number(數字)Boolean(布林型別)nullundefined,以及ES6新增的基本型別symbol
複雜(引用)資料型別可以歸納為物件型別,而物件有兩大類,本地物件與宿主物件。

宿主物件包含window和所有DOM物件,而本地物件包括了內建物件(如 Function,Array,Date)或自定義物件(var o = {});
基本資料型別與引用資料型別的區別在於,基本資料型別的變數名與值都是存放在棧記憶體中,而對於引用資料型別,變數在棧記憶體中,值存放中堆記憶體中,變數名指向由堆記憶體提供的值地址,不理解可以具體看看博主對於深淺拷貝中值的存放圖解。


說到資料型別,null是需要單獨說說的。我們可以在瀏覽器F12調出控制檯,輸入typeof null回車,可以看到輸出為object,這是為什麼呢?
在 JavaScript 最初的實現中,JavaScript 中的值是由一個表示型別的標籤和實際資料值表示的。物件的型別標籤是 0。由於 null 代表的是空指標(大多數平臺下值為 0x00),因此,null的型別標籤也成為了 0,typeof null就錯誤的返回了"object"。

ECMAScript提出了一個修復(通過opt-in),但被拒絕。這將導致typeof null === 'object'。
這段話可以理解為,這是早期JS設計留下的缺陷,但我們只要記住,雖然typeof得到的是object型別,但null本質就是基本資料型別,那我們要判斷null型別該怎麼辦呢,可以使用如下方法。

Object.prototype.toString.call(null) === [object Null]。

2.原型(prototype)
js中的繼承是程式碼重用的一種方式,繼承的方式很多,原型就是其中一種,需要注意的是,原型其實就是一個普通物件。我們建立的每一個函式其實都自帶prototype屬性,這個屬性指向一個空物件,你可以為這空物件新增各種屬性方法,而這些新增成員可以被其它物件繼承,作為其它物件的自有屬性。這個空物件也不是嚴格意義上的空,它自帶一個constructo屬性,它指向你新建的函式。
3.嚴格模式
嚴格模式是採用具有限制性JavaScript變體的一種方式,從而使程式碼顯示地 脫離“馬虎模式/稀鬆模式/懶散模式“(sloppy)模式。新增模式比較簡單,你只需要在你希望執行嚴格模式的作用域新增"use strict"即可。

"use strict"

顧名思義,嚴格模式相比傳統模式有以下改變:(筆試遇到過一次)

• 消除Javascript語法的一些不合理、不嚴謹之處,減少一些怪異行為;
• 消除程式碼執行的一些不安全之處,保證程式碼執行的安全;
• 提高編譯器效率,增加執行速度;
• 為未來新版本的Javascript做好鋪墊。

二、高質量javaScript基本要點
1.編寫可維護的程式碼
人人都喜歡開發新功能,一切從零開始,不喜歡維護舊程式碼,特別是一段段密密麻麻沒有註釋的程式碼,這點每個開發者都感同身受。但拋開閱讀遺留程式碼或者同事的程式碼,就算是我們自己開發的功能,兩三個月後回頭再讀也可能出現閱讀困難的問題,這就導致了維護成本較高的問題;閱讀程式碼超過開發功能的時間很明顯是不合理的。
因此我們在開發新功能,或者維護舊程式碼的同時,就得花時間為不合理的程式碼進行調整,例如
• 可讀的
• 一致的(看起來像同一個人寫的,有統一的規範)
• 可預測的(能拓展)
• 有文件的(或註釋)
2.減少全域性物件
減少全域性物件基本是每個前端者入行就被告知的點。js是使用函式來管理作用域(scope)的,那麼可以說,在一個函式內定義的變數就是一個區域性變數,區域性變數在當前作用域外部是不可見的;反之,全域性變數是不在任何函式體內宣告的變數,或者是直接使用而未申明的變數。

function echo() {
    var a = 1;//區域性
    b = 2;//雖然在函式體內,但是未使用var let之類申明。
};
var c = 3//全域性,雖然有申明,但是在函式體外。

每個js執行環境都有一個隱式全域性物件,通常瀏覽器用全域性物件window代表這個全域性物件隱式全域性物件,我們建立的每個全域性變數都是這個全域性物件的屬性,我們可以不在任何函式體內使用this就能檢視這個全域性物件的引用,如:

a = 1;
console.log(a);//1
console.log(this.a);//1
console.log(window.a)//1
console.log(window['a'])//1

全域性變數在js程式碼執行的整個作用域都是可見的,正因為它們存在於同一個名稱空間中,所以會發生命名衝突的問題。我們很難保證自己定義的全域性變數是否與三方庫,外掛中變數是否有重名,所以使用變數先去申明它是非常重要的。
順帶一提

function echo() {
    var a = b = 0;
}

其中b是全域性變數,a是區域性變數,等價於var a = (b = 0);(實際開發中肯定是不推薦這樣的寫法,可讀性太差,只是書中有舉例,順帶說說這種寫法帶來全域性變數的問題)
隱式全域性變數與顯式全域性變數
隱式全域性變數:通過 var 建立的全域性變數(在任何函式體之外建立的變數)不能被刪除。
隱式全域性變數:沒有用 var 建立的隱式全域性變數(不考慮函式內的情況)可以被刪除。

var a = 1;
b = 2;
console.log(delete a)//false
console.log(delete b)//true

隱式全域性變數並不算是真正的變數,可以說它們是全域性物件的一個屬性成員。而屬性是可以通過delete運算子刪除的,變數不可以被刪除,這是兩者的區別。
3.訪問全域性物件
我們在前面說,全域性變數總是被隱性的新增為全域性物件的屬性,那麼我們其實可以通過全域性物件來訪問全域性變數,例如通過window。但並不是在所有的環境下預設隱性全域性物件都是window,或者說某個環境的全域性物件可能不叫window。但我們可以利用根據this指向原則始終能找到全域性物件,
例如函式在自調情況下,this總是指向全域性物件(嚴格模式下this會指向undefined)。

var global = (function (){
    return this;
})();
console.log(global)//當前環境的全域性物件

4.單 Var 模式
申明變數在程式設計中是高頻率的,在函式頂部使用一個單獨的var語句是非常推薦的一種模式。這麼做有如下好處:
• 在同一個位置可以查詢到函式所需的所有變數(變數集中,方便查詢)
• 避免當在變數宣告之前使用這個變數時產生的邏輯錯誤(申明提前的問題)
• 提醒你不要忘記宣告變數,順便減少潛在的全域性變數
• 程式碼量更少(輸入更少且更易做程式碼優化)

var a = 1,
    b = 2,
    c,
    fun = function () {};

當然使用let const申明也是可以使用這種模式的,而且let申明變數也徹底解決了var申明提前這種較為詬病的問題,這裡還是按照書中思路去整理了筆記,大家心裡能明白就好。
5.申明提前:分散的var帶來的問題
首先,申明提前可以說是var申明模式的一個隱性問題,有時候會帶來一些不必要的麻煩,而在ES6中新增的let申明方式其實已經解決了var的申明提前問題,本來這一點可說可不說,但畢竟還是有一些面試題會說道,就簡單帶一帶。
對於js來說,當我們在某個作用域(比如同一個函式內)裡聲明瞭一個變數,這個變數在整個作用域內都是可見的,可使用的,包括在 var 宣告 語句之前,這種情況就是所謂的申明提前。

(function (){
    console.log(a);//undefined
    var a = echo;
    console.log(a);//echo
})();

在這段程式碼中,儘管第一個console在變數a申明之前,它也不會報錯,因為在這個函式體內,var a申明會提前(賦值不提前),任何一個地方,不管先後都能正確的使用它,它等同於

(function (){
    var a;
    console.log(a);//undefined
    a = echo;
    console.log(a);//echo
})();

6.更優的for 迴圈
這裡不討論for forEach while各類迴圈方法的效能優劣,畢竟迴圈之爭一直存在,可讀性,效能太多因素,還是根據實際使用場景來定奪,後面有空也確實想對於現有常用資料遍歷可行方法進行一個整理。(應該不會鴿)
我們最常見的for迴圈寫法

for (var i = 0; i < arr.length; i++) {
  //do something with arr[i];
}

在for 迴圈括號中,var i = 0其實只會申明一次,但i < arr.length 與i++是每次迴圈都會執行的。那麼就存在一個問題,上面的程式碼每次迴圈都會重複取一次陣列arr的length屬性,這會降低程式碼的效能,特別是當arr不單單是個陣列,而是一個HTMLCollection物件時。
HTMLCollection物件是由DOM方法返回的物件,例如:
• document.getElementsByName()
• document.getElementsByClassName()
• document.getElementsByTagName()
操作dom是一個很耗資源的行為,如果每次迴圈都要遍歷查詢dom元素顯然不太合理,更好的做法是用變數一開始就儲存陣列的長度。

for (var i = 0, max = myarray.length; i < max; i++) {
  // do something with myarray[i]
}

或者這樣,將變數的申明統一在一起,for只用管好自己的迴圈。

var i = 0,
myarray = [],
max = myarray.length;
for (; i < max; i++) {
  // do something with myarray[i]
}

注意括號中的第一個分號我有保留,或者寫成(i < max; i++;)也可以,分號不能丟,不然會報錯。
通過上面的改寫,不管迴圈多少次,其實都只用查詢一次DOM節點的length,是不是比較nice。
對於for迴圈,其實還可以做少量的改進,在for中我們之所以申明i = 0的作用是告訴迴圈,i是從0開始遞增並與max做判斷是否需要繼續下次迴圈,其實我們可以直接獲取陣列長度讓其遞減,效果是一樣的。

var myarray = [],
i = myarray.length
for (; i --;){
  // do something with myarray[i]
}

這樣寫分號總覺得有點奇怪,我們也可以使用while來進行代替

var myarray = [],
i = myarray.length;
while (i--) { //在某篇部落格看到過while 比 for更快的說法
  // do something with myarray[i]
}

這麼做相比前面的寫法有兩個有點,第一,變數減少了,我們直接讓將length賦予給i進行遞減,省去了變數max,其次,遞減到0的做法速度會更快,因為與零相比要比和非零數字或者陣列長度比較要高效跟多。

第一篇就先記到這裡,再寫下去篇幅就太長了點,看著就不太想想讀了,不過估計也沒人會耐著性子讀這樣的文章吧。

第二篇也會抓緊時間寫,倘若有人閱讀過,也歡迎指出錯誤。