1. 程式人生 > >《編寫可維護的JavaScript》- 讀書筆記

《編寫可維護的JavaScript》- 讀書筆記

引用

這是我刷完的第一本書。萬事開頭難,總算是在2017年02月09日開了一個好頭。

這篇總結是為了記錄在讀這本書的過程中所遇到的好的知識點和思想,以及我在實際工作中結合作者的想法所做的一些實踐和讀書的收穫。

這本書從兩個個方面(風格和實踐)來講述如何去寫維護性高、可讀性高和高效的JavaScript程式碼。

程式設計風格

“程式設計風格”是指在長期編寫程式碼的過程中,養成關於編寫方式、程式碼結構和程式碼可讀性等等方面的習慣。

統一程式設計風格的好處是:

  1. 由於程式設計風格一致,團隊合作過程中,看其他人的程式碼就像在看自己寫的程式碼,提高了可讀性;
  2. 由於程式設計風格一致,當遇到自己感到奇怪的程式碼會立刻發現其可能出現的問題。

格式化

格式化是程式設計風格指南的核心規則,這些規則最直接的體現在於程式碼的水準。

1. 縮排層級

目前主要有兩種主張縮排的方式:

    1.使用製表符進行縮排:
        每一個縮排都是用單獨的 `Tab(製表符)` 來表示。

    2.使用空格符進行縮排:
        即每一個縮排都是用單獨的 `space(空格)` 來表示

二者各自有各自的缺點:

  1. 製表符:每個系統和編輯器對製表符的解釋不一致,雖然對於編輯器來說可以單獨的對製表符進行設定,但是這些差異還是會帶來一些困惑。
  2. 空格:對於單個開發者來說,使用一個沒有配置好的文字編輯器建立格式化的程式碼的方式非常原始。

個人非常傾向於使用 “Tab” 來表示行內縮排的。

2. 語句結尾

對於 JavaScript 程式碼來說,在語句的結尾可以寫分號(;),亦可以不寫分號。

這是有賴於分析器的自動分號插入(Automatic Semicolon Insertion, ASI),ASI會自動的尋找程式碼中應當使用分號但實際沒有分號的位置,並插入分號。

個人推薦的是在每句的結尾如果可以加上分號,那麼一定加上分號,不止防止了低階錯誤的發生,也增加了程式碼的可讀性,因為會告訴正在看程式碼的人“這一句到這就結束了”。

3. 行的長度

目前關於程式碼中每行的長度是由爭議的,前段時間看過阮一峰老師的一篇關於CPL為什麼會有在72和80這兩個選項,很受啟發。

對於我來說,儘可能讓一行程式碼的字元數少於72,如果72個字元滿足不了的話,就儘可能的少於80個字元,若這樣還是滿足不了,那麼就儘量的減少吧,這種情況雖然極少數(可能和我水平有關),如果出現了也實在是沒有辦法。

4. 換行

當一行的程式碼長度達到了單行最大字元數的限制時,就需要手動將一行拆成兩行(這種情況居多)。通常需要在運算子後換行,下一行增加兩個層級的縮排。

請記住,一定要將運算子置於行的結尾,這樣ASI就不會再該行的結尾插入分號。

5. 空行

在編碼規範中,空行是常常被忽略的一個方面。在書中,介紹了關於一段程式碼應該是一系列可讀的段落(像文章一樣),而不是一大堆揉在一起的連續的文字。

因此書中建議在以下地方插入空行(即回車):

  1. 方法之間;
  2. 在方法中的區域性變數和第一條語句之間;
  3. 在多行或單行註釋之前;
  4. 在方法內的邏輯片段之間插入空行,提高可讀性;

6. 命名

電腦科學只存在兩個難題: 快取失效和命名 —— Phil Karlton

我在寫程式碼的過程中,其實是十分重視變數名的,我看過一句話大概的意思是:再好的註釋,也比不過天然自帶註釋的命名。但是由於自身的英語詞彙量少的可憐,因此變數的名稱翻來覆去也想不出多少個花樣。。。

目前存在兩種主要的命名方式:駝峰式 和 下劃線,其中駝峰式又包括小駝峰(Camel Case)和大駝峰(Pascal Case),二者主要的區別在於大駝峰要求首字母大寫,而小駝峰要求首字母小寫。下劃線的命名方式是每個單詞之間用“_”來連結,同時所有的字母均小寫。

