精讀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的做法速度會更快,因為與零相比要比和非零數字或者數組長度比較要高效跟多。

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM