跳到主要內容
黯羽輕揚每天積累一點點

《JavaScript 語言精粹》學習筆記

免費2015-04-30#JS#JavaScript语言精粹

《JavaScript 語言精粹》中的精粹

一、in 的用法

  1. for...in

枚舉一個對象的所有可枚舉屬性

  1. 檢測 DOM/BOM 屬性

    if ("onclick" in elem) {
        // 元素支持 onclick
    }
    if ("onerror" in window) {
        // window 支持 onerror
    }
    
  2. 檢測 js 對象的原型屬性(結合 hasOwnProperty() 函數)

    if ("attr" in obj && !obj.hasOwnProperty("attr")) {
        // obj 有原型屬性 attr
    }
    

注意:原型屬性會被同名的實例屬性屏蔽掉,所以無法檢測這樣的:

    function Super(){
        this.attr = 1;
    }
    function Sub(){
        this.attr = 2;
    }
    Sub.prototype = new Super();
    var obj = new Sub();
    alert(("attr" in obj && !obj.hasOwnProperty("attr")));  /// false
    

二、|| 運算符和&&運算符

  • || 可以用來填充默認值,例如:

     var obj = {};
     obj.name = "";  // 注意 js 有一堆假值:+0、-0、0、NaN、null、undefined、""、''、false
     var name = obj.name || "ayqy";
     alert(name);
     
    

P.S. 個人認為這個東西並不好用,除非能確定原屬性值不可能是任何形式的假值,否則默認值會覆蓋原值

  • && 可以用來避免從 undefined 值讀取屬性引起 TypeError,例如:

     var obj = {};
     //var attr = obj.obj.attr;   // 報錯,TypeError,不能從 undefined 值讀取屬性
     var attr = obj.obj && obj.obj.attr; // 只有 obj.obj 存在且不是假值時才會取其 attr
     
    

P.S. 個人認為和&&差不多,不如直接用 if 檢測,不僅能展現更清晰的邏輯流,還便於添加更多的檢測條件

P.S. 這兩種用法是在書的第一部分介紹的,可能更偏向於展示語言特性,並不是推薦用法

個人建議不要這樣用,當然,看到這樣的代碼要能明白其作用與漏洞

三、減少全局變量污染

  1. 用「命名空間」,即空對象,實質是把創建的全局變量減少到 1 個,比如 YUI 的 Y 對象,JQuery 的 JQuery 和$對象。。例如:

    var app = {};   // 命名空間:app
    app.Consts = {
        // 子命名空間:常量
        
        URL : {
            // 子子命名空間:URL
        }
    }
    app.Modules = {
        // 子命名空間:模塊
    }
    app.Data = {
        // 子命名空間:數據
    }
    
  2. 用 IIFE(匿名函數立即執行),實質完全不創建全局變量了,0 污染

    (function(){
        // Module1
    })();
    (function(){
        // Module2
    })();
    

缺點很明顯,無法實現對象緩存和共享,出了閉包就沒了

所以一般做法是結合命名空間和 IIFE,整體用命名空間組織起來,在模塊內部合適的地方用 IIFE

四、4 種調用模式

  • 方法調用模式:obj.fun(); 或者 obj"fun";

  • 函數調用模式:fun(); 此時 this 指向 global 對象

  • 構造器調用模式:new Fun(); 新對象的 prototype 指向 Fun 的 prototype,this 指向這個新對象

  • apply 調用模式:fun.apply(this, arrArgs);

五、關於 arguments 對象

arguments 對象不是數組對象,也不支持所有數組方法,只是一個有點特殊的普通對象,特殊在其 length 屬性(動態更新)

可以做如下測試:

    function fun(){
        var x = Object.prototype.toString.call(arguments);
        alert(x);
        var arr = [];
        alert(Object.prototype.toString.call(arr));
    };
    
    fun();
    // IE8:[object Object] 和 [object Array],Chrome:[object Arguments] 和 [object Array]
    // 不一樣不要緊,反正都不是 Array
    