個人比較習慣的命名方式是:對於變數,若是區域性的變數,使用小駝峰的方式;若是全域性變數和靜態變數,使用所有字母大寫加上下劃線的方式。函式的命名習慣是:建構函式使用首字母大寫的小駝峰式,普通函式使用小駝峰的方式命名

7. 直接量

什麼是直接量?JavaScript 中包含一些型別的原型值: 字串、數字、布林值、nullundefined。同樣的,也包含陣列直接量[]和物件直接量{}

7.1 字串

字串可以用單括號''或者是雙括號""括起來,在這裡作者強調的是,不論是單括號還是雙括號,在你的程式碼中,一定要保持相同的括號風格,即在程式碼中,儘量在以一種方式的括號表示字串。

字串還需要關注的另一個問題就是多行字串。作者建議,如果字串如果太長,那麼使用多行字串代替過長的字串。例如:

var longString = 'Here`s the story, of a man ' + 
                 'named Brady';
7.2 數字

在 JavaScript 中只需要關心數字的一個問題,即如果一個數字是浮點型,一定要寫全小數點後面幾位。萬萬不可只有小數點,省略了小數點後面的小數部分,如10.

7.3 null

null 在 JavaScript 中是特殊值,但是它很容易和undefined搞混。作者在書中介紹說,在以下幾種場景中可以使用null

  • 用來初始化一個變數,這個變數可能被賦值為一個物件;
  • 用來和一個已經初始化的變數比較,這個變數可以是也可以不是一個物件;
  • 當函式的引數期望是物件時,作為引數傳入;
  • 當函式的返回值期望是物件時,用作返回值傳出。

還有一些場景中不應當使用null

  • 不要使用 null 來檢測是否傳入了某個引數;
  • 不要用 null 來檢測一個未初始化的變數。
7.4 undefined

undefined 也是一個特殊值。其中最讓人感到困惑的是 null === undefined 的結果是 true

那些沒有被初始化的變數都有一個初始值,即 undefined,表示這個變數等待被賦值。

在我的實踐中,可以通過判斷一個引數是否是 undefined 來判斷這個引數是否在函式被呼叫是被傳入。同時,undefined 通常來說,只與 typeof 運算子一起使用,用以判斷一個變數是否被賦值或者存在。

7.5 物件與陣列直接量( {} & [] )

建立物件或者陣列的最流行也是最常見的一種方式是使用物件或陣列的直接量,在直接量中直接寫出所有的屬性。例如:

// 使用物件直接量
var book = {
    title: 'Maintaintable JavaScript',
    author: 'Nicholas C. Zakas'
}

// 使用陣列直接量
var colors = [ 'red', 'blue', 'green' ];
var numbers = [1, 2, 3 ,4, 5];

註釋

註釋共分為兩種方式:單行註釋和多行註釋;

對於註釋,我的習慣是:

單行註釋:雙斜槓 // 後面插入一個空格,獨佔一行並且在此註釋之前插入一個空行,縮排和要註釋的程式碼的縮排一致。
多行註釋:總是會出現在將要描述的程式碼段之前,縮排與要描述的程式碼段一致,同時註釋之前插入一個或兩個空行。

語句和表示式

所有語句都應當使用花括號,包括:

if/for/while/do...while.../try...catch...finally

1. 花括號的對齊方式

有兩種風格:

  1. 將左花括號放置在塊語句中第一句程式碼的末尾;
  2. 將左花括號放置於塊語句首行的下一個行。
// 第一種
if (condition) {
    doSomeThings();
}

// 第二種
if (condition)
{
    doSomeThings();
}

2. 塊語句間隔

共三種方式:

  1. 在語句名、圓括號和左花括號之間沒有空格間隔;
  2. 在括左圓括號之前和右圓括號之後各新增一個空格;
  3. 在左圓括號後和右圓括號前各新增一個空格;
// 第一種
if(condition){
    doSomeThings();
}

// 第二種
if (condition) {
    doSomeThings();
}

// 第三種
if ( condition ) {
    doSomeThings();
}

我個人比較傾向於第二種方式。

3. for-in 迴圈

for - in迴圈是用來遍歷物件屬性的。

但是,for - in迴圈有一個問題,就是它不僅遍歷物件的例項屬性,同樣的還遍歷從原型繼承來的屬性。因此,對於這個問題,最好使用 hasOwnProperty() 方法來為 for - in 迴圈過濾出例項屬性。

var props;

for (var item in props) {
    if (props.hasOwnProperty(item)) {
        doSomeThings(props[item]);
    }
}

我在編碼過程中,如果使用 for-in 迴圈都會使用 hasOwnProperty() 方法。

需要注意的是: for-in 迴圈是用來遍歷例項物件,不能用它來遍歷陣列,這會造成一些潛在的問題。

既然如此,就可以用這個特性來判斷一個物件是否為空:

const isEmptyForObject = (obj) => {
    for(let item in obj) {
        return false;
    }

    return true;
}

變數、函式和運算子

1. 變數

說到變數,首先對於 JavaScript 的變數要明白一個理論:變數提升

當建立變數時,需要用到 varletconst 關鍵字,這些關鍵字意義和作用並不相同:

  • var:是 JavaScript 最經典的建立變數的關鍵字,它定義的變數可以修改並且它並不存在“塊”的作用域,也就是說在ES6之前,函式內部定義的變數,在其外部依舊可以呼叫:
function change () {
  var demo = 4;
  console.log(demo);
}

change();           // 4
console.log(demo)   // demo = 4
  • let:是ES6新添的建立變數的關鍵字,它定義的變數也可以修改,但是它強調“塊作用域”的概念,即在函式內部使用let並修改,對函式外部沒有影響:
function change () {
  let demo = 4;
  console.log(demo);
}

change();           // 4
console.log(demo)   // demo is not defined
  • const:同樣是ES6特有的建立變數的關鍵字,不過它定義的變數除了初始化以外,是不可以修改賦值的。
const demo = 4;
demo = 5;       // Assignment to constant variable.

請注意:
當我在建立變數的時候,習慣於將所有的變數放到區域性程式碼塊的首行,並且很喜歡將所有的 var/let/const 語句合併成一句:

var a = 5,
    b = 6;

let c = 7,
    d = 8;

const e = '123',
      f = 'afdasdf'; 
變數提升

在 JavaScript 中,函式變數宣告都將被提升到函式的最頂部。

在 JavaScript 中,變數可以在使用後宣告,也就是變數可以先使用再宣告,即:在函式內部任意地方定義變數和在函式定義變數是完全啊一樣的。例如:

// 提升前(原始碼)
function doSomething () {
    var result = 10 + value;
    var value = 10;

    return result;
}

// 變數提升
function doSomething () {
    var result, value;
    result = 10 + value;
    valut = 10;

    return result;
}

doSomething()   // NaN (not a number)

需要注意的是,只有變數的宣告才會被提升,至於變數的初始化,並不會被提升。

我的習慣是,總是將區域性變數的定義作為函式內第一條語句。

2. 函式

和變數宣告一樣的,函式的宣告也會被變數提升機制提升到當前作用域下的頂部。

// 函式宣告提升之後:
function doSomethingWithItems (items) {
    var i, len,
        value = 10,
        result = value + 10;

    function doSomething (item) {
        // todo...
    }

    for (i = 0; len = item.length; i < len; i++) {
        doSomething(items[i])
    }
}

同樣需要注意的是,請看下面的栗子:

if (condition) {
    function doSomething (item) {
        alert("HI!");
    }
} else {
    function doSomething (item) {
        alert("YO!");
    }
}

這段程式碼的實際效果是 alert 彈窗裡面的內容是 “YO!”。這也是由於函式宣告提升造成的,上面這段程式碼轉換為下面這段程式碼:

// 被第二個 doSomething 覆蓋
function doSomething () {
   alert("HI!");
};

function doSomething () {
   alert("YO!");
}

if (condition) {
    doSomething();
} else {
    doSomething();
}

這也是大多數瀏覽器都會自動執行第二個宣告的原因。

函式呼叫的風格是:在函式名和左括號之間沒有空格。

在 JavaScript 中允許宣告匿名函式,並將匿名函式賦值給變數或者是物件的屬性。

var doSomething = function () {
    // todo...
}