一個小技巧:可以用 slice 方法把 arguments 對象轉換為數組,例如:

    function fun(){
        //arguments.sort();   // 報錯,不支持 sort() 函數
        var arr = Array.prototype.slice.call(arguments);    // 轉換
        arr.sort(); // 不報錯,說明轉換成功
        alert(arr);
    };
    
    fun(3, 2);  // 2, 3
    

注意:只有 slice 有這種奇效,concat 就沒有,雖然無參的 slice 和無參的 concat 用於數組的效果一樣(都是複製一份)

六、級聯(鏈式調用)的實現方式

讓沒有返回值的函數返回 this,所以支持鏈式調用,比如 JQuery 中的:

$("#adBlock").removeClass("active").hide();

七、構造函數調用方式不當會引起作用域污染

如果不用 new 操作符,直接調用構造函數,比如 Fun(); 此時 this 指向 global 屬性,即瀏覽器環境下的 window 對象,可能會污染全局作用域

八、把形參列表簡化為單一對象

例如:

function fun(height, width, margin, padding){   // 參數太長,順序記不住
    // do something
}

/*
 * @param {number} arg.height 高度
 * @param {number} arg.width 寬度 
 * @param {number} arg.margin 留白
 * @param {number} arg.padding 補白
 */
function fun(arg){  // 不用記參數順序了
    // do something
}

好處如下:

  1. 調用時不用再關心參數順序了,接口變得更加易用

  2. 可以直接傳個 JSON 對象進去

缺點:要寫一堆注釋說明具體需要哪些參數,因為通過一個 arg 完全看不出來

九、防偽對象(持久性的對象)

防偽對象的屬性可以被替換或者刪除,但該對象的完整性不會受到損害

也被稱為持久性的對象,一個持久性對象就是一個簡單功能函數的集合

P.S. 防偽對象的概念出現在書的 [函數化] 部分,目前還不能完全理解函數化想要表達的意思,把自己養肥了再看