這種匿名函式在函式的最後加上一對括號可以立即執行並返回一個值,然後將這個值賦值給變數或者物件的屬性。

// 這種寫法並不推薦,只是為了展示作用
var value = funciton () {
    return {
        message: 'hi'
    };
}();

這種模式的問題在於,會讓人誤認為將一個匿名函式賦值給了這個變數。除非讀完了完整的程式碼。更好的做法是,為了能讓立即執行的函式能夠被一眼看出來,用一對括號將立即執行的函式包裹起來:

var value = (funciton () {
    return {
        message: 'hi'
    };
}());

3. 相等

由於 JavaScript 具有強制型別轉換機制,因此在 JavaScript 中判斷相等的操作是很微妙的。

發生強制型別轉換最常見的場景,就是使用了判斷相等運算子 ==!=。如果發生了強制型別轉換,那麼對於判斷變數的型別或者是否相等就變得尤為的困難。

在使用 ==!= 的情況下:

  1. 在判斷數字和字串時,字串會首先轉換為數字,然後進行比較;
  2. 在判斷數字與布林值時,布林值會首先轉換為數字,然後進行比較;
  3. 若其中一個值是物件,則會先呼叫物件的 valueOf() 方法,得到原始型別。若沒有定義 valueOf(), 則呼叫 toString()
  4. 需要注意的是,如果 nullundefined 相比較,這個兩個特殊值是相等的。
// 比較數字 5 和字串 5
console.log(5 == '5');          // true

// 比較數字 25 和十六進位制的字串 25
console.log(25 == '0x19');      // true

// 數字 1true
console.log(1 == true);         // true

// 物件
var object = {
    toString: function () {
        return '0x19';
    }
}

console.log(object == 25);      // true

// null & undefined
console.log(null == undefined);

前面說了,如果使用 ==!= 會造成變數之間的型別互相轉換,但是有些時候我們想要嚴格檢查,即兩個變數的型別不同,我們就認為二者不同,這就需要使用 ===!==,這兩個運算子並不會觸發強制型別轉換。

我的習慣是總是使用 ===!== 進行嚴格檢查。

4. 原始包裝型別

JavaScript 中一個不易被理解且常常被誤解的方面是,這門語言對原始包裝型別的依賴。

JavaScript 有3種原始包裝型別: StringBooleanNumber。每種型別代表全域性作用域中的一個建構函式,並分別表示各自對應的原始值的物件。 原始包裝型別的主要作用是讓原始值具有物件般的行為

var name = 'Nicholas';
console.log(name.toUpperCase());        // NICAHOLAS

儘管 name 是一個字串,是原始型別不是物件,但是仍然可以使用如 toUpperCase()toLowerCase()等方法,把字串當成物件來用。

原因是:
在建立一個字串變數時,JavaScript 引擎建立了 String 型別的新例項,緊跟著就被銷燬了,當在此需要的時候再重新建立另一個例項。

var name = 'Nicholas';
name.author =  true;
console.log(name.author);               // undefined

程式設計實踐

現如今的前端工程師幾乎人人都在談模組化,如:AMDCMDUMD等等,就是在解決某一個複雜問題或者一系列的雜糅問題時,依照一種分類的思維把問題進行系統性的分解以之處理。

那麼 AMD / CMD / UMD 的區別是什麼呢?參考資料 Javascript模組化

首先來解釋下什麼是模組化,模組化是指將複雜的系統分解為多個程式碼結構合理可維護性更高可管理的模組的方式。解耦軟體系統的複雜性,使得不管多麼大的系統,也可以將管理,開發,維護變得“有理可循”。

作為模組化的系統必須具有如下三個特點:

  1. 定義封裝的模組。
  2. 定義新模組對其他模組的依賴。
  3. 可對其他模組的引入支援。

因此基於模組化,目前在 JavaScript 中出現了一些非傳統開發方式的模式: CommonJS模式、AMD模式、CMD模式和UMD模式。

CommonJS

CommonJS 是用於伺服器端的模組規範,NodeJs 主要採用這種模式。

根據 CommonJs 模式的規範,一個單獨的檔案即為一個模組。載入模組使用 require 方法,該方法讀取一個檔案並執行,最後返回檔案內部的 exports 物件。

// foobar.js

//私有變數
var test = 123;

//公有方法
function foobar () {

    this.foo = function () {
        // do someing ...
    }
    this.bar = function () {
        //do someing ...
    }
}

//exports物件上的方法和變數是公有的
var foobar = new foobar();
exports.foobar = foobar;
//require方法預設讀取js檔案,所以可以省略js字尾
var test = require('./boobar').foobar;

test.bar();

從上面可以看出,CommonJS 是同步載入的,只有將其他的依賴都載入完才可以執行檔案本身的內容。如 NodeJs 編寫的伺服器程式碼,檔案一般都是存放在本地硬碟上,載入起來比較快,因此適用於 CommonJS 模式。但是對於瀏覽器來說,下載資源必須要非同步的方式,所以就有了 AMD / CMD 解決方案。

AMD(Asynchronous Module Definition)

AMD 模式是非同步模組定義模式,它設計出一個簡潔的寫模組 API:

define(id?, dependencies?, factory);

第一個引數 id 為字串型別,表示了模組標識,為可選引數。若不存在則模組標識應該預設定義為在載入器中被請求指令碼的標識。如果存在,那麼模組標識必須為頂層的或者一個絕對的標識。
第二個引數 dependencies 是一個當前模組依賴的,已被模組定義的模組標識的陣列字面量(直接量)。
第三個引數 factory 是一個需要進行例項化的函式或者一個物件。

define("alpha", [ "require", "exports", "beta" ], function( require, exports, beta ){
    export.verb = function(){
        return beta.verb();
        // or:
        return require("beta").verb();
    }
});

在不考慮多了一層函式外,格式和 NodeJs 是一樣的:使用 require 獲取依賴模組,使用 exports 匯出 API。

除了define之外,AMD 還保留了一個關鍵字 requirerequire 作為規範保留的全域性識別符號,可以實現為 module loader,也可以不實現。

require([module], callback)

第一個引數 [module] 是一個數組,裡面的成員就是要載入的模組;
第二個引數 callback 是載入完成這些依賴的模組之後執行的回撥函式。

require(['math'], function(math) {
    math.add(2, 3);
});

CMD(Common Module Definition)

CMD 是 SeaJS 在推廣過程中對模組定義的規範化產出:

  • 對於依賴的模組 AMD 是提前執行,CMD 是延遲執行。不過 RequireJS 從2.0開始,也改成可以延遲執行(根據寫法不同,處理方式不通過)。
  • CMD 推崇依賴就近,AMD 推崇依賴前置。
// AMD
define(['./a','./b'], function (a, b) {

    //依賴一開始就寫好
    a.test();
    b.test();
});

// CMD
define(function (requie, exports, module) {

    //依賴可以就近書寫
    var a = require('./a');
    a.test();

    ...
    //軟依賴
    if (status) {

        var b = requie('./b');
        b.test();
    }
});

雖然 AMD也支援CMD寫法,但依賴前置是官方文件的預設模組定義寫法。

UMD(Universal Module Definition)

UMD 是 AMD 和 CommonJS 的結合。

AMD 模組以瀏覽器第一的原則發展,非同步載入模組。
CommonJS 模組以伺服器第一原則發展,選擇同步載入,它的模組無需包裝。
這迫使人們又想出另一個更通用的模式 UMD(Universal Module Definition)
希望解決跨平臺的解決方案。

UMD 先判斷是否支援 NodeJs 的模組(exports)是否存在,存在則使用 NodeJs 模組模式。在判斷是否支援 AMD(define是否存在),存在則使用 AMD 方式載入模組。

(function (window, factory) {
    if (typeof exports === 'object') {
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
        define(factory);
    } else {

        window.eventUtil = factory();
    }
})(this, function () {
    // this is factory
    // module ...
});