函數化部分請查看 [黯羽輕揚:《JavaScript 語言精粹》之函數化](http://ayqy.net/blog/《JavaScript 語言精粹》之函數化/),內容已經補充完整了

十、數組

本質是鍵值對,也就是對象,所以並沒有訪問速度上的優勢

區別是 Array.prototype 提供了很多數組操作函數,此外還有特殊的 length 屬性

注意:

  1. 數組實際佔用的內存空間

    var arr = [1];
    arr[99] = 100;
    

此時 arr 指向的值並沒有佔用 100 個元素的空間,因為上面的結果相當於:

    var arr = {
        "0" : 1,
        "99" : 100
    };

2. length 屬性是可寫的

可以用 arr.length = n; 刪掉下標值 >= n 的所有元素

  1. 常見的 length 屬性用法

省計數器的方式:arr[arr.length] = value;

或者更簡單的:arr.push(value);

  1. 數組類型檢測

因為 typeof arr 返回"object",而且 instanceof 操作符在跨 frame 時失效,所以檢測數組類型不太容易

書上給了一種超麻煩的方式:

    function isArray(value){
        return value &&
            typeof value === "object" &&
            typeof value.length === "number" &&
            typeof value.splice === "function" &&
            !(value.propertyIsEnumerable("length"));
    }
    

其實有在作者寫書的時候還沒出現的簡單方法:

可以用 Object.prototype.toString.call(value) === '[object Array/Function...]' 來做類型檢查,也可以用來區分原生對象和自定義對象,例如:

    [object JSON]   // 原生 JSON 對象
    [object Object] // 自定義 JSON 對象
    

關於類型檢測的更多信息請查看 [黯羽輕揚:JS 學習筆記 11_高級技巧](http://ayqy.net/blog/JS 學習筆記 11_高級技巧/)

十一、正則表達式

js 的正則表達式功能並不完整,但基本夠用,比如只支持 3 種模式:g/i/m ~ 全局模式/忽略大小寫模式/多行模式

而且支持的特殊元字符(\d、\s 之類的)也比較少,但好在支持非捕獲型分組和正則環視,還是很不錯的

所以在 js 中使用正則表達式需要做更多的測試,更多關於正則表達式的信息請查看 黯羽輕揚:正則表達式學習筆記

還有一個不常用的點:RegExp 對象的屬性

  • global:是否開了 g 模式

  • ignoreCase:返回是否開了 i 模式

  • lastIndex:返回下一次 exec 匹配的起始位置

  • multiline:返回是否開了多行模式

  • source:返回正則表達式串

需要特別注意:用字面量方式創建的 RegExp 對象可能引用同一個實例,例如:

var regex1 = /abc/g;
var regex2 = /abc/g;
var str = "aaa\r\nabc\r\naaa";

alert([regex1.lastIndex, regex2.lastIndex]);    // 0, 0
regex1.test(str);
alert([regex1.lastIndex, regex2.lastIndex]);    // 8, 0
alert(regex1 === regex2);   // false

老版本瀏覽器最後兩行會輸出 8, 8 和 true,據說正則字面量共享實例是 ES3 的規定,本機���試發現 IE8 不存在問題,但理論上 IE6 就存在這個問題(出 IE6 的時候,ES5 還沒出)

十二、原生對象操作函數

P.S. 此處只介紹需要特別注意的點,完整的各種操作函數請查看 [黯羽輕揚:JS 學習筆記 1_基礎與常識](http://ayqy.net/blog/JS 學習筆記 1_基礎與常識/)

1. 數組操作函數

  1. arr1.push(arr2); 會把 arr2 作為單個元素插入,例如:

    var arr1 = [1];
    var arr2 = [2, 3];
    arr1.push(arr2);
    alert(arr1[2]); // undefined
    alert(arr1[1][1]);  // 3
    
  2. arr.shift() 比 arr.pop() 慢很多,因為刪掉首元需要更新所有元素的索引,而刪掉尾元不需要

  3. arr.unshift(),在數組頭部插入元素,IE6 返回 undefined,本來應該返回新數組的長度

2. 對象操作函數

obj.hasOwnProperty("hasOwnProperty") 返回 false

3. 正則表達式對象操作函數

regex.exec(str) 函數功能最強大,當然,執行速度也最慢

regex.test(str) 最簡單,最快,注意:不要開 g 模式,因為純屬浪費(test 只返回匹配/不匹配,一次掃描就夠了)

4. 字符串操作函數

  • str.replace(regex, fun); 是很好用的一個函數,可以用 regex 匹配目標部分,還可以用 fun 進一步處理匹配的部分

特別注意:如果 regex 沒有開 g 模式,那麼只替換第一個匹配部分,並不是全串替換

fun 的各個參數含義如下:

<table class="fullwidth-table"><tbody><tr><td class="header">Possible name</td><td class="header">Supplied value</td></tr><tr><td><code>match</code></td><td>The matched substring. (Corresponds to <code>$&amp;</code> above.)</td></tr><tr><td><code>p1, p2, ...</code></td><td>The <em>n</em>th parenthesized submatch string, provided the first argument to <code>replace()</code> was a <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp" title="The RegExp constructor creates a regular expression object for matching text with a pattern."><code>RegExp</code></a> object. (Corresponds to <code>$1</code>, <code>$2</code>, etc. above.) For example, if&nbsp;<code>/(\a+)(\b+)/</code>, was given, <code>p1</code> is the match for <code>\a+</code>, and&nbsp;<code>p2</code> for <code>\b+</code>.</td></tr><tr><td><code>offset</code></td><td>The offset of the matched substring within the total string being examined. (For example, if the total string was <code>'abcd'</code>, and the matched substring was <code>'bc'</code>, then this argument will be 1.)</td></tr><tr><td><code>string</code></td><td>The total string being examined.</td></tr></tbody></table>
  • str.replace(regex, replacement); 的 replacement 部分中的$有特殊含義:

    • $$:表示$,如果替換部分中有$需要用$來轉義

    • $&:表示整個匹配的文本

    • $n:例如$1, $2, $3... 表示捕獲到的文本

    • $`:表示位於匹配部分之前的文本

    • $':表示位於匹配部分之後的文本

更多詳細示例請查看 W3School:JavaScript replace() 方法

  • str.split(regex); 存在一個特例,需要特別注意

     var str = "a & b & c";    // &是分隔符
     var arr = str.split(/\s*(&)\s*/);   // 含捕獲
     var arr2 = str.split(/\s*&\s*/);    // 不含捕獲
     var arr3 = str.split(/\s*(?:&)\s*/);    // 含非捕獲
     alert(arr); // [a, &, b, &, c]
     alert(arr2);    // [a, b, c]
     alert(arr3);    // [a, b, c]
     
    

    如果不小心用了含捕獲型分組的正則表達式,結果可能與預期的不同

十三、糟粕

P.S. 這裡不打算原樣照搬書上的內容,只給出筆者同意的最糟糕的地方

  • 自動插入分號。確實不好,比如典型的 ASI 錯誤:

     function fun(){
         return
         {
             attr : 1;
         }
     }
     
     alert(fun().attr);  // TypeError: Cannot read property 'attr' of undefined
     
    
  • typeof。不好用,這是設計上的失誤,歷史原因

  • parseInt() 與八進制。一個很隱蔽的錯誤

     var year = "2015";
     var month = "08";
     var day = "09";
     
     alert(parseInt(year));  // IE8:2015 Chrome:2015
     alert(parseInt(month)); // IE8:0    Chrome:8
     alert(parseInt(day));   // IE8:0    Chrome:9
     
    

因為 0 開頭的數值被認為是八進制,省略第二個參數的 parseInt() 會把 08/09 當作八進制來解析,所以結果是 0

處理時間日期太容易引起解析錯誤了,所以最好不要省略 parseInt() 的第二個參數

P.S. Chrome 中正常了,可能是某個 ES 版本對 parseInt() 做了修改,Chrome 做了實現。所以糟粕並不是永遠存在的,當然,是不是糟粕也要看程序員怎麼用。。

  • Unicode。js 對 Unicode 的支持不好,雖然也是歷史原因,但不支持就是不支持

P.S. 出 js 的時候 Unicode 自己也沒想到能有 100 萬個字符,js 的字符是 16 位的,能對應前 65536 個 Unicode 字符,而 Unicode 為了擴容,就把剩下的每個字符都用一對字符來表示,但 js 會把這樣的字符當作兩個字符,所以。。

十四、雞肋

P.S. 同上,只列筆者同意的

  • continue 性能低。可以 if 消除

  • 位運算性能低。(其它的書都沒有提到這一點)因為要折騰:double -> int -> 做位運算 -> double

  • new 到底應該用還是不用。用的話可能污染作用域

  • void 運算符。單目運算,返回 undefined,所以可以用來實現 IIFE,例如:

     void function(){
         // do something
     }();
     
    

作者建議不要用 void,因為沒什麼人知道它是做什麼的

十五、JSON

整數的首位不能是 0,道格拉斯(發明 JSON 的大爺)本人說的,數值可以是整數、實數或者科學計數,但首位不能是 0,為避免八進制歧義

測試代碼如下:

var json = '{"number" : 9}';
var obj = JSON.parse(json);
alert(obj.number);  // 9

var json = '{"number" : 09}';   // 注意前導 0
var obj = JSON.parse(json);
alert(obj.number);  // 報錯
// Chrome:SyntaxError: Unexpected number
// IE8:語法錯誤
// FF:SyntaxError: JSON.parse: expected ',' or '}' after property value in object

參考資料:

  • 《JavaScript 語言精粹》

評論

暫無評論,快來發表你的看法吧

提交評論