關於這本書的收穫

  • 寫出一個具有相容性的事件繫結/掛載的方法(原生JS):

    function addListener (target, type, handler) {
    
        if (target.addEventListener) {
            // 判斷 addEventListener 是否存在
            target.addEventListener(type, handler, false);
        } else if (target.attachEvent) {
            // 判斷 attachEvent 是否存在
            target.attachEvent('on' + type, handler);
        } else {
            target['on' + type] = handler;
        }
    
    }
  • 全域性 name 實際上是 window 的一個預設屬性,window.name 屬性經常用於框架(frame)和 iframe 的場景中。

  • 瀏覽器處理事件的過程: 事件捕獲 –> 事件目標 –> 事件冒泡;

    事件捕獲:事件(click/mouseenter/mouseleave…)首先發生在 document -> body -> … -> 節點(事件目標);

    事件冒泡:事件到達事件目標之後不會結束,會逐層向上冒泡,直至document物件,跟事件捕獲相反;

    事件委託即利用這2個特性,將事件繫結在父節點中,通過事件捕獲獲得節點,通過冒泡執行事件。避免了對每個節點都進行事件的繫結,節省了勞動力。

  • 事件觸發程式碼要與應用邏輯程式碼分開,這麼做的好處是應用邏輯程式碼可以複用,同時在測試時直接功能測試,而不需要模擬對元素觸發事件來測試,同時不要無限地分發事件物件

    var myApplication = {
        handleClick: function (event) {
            // 阻止 事件預設行為 和 事件冒泡
            event.preventDefault();
            event.stopPropagation();
    
            // 傳入應用邏輯
            this.showPopup(event.clientX, event.clientY);
        },
    
        showPopup: function (x, y) {
            var popup = document.getElementById('popup');
            popup.style.left = x + 'px';
            popup.style.top = y + 'px';
            popup.className = 'reveal';
        }
    };
    
    addListener(element, "click", function(event) {
        myApplication.handleClick(event);
    })
  • typeof 是運算子;

  • instanceof 也是運算子;
  • 檢測一個物件是否是 Array

    function isArray (arg) {
        if ('function' === typeof Array.isArray) {
            return Array.isArray(arg);
        } else {
            return Object.prototype.toString.call(arg) === '[object Array]';
        }
    }
  • inhasOwnProperty 的區別

    1. 使用 in 的方式來檢測某例項是否具有某屬性時,in 會在其例項屬性中、原型鏈中遍歷查詢該屬性;
    2. 使用 hasOwnProperty 的方式檢測時,只會檢測例項屬性,不會檢測其原型鏈;

    若不確定是否存在 hasOwnProperty 可以這麼寫:

    if ('hasOwnProperty' in object && object.hasOwnProperty('xxxx')) {
        // todo...
    }
  • 配置資料需要從程式碼中分離開,這麼做的目的防止在修改原始碼的時候引入bug,尤其是對修改一些資料的值從而帶來一些不必要的bug風險,那麼什麼是配置資料呢?配置資料是可發生變更的,而且你不希望因為有人突然想修改頁面中的展示資訊,導致去修改 JavaScript 原始碼。

  • JavaScript 的常見可用檔案格式有三種:JSON/JSONP/JS格式。

    1. JSON:這是一種很常見的資料格式,使用一組 JavaScript 的陣列轉換為字串作為表示格式。
    2. JSONP:是將 JSON 結構用一個函式(呼叫)包裝起來。
    3. JavaScript:將 JSON 物件賦值給一個變數,這個變數會被程式用到。

    這裡,我習慣的方式是第三種,使用 JavaScript 的格式,同樣的會將所有的配置資料全部存入全域性物件中,防止過多的全域性變數導致的變數混亂問題。

  • JavaScript 錯誤:

    • 丟擲錯誤:
      使用 throw 的操作符,將提供的一個物件作為錯誤丟擲,任何型別的物件都可以作為錯誤丟擲,一般的,Error 物件是最常用的。

      throw new Error('something is happened.');
    • 捕獲錯誤:
      JavaScript 提供了 try-catch 的語句,使得能在瀏覽器處理丟擲的錯誤之前捕獲它,可能引發錯誤的程式碼塊放到 try 快中,錯誤的程式碼放在 catch 塊中。
      但是,請注意 try-catch 還有一種寫法 try-catch-finally 的格式,這種格式中 finally 塊中的程式碼是在try-catch 中不論是否發生錯誤,均會執行。但是,如果 try 塊中包含了一個 return 語句,那麼它必須等到 finally 中的程式碼執行完畢之後才能返回。

    • 錯誤型別:
      所有的錯誤型別繼承了 Error,因此用 instanceof Error 運算子檢查其型別得不到任何的有用的資訊。但是可以通過檢查“特定”的錯誤型別來處理:

      try {
          if (ex instanceof TypeError) {
              // 處理 TypeError 錯誤    
          } else if (ex instanceof ReferenceError) {
              // 處理 ReferenceError 錯誤            
          } else {
              // 處理其他型別的錯誤
          }
      }

      因為錯誤型別比較多,在判斷錯誤型別時並不好區分,因此一個比較好的解決方案是建立自己的錯誤型別,讓它繼承 Error這種做法的好處是自定義錯誤型別可以檢測自己的錯誤。

      function MyError (msg) {
          this.message = msg;
      }
      
      MyError.prototype = new Error();
  • Object 的鎖:
    Object 的鎖主要有三種:prevent extensionsealfreeze;

    1. preventExtensions & isExtensible

      preventExtensions: Object.prevenExtensions() 方法是阻止物件擴充套件;

      isExtensible: Object.isExtensible() 方式是檢查物件是否已經阻止了擴充套件。

      var person = {
          name: 'testName'
      };
      
      Object.preventExtensions(person);
      Object.isExtensible(person);            // true
      
      person.age = 25                         // 正常情況下會悄悄地失敗,嚴格模式下會報錯
    2. seal & isSealed

      seal: Object.seal() 方法是將物件密封,即不允許新增/刪除屬性或者方法;

      isSealed: Object.isSealed() 方式是檢查物件是否已經設定了密封狀態。

      var person = {
          name: 'testName'
      };
      
      Object.seal(person);
      Object.isSealed(person);                // true
      
      // 新增或刪除物件的屬性或者方法
      person.age = 25                         // 正常情況下會悄悄地失敗,嚴格模式下會報錯
      delete person.name                      // 正常情況下會悄悄地失敗,嚴格模式下會報錯
      
      // 修改物件的屬性
      person.name = 'Nicholas';               // 正常發生
    3. freeze & isFrozen

      freeze: Object.freeze() 方法是凍結物件,即不能新增/刪除/更改物件的屬性和方法;

      isFrozen: Object.isFrozen() 方式是檢查物件是否已經被凍結。

      var person = {
          name: 'testName'
      };
      
      Object.freeze(person);
      Object.isFrozen(person);            // true
      
      // 新增/刪除/修改物件的屬性或者方法
      person.age = 25                         // 正常情況下會悄悄地失敗,嚴格模式下會報錯
      delete person.name                      // 正常情況下會悄悄地失敗,嚴格模式下會報錯
      person.name = 'Nicholas';               // 正常情況下會悄悄地失敗,嚴格模式下會報錯

    由以上可知,物件的許可權的大小級關係是:preventExtensions < seal < freeze

  • 瀏覽器的相容問題:
    書中所說的大概的意思是,禁止使用特性推斷,建議使用特性檢測,不建議使用瀏覽器嗅探(user-agent)。

    // 特性檢測
    function setAnimation (cb) {
    
        // 標準
        if (window.requestAnimationFram) {
            return window.requestAnimationFram(cb);
    
        // Firefox
        } else if (window.mozRequestAnimationFram) {
            return window.requestAnimationFram(cb);
    
        // webkit
        } else if (window.webkitRequestAnimationFram) {
            return window.requestAnimationFram(cb);
    
        // Opera
        } else if (window.oRequestAnimationFram) {
            return window.requestAnimationFram(cb);
    
        // IE
        } else if (window.msRequestAnimationFram) {
            return window.requestAnimationFram(cb);
    
        // 瀏覽器都不支援的時候,使用 setTimeout 方法
        } else {
            setTimeout(cb, 0);
        }
    }

個人總結

關於這本書,其實我並不知道這本書會給我帶來多大的價值,但是卻堅定我的信心。通過通讀和細讀,加之寫了這篇回顧和總結,我收穫了在寫程式碼過程需要注意的和需要改進的地方,同時也得到了一些我意想不到的處理問題的方法。然而每個人都有自己的風格和理解,因此未來的 coding 之路,我會基於這本書的思想來做,但是多少會有些出入。總之,就是希望自己越來越好,努力成為牛逼閃閃的攻城獅之類的就不說了,至少要做到讓自己身後的家人幸福快樂。

2017-02-13 完!