編寫高質(zhì)量Javascript的68個方法筆記+自我理解

Effective JavaScript

讓自己習(xí)慣javaScript

1 - 了解你使用的javaScript版本

ES5引入的'嚴(yán)格模式',在js文件連接使用時可能導(dǎo)致問題.

如:

//file1.js
"use strict";
function f(){
  //...
}
//file2.js no strict-mode
function f(){
  //arguments為方法自帶的,在嚴(yán)格模式下重新定義會報錯
  var arguments = [];
}
//兩個倒過來,非嚴(yán)格模式的在前面,又會導(dǎo)致嚴(yán)格模式文件嚴(yán)格模式無效了.

解決方案:

  1. 避免使用嚴(yán)格模式的js文件和不使用嚴(yán)格模式的js文件連接使用.
  2. 通過(function(){...})();的方式包裹自身,在被連接后,都會被獨立的解釋執(zhí)行.缺點是,這種方法會導(dǎo)致文件的內(nèi)定義的全局變量,方法等不會被視作全局的.
2 - 理解javaScript浮點數(shù)

tips:

  1. javaScript的數(shù)字都是雙精度浮點數(shù),即double(64位浮點數(shù)).可精確表示-253~253之間的數(shù).

    數(shù)字類型 .toString(n);可以把數(shù)字轉(zhuǎn)換成對應(yīng)進制展示 注意數(shù)字后要加一個空格

    如 console.log(29 .toString(15)); => "1e"

  2. javaScript的整數(shù)僅僅是雙精度浮點數(shù)的一個子集

  3. 位運算會將數(shù)字視為32位有符號整數(shù)(會直接舍棄后面的小數(shù)位),計算完后把結(jié)果轉(zhuǎn)換成浮點數(shù)

    8.1 | 1 => 1000.0001100110011001100110011001100110011001100110011 | 1 => 1000 | 1 => 1001 => 1001(浮點數(shù))

  4. 盡量避免使用浮點數(shù)運算,可以的話轉(zhuǎn)換成整數(shù)計算.如金錢計算都乘以100,以分為單位就不會有小數(shù),就不存在浮點精度誤差了.

3 - 當(dāng)心隱式類型轉(zhuǎn)換

由于javaScript對類型錯誤極其寬容導(dǎo)致.JavaScript會按照多種多樣的自動轉(zhuǎn)換協(xié)議將值強制轉(zhuǎn)換為期望的類型.

  1. 算數(shù)運算符( + - * / % )會嘗試把兩邊都轉(zhuǎn)換成數(shù)字(+ 既重載了數(shù)字+也重載了字符串加法,具體是數(shù)字相加還是字符串相連取決于參數(shù)類型, 從左到右)

    • null在算數(shù)運算中會被隱式轉(zhuǎn)換成0

    • 未定義的變量會變轉(zhuǎn)換成NaN

      • 如果知道是數(shù)字,isNaN可以判斷是否是NaN;但如果不是數(shù)字,isNaN無法區(qū)分.

        如: isNaN("foo")=>true ,同理undefined,{},{valueOf:"foo"}都會返回true

        tips:可以利用NaN是Js中唯一不等于自身的值這個特性, 用 x !== x判斷是否為真的NaN

  2. 位運算( ~ & ^ | << >> >>> )會把操作數(shù)轉(zhuǎn)換成數(shù)字,且會轉(zhuǎn)換成32位整數(shù)

  3. 對象隱式轉(zhuǎn)換toString()方法轉(zhuǎn)換成字符串,valueOf方法轉(zhuǎn)換成數(shù)字.除了 "+"外,其他算數(shù)運算符會調(diào)用valueOf的方法轉(zhuǎn)換成數(shù)字計算;位運算也會調(diào)用valueOf轉(zhuǎn)換成數(shù)字計算;而加法的隱式轉(zhuǎn)換常常會出人意料.所以建議對象有valueOf方法時,同時也要定義toString方法,返回與valueOf值一致的字符串結(jié)果.(當(dāng)然,最好避免使用對象的valueOf方法)(見5)

    請留意:

    var x = "s";
    x == "s" //true
    x === "s" //true
    new String("s") == new String("s") //false
    new String("s") === new String("s") //false
    x == new String("s") //true
    x === new String("s") //false
    
  4. 真值轉(zhuǎn)換.JavaScript只有7個假值: false, 0, -0, "", NaN, null, undefined.其他都為真值.

    if, || 和 && 等運算符時,將值轉(zhuǎn)換成真值計算.

  5. 判斷是否為undefined的兩種方式

    • typeof x === "undefined"
    • x === undefined
4 - 原始類型優(yōu)于封裝對象

除對象外,JavaScript有5個原始值類型:布爾值boolean,數(shù)字number,字符串string,nullundefined

注意: typeof null會返回object

布爾值,數(shù)字,字符串有對應(yīng)的包裝類型Boolean,Number,String

隱式轉(zhuǎn)換②: 隱式封裝.

舉例: "hello".toUpperCase(); //"HELLO"

會生成一個新的String對象,調(diào)用方法返回結(jié)果,且每次隱式封裝都會產(chǎn)生一個新的對象,

所以對原始值設(shè)置屬性是沒有任何意義的.

所以:

"hello".someProperty = 17;
"hello".someProperty; //undefined

第一次產(chǎn)生的對象和第二次調(diào)用時并不相同,所以第二次會返回undefined

5 - 避免對混合類型使用 == 運算符

當(dāng)兩個參數(shù)屬于同一類型時,使用 == 和 === 運算符沒有區(qū)別.

參數(shù)類型1 參數(shù)類型2 強制轉(zhuǎn)換
null undefined 不轉(zhuǎn)換,總返回true
null/undefined 非null/非undefined 不轉(zhuǎn)換,總返回false
原始類型string,number,boolean Date對象 原始類型轉(zhuǎn)換成數(shù)字,Date對象轉(zhuǎn)換成原始類型(優(yōu)先嘗試toString,再嘗試valueOf方法)
原始類型string,number,boolean 非Date對象 原始類型轉(zhuǎn)換成數(shù)字,非Date對象轉(zhuǎn)換成原始類型(優(yōu)先嘗試valueOf,再嘗試toString方法)
原始類型string,number,boolean 原始類型string,number,boolean 原始類型轉(zhuǎn)換成數(shù)字

解決方案:

  1. 盡量使用 === 運算符
  2. 比較不同類型時,自己定義顯示的強制轉(zhuǎn)換方法,讓比較的行為更清晰.
6 - 了解分號插入的局限

分號插入規(guī)則:

  1. 分號會在 } 標(biāo)記之前, 一個或多個換行之后, 程序輸入的結(jié)尾處被插入.

    (即 你可以省略一行后、一個代碼塊和一段程序結(jié)束處的分號)

    function square(x){
       var n = +x
       return n * n
    }
    function area(r) { r  = +r; return Math.PI * r * r}
    function add1(x) {return x+1}
    
  2. 分號僅在隨后的輸入標(biāo)記不能解析時插入.

    如:

    a = b 
    (f());
    
    系統(tǒng)會判斷 a = b(f());可以正常解析,所以此處第一行不能省略分號
    

5個明確有問題的開頭字符: ( [ + - /

  • 解決方案:在出現(xiàn)這五個開頭字符時,顯式地在前面加上分號

javaScript限制產(chǎn)生式(restricted production)不允許兩個字符之間出現(xiàn)換行時,會強制插入分號

包括:

  • return語句

    return
    {};
    

    等價于

    return ;
    {}
    ;
    
  • throw語句

  • 帶有顯式標(biāo)簽的break或continue語句

  • 后置自增或自減運算符

    a
    ++
    b
    

    等價于

    a; ++b; //因為后置++ 就會在a后面強制加上分號
    
  1. 分號不會作為分隔符在for循環(huán)空語句的頭部被自動插入,空循環(huán)體的while循環(huán)也需要顯式的分號

    for(var i = 0, total = 1    //會報錯,不會自動插入分號
    i < n
    i++){}
    
    function infiniteLoop(){ while(true) ; }//沒有這個分號會報錯
    
7 - 視字符串為16位的代碼單元序列

Unicode為所有文字每個字符都分配了唯一整數(shù),整數(shù)介于0和1114111之間,稱為代碼點.

Unicode允許多個不同二進制編碼的代碼點.目前最流行的有UTF-8,UTF-16,UTF-32.

將代碼點與其編碼元素一對一映射起來,稱為一個代碼單元.

由于歷史原因,JavaScript字符串的元素是一個16位的代碼單元,字符串屬性和方法(如length,charAt,charCodeAt)都是基于代碼單元層級,而不是代碼點層級.所以每當(dāng)字符串包含輔助平面的代碼點時(即需要用兩個代碼單元去表示一個代碼點時),javascript將每個代碼點表示為兩個元素而不是一個(一對UTF-16代理對的代碼點).(正則表達式模式也一樣有這個問題)

當(dāng)出現(xiàn)兩個代碼單元表示一個代碼點的時候,length,charAt,charCodeAt等方法在執(zhí)行時,會出現(xiàn)意想不到的結(jié)果.

而例如sendcodeURI,decodeURI,encodeURIComponent和decodeURIComponent等URI操作函數(shù)的ECMAScript庫正確的處理了代理對,就不會有問題.

tips:

  1. 使用第三方的庫編寫可識別代碼點的字符串操作.
  2. 每當(dāng)使用一個含有字符串操作的庫時,都要查閱該庫文檔,看他如何處理代碼點的整個范圍.

變量作用域

8 - 盡量少用全局變量

定義全局變量會污染共享的公共命名空間,可能導(dǎo)致意外的命名沖突;不利于模塊化,因為他會導(dǎo)致程序獨立組件之間不必要的耦合.

tips:

  1. 避免聲明全局變量,盡量使用局部變量

  2. 避免對全局對象添加屬性

  3. 使用全局對象來做平臺特性檢測

    如:ES5引入的全局JSON對象(用于讀寫JSON格式的數(shù)據(jù)),檢測當(dāng)前環(huán)境是否提供了這個對象

    if(!this.JSON){
        //如果沒有這個對象,就定義一個.
       this.JSON = {
           parse: ...,
           stringify: ...      
       }
    }
    
9 - 始終聲明局部變量

不使用var,let,const聲明變量,該變量會隱式的轉(zhuǎn)變?yōu)槿肿兞?/p>

tips:

  1. 使用lint工具幫助檢查未綁定的變量
10 - 避免使用with

with的簡單介紹:

//with 擴展一個語句的作用域鏈 ES5的嚴(yán)格模式已禁用with
//with 出現(xiàn)的目的是減少變量的長度,減少不必要的指針路徑解析運算
with(expression){//將表達式添加到評估語句時使用的作用域鏈上
    statement//任何語句
}
//舉例
var a, x, y;
var r = 10;

with(Math){//Math被作為默認對象,PI,cos,sin前面不需要添加命名空間
    a = PI * r * r;
    x = r * cos(PI);
    y = r * sin(PI / 2);
}
//with 使程序查找變量時都會先去指定對象中找,那些不屬于他的屬性查找速度會很慢
//另外,with導(dǎo)致了語義不明的嚴(yán)重問題,如下:
function f(x,o){
  with(o){
        print(x);//方法f獲取的參數(shù)x可能取到值,可能為undefined;能取到時,可能在o上,也可能是函數(shù)的第一個參數(shù)(o中沒有x這個屬性的話),人力無法預(yù)測此處的值
    }
}

tips:

  1. 避免使用with語句.不僅性能差,而且有語義問題

  2. 使用簡單的變量名代替重復(fù)訪問的對象;想要實現(xiàn)with的初衷,可以將局部變量顯式的綁定到相關(guān)屬性上

    如:

    function(x,y){
        var min = Math.min, round = Math.round, sqrt = Math.sqrt;
        return min(round(x), sqrt(y));
    }
    
11 - 熟練掌握閉包

JavaScript的函數(shù)值包含了比調(diào)用他們時執(zhí)行所需的代碼需要的信息以外的更多的信息.

他們可以獲取在其封閉作用域的變量

那些在其所涵蓋的作用域內(nèi)跟蹤變量的函數(shù),稱為閉包.

閉包的三個事實:

? //此處的外部指的是閉包函數(shù)的外部

  1. JavaScript允許你引用當(dāng)前函數(shù)以外定義的變量.=>閉包可以引用定義在其外部作用域的變量
  2. 即使外部函數(shù)已經(jīng)返回,當(dāng)前函數(shù)仍可以引用外部函數(shù)所定義的變量.=>閉包比創(chuàng)建他們的函數(shù)有更長的生命周期
  3. 閉包可以更新外部變量的值

warning:閉包存儲的外部變量的引用,而不是值!!!

舉例:

//假設(shè)一個獲取全名的方法
//函數(shù)柯里化
function initLastname(lastname){
  return function(firstname){
     return `${lastname} ${firstname}`;
 }
}
//此處獲取定義了姓的方法
const getNameStartWithSheng = initLastname('sheng');
getNameStartWithSheng('wei');// sheng wei
getNameStartWithSheng('jie');// sheng jie
//重新定義一個新的方法...
const getNameStartWithLi = initLastname('li');
12 - 理解變量聲明提升

tips:

  1. javaScript隱式地提升聲明部分到封閉函數(shù)的頂部,而把賦值留在原地;提升方法聲明到頂部,而不提升方法調(diào)用.

  2. 在一個作用域中重復(fù)聲明變量,會被視為單個變量.

  3. 考慮手動提升局部變量,從而避免混淆.

舉例:

console.log(a);//undefined        不會出現(xiàn)Uncaught ReferenceError錯誤
// console.log(b);//打開的話會出現(xiàn)Uncaught ReferenceError: b is not defined
c();//"c"     不會出現(xiàn)Uncaught ReferenceError錯誤
var a = "a";
function c(){
  console.log("c");
}

因為上面的內(nèi)容等效于

var a;
function c(){
  console.log("c");
}
console.log(a);
//console.log(b);
c();
a = "a";
13 - 使用立即調(diào)用的函數(shù)表達式創(chuàng)建局部作用域
function wrapElements(a){
    var result = [];
    for(var i = 0, n = a.length; i < n; i++){
        result[i] = function(){return a[i];};
    }
    return result;
}
var wrapped = wrapElements([10,20,30,40,50]);
var f = wrapped[0];
console.log(f());//undefined        此處為什么不是10而是undefined?

原因: 閉包是由函數(shù)與對其狀態(tài)即詞法環(huán)境(lexical environment)的引用共同構(gòu)成.所以上面產(chǎn)生的每一個閉包都綁定了同一個詞法環(huán)境! 即每個函數(shù)都為 return a[5],因為索引越界所以返回undefined

解決方法:

  1. 使用更多的閉包,讓回調(diào)不再共享同一個詞法環(huán)境

    function wrapElements(a){
       var result = [];
       for(var i = 0, n = a.length; i < n; i++){
           result[i] = returnValue(a[i]);//此處多一層閉包,強制不共享同一個詞法環(huán)境
       }
       return result;
    }
    function returnValue(value){
       return function(){return value;}
    }
    var wrapped = wrapElements([10,20,30,40,50]);
    var f = wrapped[0];
    console.log(f());//10
    
  2. 創(chuàng)建一個嵌套函數(shù)并立即調(diào)用它來創(chuàng)建一個局部作用域

    warnings:要當(dāng)心函數(shù)中包裹代碼塊導(dǎo)致代碼塊的微妙變化.

    • 代碼塊不能包含break; continue語句.是不合法的
    • 代碼塊如果引用了this或特別的arguments變量,這種方式會改變他們的含義!
    function wrapElements(a){
        var result = [];
        for(var i = 0, n = a.length; i < n; i++){
           (function(){
               var j = i;
               result[i] = function(){return a[j];};
           })();
            //此處也可以是如下形式,將局部變量作為實參傳入
            //(function(j){
           //  result[i] = function(){return a[j];};
           //})(i);
        }
        return result;
    }
    var wrapped = wrapElements([10,20,30,40,50]);
    var f = wrapped[0];
    console.log(f());//10
    
  3. 使用ES6 let關(guān)鍵字,綁定塊作用域

    function wrapElements(a){
        var result = [];
        //僅僅是此處聲明i時使用let.因為let不會提升變量的聲明.每個閉包都綁定了塊作用域的變量
        for(let i = 0, n = a.length; i < n; i++){
            result[i] = function(){return a[i];};
        }
        return result;
    }
    var wrapped = wrapElements([10,20,30,40,50]);
    var f = wrapped[0];
    console.log(f());//10
    
14 - 當(dāng)心命名函數(shù)表達式笨拙的作用域

缺陷1:

命名函數(shù)表達式,即 function xxx(){};

常用于調(diào)試.在對Error對象的棧跟蹤功能中,函數(shù)表達式的名稱通常作為其入口使用.但是命名函數(shù)表達式是作用域和兼容性問題臭名昭著的來源.

ES3中,JavaScript引擎被要求將命名函數(shù)表達式的作用域表示為一個對象,這個對象只有單個屬性,將函數(shù)名和函數(shù)自身綁定起來,但是這個對象也繼承了Object.prototype的屬性.因此,就把Object.prototype中的所有屬性引入了作用域.

var constructor = function(){return null;};
var f = function f(){
    return constructor();
};
f();//{} (in ES3 environments)
//因為命名函數(shù)表達式在其作用域內(nèi)集成了Object的構(gòu)造函數(shù)
這種情況刪除函數(shù)表達式名即可
即
var f = function(){
    return constructor();
}

//存在:一些不符合標(biāo)準(zhǔn)的js環(huán)境中,即使刪除了函數(shù)表達式名,調(diào)用f()依舊會返回一個空對象.

避免方式:

在任何時候都避免對Object.prototype中添加屬性,以及使用與Object.prototype屬性同名的局部變量.

缺陷2:

對命名函數(shù)表達式的聲明進行提升.

//在一些不符合標(biāo)準(zhǔn)的環(huán)境中
var f = function g(){return 17;};
g();//17
//甚至這兩個對象是兩個不同的對象,導(dǎo)致了不必要的內(nèi)存分配

解決辦法:

創(chuàng)建一個與函數(shù)表達式同名的局部變量并賦值為null;//確保重復(fù)的函數(shù)會被垃圾回收.

var f = function g(){return 17;};
var g = null;

tips:

  1. 在Error對象和調(diào)試器中使用命名函數(shù)表達式改進棧跟蹤.

  2. 在ES3和有問題的js環(huán)境中謹(jǐn)記函數(shù)表達式作用域會被Object.prototype污染.//ES5中已解決bug

  3. 謹(jǐn)記在錯誤百出的js環(huán)境中會提升命名函數(shù)表達式聲明,導(dǎo)致重復(fù)存儲

  4. 考慮避免使用命名函數(shù)表達式或在發(fā)布前刪除函數(shù)名

    => 如果代碼發(fā)布到正確實現(xiàn)的ES5環(huán)境,就不必擔(dān)心上述問題.

15 - 當(dāng)心局部塊函數(shù)聲明笨拙的作用域
function f(){return "global"};
function test(x){
    var result = [];
    if(x){
        function f(){return "local";};
        result.push(f());
    }
    result.push(f());
    return result;
}
test(true);//?
test(false);//?

//在ES5及以前,不存在塊級作用域,test內(nèi)部的f()作用域是整個函數(shù).
//因此,結(jié)果為["local","local"]和"["local"]

//在ES6中,存在塊級作用域.
//如上代碼在執(zhí)行到result.push(f())時,會報f is not a function的錯
//將內(nèi)部修改為 let f = function(){return "local";};則返回["local","global"]和"["global"]
16 - 避免使用eval創(chuàng)建局部變量

如: eval("var y = 'hello'; ");這一句在被調(diào)用后才會執(zhí)行,將y變量加入到作用域.

tips:

  1. 避免使用eval函數(shù)創(chuàng)建的變量污染調(diào)用者的作用域.

  2. 如果eval函數(shù)代碼可能創(chuàng)建全局變量,可以將此調(diào)用封裝到嵌套的函數(shù)中,以防止作用域污染.

    var y = "global";
    function test(src){
       (function(){eval(src);})();
       return y;
    }
    test("var y = 'local';"); //"global"
    test("var z = 'local';"); //"global"
    
(remain learning)17 - 間接調(diào)用eval函數(shù)優(yōu)于直接調(diào)用

eval函數(shù)不僅僅是一個函數(shù),還可以訪問調(diào)用它時的整個作用域.

盡可能使用間接的方式調(diào)用eval函數(shù).

兩種調(diào)用eval的方式:

  1. 直接調(diào)用

    var x = "global";
    function test(){
       var x = "local";
       return eval("x");//direct eval
    }
    test(); //"local",此時調(diào)用時具有完全訪問局部作用域的權(quán)限
    
  2. 間接調(diào)用

    var x = "global";
    function test(){
       var x = "local";
       var f = eval;//綁定eval函數(shù)到另一個變量名
       return f("x");//通過變量名調(diào)用函數(shù),會使代碼失去對局部作用域訪問的能力
    }
    test();//"global"
    
    (0,eval)(src);
    //(,)表達式序列運算符總會返回最后一項,所以前面無論是0還是其他字面量都沒關(guān)系,都會返回eval,整個表達式被視為間接調(diào)用eval函數(shù)
    //直接使用evel(src);左側(cè)被視為一個引用
    //(0,eval)(src);左側(cè)被視為一個值
    

使用函數(shù)

18 - 理解函數(shù)調(diào)用、方法調(diào)用及構(gòu)造函數(shù)調(diào)用之間的不同

在javaScript中,他們只是單個構(gòu)造對象的三種不同的使用模式

函數(shù)調(diào)用

function hello(username){
    return "hello, " + username;
}
hello("keyser Soze");//"hello, keyser Soze"

方法調(diào)用

var obj = {
    hello: function(){
        return "hello, " + this.username;
    },
    username: "hans Gruber"
};
obj.hello();//"hello, hans Gruber"http://方法調(diào)用需要對象來調(diào)用,直接調(diào)用就時函數(shù)調(diào)用了
//注意以下情況
var obj2 = {
    hello: obj.hello,
    username: "Boo Radley"
};
obj2.hello();//"hello, Boo Radley"
//方法調(diào)用中,由調(diào)用表達式自身來確定this變量的綁定. 調(diào)用表達式為obj2.hello()所以this指向obj2,而不是obj
//此處先略過call,bind,apply

構(gòu)造函數(shù)調(diào)用

function User(name, password){
    this.name = name;
    this.password = password;
}
var u = new User("sfalken","xxx");//構(gòu)造函數(shù)需要通過new運算符調(diào)用
u.name;//"sfalken"
//構(gòu)造函數(shù)調(diào)用一個全新的對象作為this變量的值,并隱式地返回這個新對象作為調(diào)用的結(jié)果,即 產(chǎn)生的新對象傳遞給了u.構(gòu)造函數(shù)的主要職責(zé)時初始化該新對象.
19 - 熟練掌握高階函數(shù)

高階函數(shù): 將函數(shù)作為參數(shù)或返回值的函數(shù).

學(xué)會發(fā)現(xiàn)可以被高階函數(shù)取代的常見編碼模式.

舉例:

[3,1,4,1,5,9].sort(function(x, y){
    if(x < y)return -1;
    if(x > y)return 1;
    return 0;
});//1,1,3,4,5,9
//此處定義了一個判斷數(shù)字大小的方法作為參數(shù)傳遞
20 - 使用call方法自定義接收者來調(diào)用方法

func.call(thisArg [, arg1[,arg2...]])

第一個參數(shù)顯式地提供了接收者對象.

tips:

  1. 使用call方法自定義接收者來調(diào)用函數(shù)

  2. 使用call方法可以調(diào)用在給定的對象中不存在的方法

  3. 使用call方法定義高階函數(shù)允許使用者給回調(diào)函數(shù)指定接收者

    舉例:

    var table = {
       entries: [],
       addEntry: function(key, value){
           this.entries.push({key:key, value: value});
       },
       forEach: function(f, thisArg){
           var entries = this.entries;
           for(var i = 0, n = entries.length; i < n; i++){
               var entry = entries[i];
               //回調(diào)函數(shù)f,并指定接收者為thisArg
               f.call(thisArg, entry.key, entry.value, i);
           }
       }
    }
    //如table2賦值table1的內(nèi)容
    table1.forEach(table2.addEntry, table2);
    //如果call的第一個參數(shù)為null或undefined,則內(nèi)部this指向window
    
    call, apply, bind

    call和apply的區(qū)別是參數(shù)提供的方式

    bind與call,apply的區(qū)別是,bind只綁定,而call和apply會立即調(diào)用

21- 使用apply 方法通過不同數(shù)量的參數(shù)調(diào)用函數(shù)

func.apply(thisArg, [argsArray])

apply可以處理任意數(shù)量的參數(shù)

apply與call十分類似,區(qū)別就是參數(shù)為參數(shù)數(shù)組傳入,而call從第二個參數(shù)開始有0至多個參數(shù)

22 - 使用arguments創(chuàng)建可變參數(shù)的函數(shù)

JavaScript給給個函數(shù)都隱式提供了一個名為arguments的局部變量.可以用索引獲取方法的每個實參(arguments對象并不是一個數(shù)組),并且該對象還有一個length屬性來指示參數(shù)的個數(shù).

tips:

  1. 使用隱式的arguments對象實現(xiàn)可變參數(shù)的函數(shù)
  2. 考慮對可變參數(shù)的函數(shù)提供一個額外的固定元數(shù)的版本,從而使使用者無需借助apply方法
23 - 永遠不要修改arguments對象
function callMethod(obj, method){
    var shift = [].shift;
    shift.call(arguments);
    shift.call(arguments);
    return obj[method].apply(obj, arguments);
}
var obj = {
    add: function(x, y){return x + y;}
};
callMethod(obj, "add", 17, 25);
//error: cannot read property "apply" of undefined
//arguments對象并不是函數(shù)參數(shù)的副本.所有命名參數(shù)都是arguments對象中對應(yīng)索引的別名
//=> 參數(shù)obj依舊是arguments[0]的別名,method依舊是arguments[1]的別名
//因此此處調(diào)用的不是obj['add']而是17['25']
//=>arguments對象和函數(shù)的命名參數(shù)之間的關(guān)系非常脆弱.所以不建議修改arguments對象
//對比嚴(yán)格模式下
function strict(x){
    "use strict";
    arguments[0] = "modified";
    console.log(arguments[0],x);//modified unmodified
    return x === arguments[0];
}
function nostrict(x){
    arguments[0] = "modified";
    console.log(arguments[0],x);//modified modified
    return x === arguments[0];
}
console.log(strict("unmodified"));//false
console.log(nostrict("unmodified"));//true
//非嚴(yán)格模式下,arguments和函數(shù)命名參數(shù)都修改了,而嚴(yán)格模式下,命名參數(shù)沒被修改
//嚴(yán)格模式下,不支持對其arguments對象取別名,即 此處x不是arguments[0].所以嚴(yán)格模式下返回的結(jié)果為false

tips:

使用[].slice.call(arguments)將arguments復(fù)制到一個真正的數(shù)組中再進行修改.

24 - 使用變量保存arguments的引用
//實現(xiàn)一個迭代器
function values(){
    var i = 0, n = arguments.length;
    return {
        hasNext: function(){
            return i < n;
        },
        next: function(){
            if(i >= n ){
                return;
            }
            return arguments[i++];
        }
    }
}
var it = values(1,4,1,4,2,1,3,5,6);
//console.log(it);
console.log(it.next());//undefined
console.log(it.next());//undefined
console.log(it.next());//undefined
//原因是一個新的arguments變量被隱式地綁定到每個函數(shù)體內(nèi)
//it對象的next方法有自己的arguments對象,且為null,打印時就成了undefined

//解決方案:
//在我們需要的arguments對象的作用域中,使用一個變量去綁定,引用這個變量即可
//?感覺可以理解為閉包?訪問作用域內(nèi)的參數(shù),因為it的方法中有本地的arguments對象了,所以會優(yōu)先使用本地的arguments,而此處添加一個引用,在it的方法中不再擁有,就不會出問題了.
function values(){
+   var i = 0, n = arguments.length, a = arguments;
-   var i = 0, n = arguments.length;
    return {
        hasNext: function(){
            return i < n;
        },
        next: function(){
            if(i >= n ){
                return;
            }
+           return a[i++];
-           return arguments[i++];
        }
    }
}
var it = values(1,4,1,4,2,1,3,5,6);
//console.log(it);
console.log(it.next());//undefined
console.log(it.next());//undefined
console.log(it.next());//undefined

tips:

  1. 當(dāng)引用arguments時當(dāng)心函數(shù)嵌套層級
  2. 綁定一個明確作用域的引用到arguments變量,從而可以在嵌套的函數(shù)中引用他
25 - 使用bind方法提取具有確定接收者的方法
var buffer = {
    entries:[],
    add: function(s){
        console.log(arguments);//Arguments(3) ["867", 0, Array(3), callee: ?, Symbol(Symbol.iterator): ?]
        this.entries.push(s);
    },
    concat:function(){
        return this.entries.join("")
    }
};
var source = ["867","-","5309"];

source.forEach(buffer.add);// Cannot read property 'push' of undefined
console.log(buffer.concat());

//原因:此處buffer.add的接收者不是buffer對象
//forEach方法的實現(xiàn)使用全局對象作為默認的接收者,所以此處傳遞給了全局對象,全局對象沒有entries,所以報錯
//arr.forEach(callback(currentValue [, index [, array]])[, thisArg]);
//thisArg為null或者undefined時,this都指向全局對象

//解決方案:
//1.forEach提供了綁定接收者的參數(shù)
source.forEach(buffer.add, buffer);
//2.bind方法綁定this到buffer上
source.forEach(buffer.add.bind(buffer));

26 - 使用bind方法實現(xiàn)函數(shù)柯里化

函數(shù)柯里化: 創(chuàng)建一個固定需求參數(shù)子集的委托函數(shù),即只傳遞給函數(shù)一部分參數(shù)來調(diào)用它,讓它返回一個函數(shù)去處理剩下的參數(shù)(11 - 數(shù)量掌握閉包里有)

function simpleURL(protocol, domain, path){
    return protocol + "://" + domain + "/" + path;
}
var urls = paths.map(function(path){
    return simpleURL("http", siteDomain, path);
});

等效于

var urls = paths.map(simpleURL.bind(null, "http", siteDomain));
//因為不需要引用this變量,所以bind的第一個參數(shù)可以是任何值,使用null或undefined為習(xí)慣用法
27 - 使用閉包而不是字符串來封裝代碼(?現(xiàn)在ES6有塊級作用域了,感覺答案更顯而易見了?)

//這里說的字符串來封裝代碼使用的是eval(str)的方法來執(zhí)行

不使用字符串的原因:

  1. eval內(nèi)的代碼要執(zhí)行到這一行才會生效,使用的參數(shù)可能跟想象中的有差異.
  2. 高性能引擎很難優(yōu)化eval字符串里面的代碼

tips:

  1. 當(dāng)將字符串傳遞給eval函數(shù)以執(zhí)行他們的API時,絕不要在字符串內(nèi)包含局部變量的引用
  2. 接受函數(shù)調(diào)用的API優(yōu)于使用eval函數(shù)執(zhí)行字符串的API
28 - 不要信賴函數(shù)對象的toString方法

//此處指的是用toString試圖獲取函數(shù)的源代碼

tips:

  1. 當(dāng)調(diào)用函數(shù)的toString方法時,并沒有要求js引擎能夠精確地獲取到函數(shù)的源代碼
  2. 由于在不同引擎下調(diào)用的toString方法的結(jié)果可能不同,所以絕不要信賴函數(shù)toString獲得的源代碼的詳細細節(jié).
  3. toString方法的執(zhí)行結(jié)果并不會暴露存儲在閉包中的局部變量值.
  4. 通常情況下,避免使用函數(shù)對象的toString方法
(remain learning)29 - 避免使用非標(biāo)準(zhǔn)的棧檢查屬性

許多javaScript環(huán)境提供檢查調(diào)用棧的功能.每個arguments對象都含有兩個額外的屬性,arguments.callee(除了允許匿名函數(shù)遞歸地引用自身之外,沒有用途)和arguments.caller(這個屬性不可靠,大多數(shù)環(huán)境已移除),前者指向使用該arguments對象被調(diào)用的函數(shù),后者指向調(diào)用該arguments對象的函數(shù).

tips:

  1. 避免使用非標(biāo)準(zhǔn)的arguments.caller和arguments.callee屬性,因為他們不具有良好的移植性.
  2. 避免使用非標(biāo)準(zhǔn)的函數(shù)對象caller屬性,因為在包含全部棧信息方面,是不可靠的.
  3. 最好的策略是使用交互式的調(diào)試器.

對象和原型

30 - 理解prototype, getPrototypeOf和_proto_之間的不同

User.prototype指向由new User()創(chuàng)建的對象的原型

Object.getPrototypeOf(user)是ES5中用來獲得user對象的原型對象的標(biāo)準(zhǔn)方法

user._proto_是獲取user對象的原型對象的非標(biāo)準(zhǔn)方法

原型圖.jpg

User構(gòu)造函數(shù)有一個默認的prototype屬性指向原型對象.

由new User()創(chuàng)建的user對象繼承User原型對象.當(dāng)查找user對象屬性時,如果user沒有,就會到他繼承的User原型對象中去找.

ES5提供了Object.getPrototypeOf()的方法去獲取對象的原型對象

Object.getPrototypeOf(user) === User.prototype; //true

一些環(huán)境提供了非標(biāo)準(zhǔn)的方法獲取對象的原型

user.__proto__ === User.prototype; //true
31 - 使用Object.getPrototypeOf函數(shù),而不要使用_proto_屬性
32 - 始終不要修改_proto_屬性

原因:

  1. _proto_屬性很特殊,他提供了Object.getPrototypeOf方法不具備的修改對象原型鏈接的能力.因為并不是所有平臺都支持改變對象原型的特性,所以有可移植性的問題
  2. 性能原因.因為現(xiàn)代的js引擎都深度優(yōu)化了獲取和設(shè)置對象屬性的行為,更改對象的內(nèi)部結(jié)構(gòu)(如添加或刪除該對象或其原型鏈中對象的屬性),將會使一些優(yōu)化失效.修改_proto_屬性實際上改變了繼承結(jié)構(gòu)本身,可能是最具破壞性的修改.會導(dǎo)致更多的優(yōu)化失效.
  3. 為了保持行為的可預(yù)測性.修改對象的原型鏈會交換對象的整個繼承層次結(jié)構(gòu).

tips:

  1. 使用Object.create函數(shù)給新對象設(shè)置自定義的原型.
33 - 使構(gòu)造函數(shù)與new操作符無關(guān)(ES6引入了class,這部分內(nèi)容應(yīng)該過時了)
//調(diào)用構(gòu)造函數(shù)時,忘記使用new關(guān)鍵字
//這樣函數(shù)的接收者將是全局對象
//會災(zāi)難性地創(chuàng)建全局變量,如果已存在則會修改其值
var u = User("name", "password");
function User(name, password){
    //"use strict";
    //如果使用嚴(yán)格模式, 則this.name = name;這一行會報錯
    this.name = name;
    this.password = password;
}
//=>可以修改為以下方式
function User(name, password){
    var self = this instanceof User ? this : Object.create(User.prototype);
    self.name = name;
    self.password = password;
    return self;
}
34 - 在原型中存儲方法

好處:

? 將方法存儲在原型中,使其可以被所有的實例使用,減少了占用的內(nèi)存.現(xiàn)代的js引擎深度優(yōu)化了原型查找,所以相對于把方法冗余地寫在實例對象中,并不一定能保證在查找速度上有明顯的提升.

35 - 使用閉包存儲私有數(shù)據(jù)

閉包時一種簡樸的數(shù)據(jù)結(jié)構(gòu).他們將數(shù)據(jù)存儲到封閉的變量中而不提供對這些變量的直接訪問.獲取閉包內(nèi)部結(jié)構(gòu)的唯一方式是該函數(shù)顯式地提供獲取它的途徑.

//該實現(xiàn)以變量的方式來引用name和passwordHash變量,而不是this屬性的方式
//此時User的實例中不會包含任何實例屬性,因此外部的代碼不能直接訪問User的實例的name和passwordHash變量
function User(name, passwordHash){
    this.toString = function(){return `[User ${name}]`;}
    this.checkPassword = function(password){
        return hash(password) === passwordHash;
    }
}
//缺點:
//為了使構(gòu)造函數(shù)中的變量在使用他們的方法的作用域內(nèi),那些方法必須置于實例對象中
//即不能用繼承的方式去減少占用內(nèi)存了.因為對象內(nèi)部根本沒有name和passwordHash這兩個屬性了.僅實例中定義的方法能訪問這兩個變量

tips:

  1. 閉包的變量是私有的,只能通過局部的引用獲取
  2. 將局部變量作為私有屬性從而通過方法實現(xiàn)信息隱藏.
36 - 只將實例狀態(tài)存儲在實例對象中

有狀態(tài)的數(shù)據(jù)可以存儲在原型中,只要你真的想要共享它.

一般情況下,原型對象中共享方法,而每個實例的狀態(tài)存儲在各自的實例對象中.

37 - 認識到this變量的隱式綁定問題

tips:

  1. this變量的作用域總是由其最近的封閉函數(shù)所確定.
  2. 使用一個局部變量(通常為self,me或that)綁定this,然后在內(nèi)部函數(shù)中使用.
38 - 在子類的構(gòu)造函數(shù)中調(diào)用父類的構(gòu)造函數(shù)(ES6引入了class,這部分內(nèi)容應(yīng)該過時了)

tips:

  1. 在子類的構(gòu)造函數(shù)中,顯示傳入this作為顯示的接收者調(diào)用父類的構(gòu)造函數(shù)
  2. 使用Object.create函數(shù)來構(gòu)造子類的原型對象以避免調(diào)用父類的構(gòu)造函數(shù)
//Object.create()方法創(chuàng)建一個新對象,使用現(xiàn)有的對象來提供新創(chuàng)建的對象的__proto__
Son.prototype = Object.create(Father.prototype);
39 - 不要重用父類的屬性名
40 - 避免繼承標(biāo)準(zhǔn)類

ECMAScript標(biāo)準(zhǔn)庫內(nèi)的類定義了很多特殊的行為,很難寫出行為正確的子類.

ECMAScript定義的完整的[[Class]]屬性值集合:

[[Class]] Construction
"Array" new Array(…), [...]
"Boolean" new Boolean(...)
"Date" new Date(...)
"Error" new Error(...), new EvalError(...), new RangeError(...), new ReferenceError(...), new SyntaxError(...), new TypeError(...), new URIError(...)
"Function" new Function(...), function (...){...}
"JSON" JSON
"Math" Math
"Number" new Number(...)
"Object" new Object(...), {...}, new MyClass(...)
"RegExp" new RegExp(...), /.../
"String" new String(...)

通過右側(cè)的構(gòu)造函數(shù)創(chuàng)建的才能有[[Class]]這個內(nèi)部屬性.

因為[[Class]]屬性及其他特殊的內(nèi)部屬性導(dǎo)致繼承這些類總會有問題,因此,最好避免繼承: Array, Boolean, Date, Function, Number, RegExp或 String.

Math對象所有屬性和方法都是靜態(tài)的,可對其作拓展.

JSON對象只有parse()和stringify()兩個方法,除了這兩個方法外,對象本身并無其他作用,也不能被調(diào)用或作為構(gòu)造函數(shù)調(diào)用.

Error, Object常被繼承.

tips:

  1. 使用屬性委托優(yōu)于繼承標(biāo)準(zhǔn)類.
41 - 將原型視為實現(xiàn)細節(jié)

因為對一個對象的操作,并不需要在意屬性或方法是在原型繼承結(jié)構(gòu)的那個位置,修改原型上的方法/屬性,將會影響依賴這個原型的對象.

tips:

  1. 對象是接口,原型是實現(xiàn)(即原型實現(xiàn)了實際的功能, 我們調(diào)用繼承這個原型的對象,這個對象就相當(dāng)于一個接口)
  2. 避免檢查你無法控制的對象的原型結(jié)構(gòu)
  3. 避免檢查實現(xiàn)在你無法控制的對象內(nèi)部的屬性
42 - 避免使用輕率的猴子補丁

猴子補丁(monkey-patching): 對象共享原型,所以每一個對象都可以增加、刪除或修改原型的屬性,這個操作被稱為猴子補丁.

使用場景:polyfill.即在某些js環(huán)境中,使用舊的方式實現(xiàn)新的API.

舉例:

//先判斷是否有這個方法
if(typeof Array.prototype.map !== "function"){
    //沒有這個方法的話,為之定義
    Array.prototype.map = function(f, thisArg){
        var result = [];
        for(var i = 0, n = this.length; i < n; i++){
            result[i] = f.call(thisArg, this[i], i);
        }
        return result;
    };
}

tips:

  1. 避免輕率地使用猴子補丁.

  2. 記錄程序庫所執(zhí)行的所有猴子補丁.

  3. 考慮通過將修改置于一個導(dǎo)出函數(shù)中,使猴子補丁稱為可選(即可以選擇執(zhí)行這個方法,也可選擇執(zhí)行其他的,而不是直接打在原型上)

    如:

    //置于函數(shù)中,用戶可以選擇調(diào)用該函數(shù)或忽略
    //如果忽略,Array原型上就不具有該方法,調(diào)用,則具有
    //這樣就不會有多處定義導(dǎo)致沖突的問題
    function addArrayMethods(){
       Array.prototype.split = function(i){
           return [this.slice(0,i), this.slice(i)];
       };
    };
    
  4. 使用猴子補丁為缺失的API提供polyfills.


數(shù)組和字典

43 - 使用Object的直接實例構(gòu)造輕量級的字典

輕量級字典的首要原則: 使用Object的直接實例作為字典,而不是其子類或者數(shù)組.這樣不容易遇到原型污染的問題(因為原型污染的范圍縮小到僅有Object.prototype).并且我們本身也建議不要直接添加屬性到Object.prototype中.

使用for...in循環(huán)去枚舉字典的屬性.

44 - 使用null原型以防止原型污染

防止原型污染最簡單的方式之一就是一開始就不使用原型.

ES5:

var o = Object.create(null);
Object.getPrototypeOf(o) === null;//true

存在的問題:

  1. 在不同環(huán)境中,還是會有一些問題.

    理想中,空對象是不存在_proto_屬性的. in和hasOwnProperty都應(yīng)該返回false

    而在一些環(huán)境中(只有in操作符為true)

    "xxx" in o //true

    {}.hasOwnProperty.call(o, "_proto_") //false

    而在一些環(huán)境中,會存在一個實例屬性_proto_

    "xxx" in o //true

    {}.hasOwnProperty.call(o, "_proto_") //true

ES5前不支持Object.create函數(shù)的環(huán)境:

var x = {__proto__: null};
x instanceof Object;//false

存在的問題:

  1. _proto_既不標(biāo)準(zhǔn),也不是完全可移植的,并且在未來可能被移除
  2. 如果使用_proto_作為屬性名,可能會導(dǎo)致一些問題,因為一些環(huán)境中把他作為特殊的屬性對待.

=>推薦45中的方式定義字典.

45 - 使用hasOwnProperty方法避免原型污染

一種避免原型污染的字典的有效實踐

//為了達到最大的可移植性和安全性,對__proto__屬性做了特殊處理
function Dict(elements){
    this.elements = elements || {};
    //是否設(shè)置了__proto__屬性的標(biāo)識符
    this.hasSpecialProto = false;
    //用于存儲設(shè)置的__proto__的值
    this.specialProto = undefined;
}
Dict.prototype.has = function(key){
    if(key === "__proto__"){
        return this.hasSpecialProto;
    }
    //使用{}.hasOwnProperty的方式,避免原型污染
    //普通的類,很可能因為原型定義了hasOwnProperty的方法導(dǎo)致原型污染
    return {}.hasOwnProperty.call(this.elements, key);
}
Dict.prototype.get = function(key){
    if(key === "__proto__"){
        return this.specialProto;
    }
    return this.has(key) ? this.elements[key] : undefined;
}
Dict.prototype.set = function(key, val){
    if(key === "__proto__"){
        this.hasSpecialProto = true;
        this.specialProto = val;
    }else{
        this.element[key] = val;
    }
}
Dict.prototype.remove = function(key){
    if(key === "__proto__"){
        this.hasSpecialProto = false;
        this.specialProto = undefined;
    }else{
        delete this.elements[key];
    }
}
46 - 使用數(shù)組而不要使用字典來存儲 有序 集合

原因: JavaScript對象是一個無序的屬性集合,獲得和設(shè)置不同的屬性與順序無關(guān),與js引擎有關(guān).

tips:

  1. 使用for...in循環(huán)來枚舉對象的屬性應(yīng)當(dāng)與順序無關(guān)
  2. 如果聚集運算字典中的數(shù)據(jù),確保聚集操作與順序無關(guān)(其實也就是不要用字典來存儲有序的數(shù)據(jù))
  3. 使用數(shù)組而不是字典來存儲有序集合
47 - 絕不要在Object.prototype中增加可枚舉的屬性

原因:在Object.prototype中添加可枚舉的屬性,會污染所有字典的枚舉遍歷.所以避免在Object.prototype中增加屬性

折中方式:

  1. 考慮編寫一個函數(shù)代替Object.prototype

    對象中的函數(shù)就叫做方法,通過"."的方式調(diào)用

  2. ES5后提供的方法,定義不可枚舉的屬性

    Object.defineProperty(Object.prototype, "allKeys", {
        value: function(){
            var result = [];
            for(var key in this){
                result.push(key);
            }
            return result;
        },
        writable: true,
        enumerable: false,//這一項就是定義allKeys是不可枚舉的,for...in時不會出現(xiàn)它
        configurable: true
    })
    
48 - 避免在枚舉期間修改對象

原因:for...in遍歷枚舉的期間,添加新的屬性,不能保證在枚舉期間能訪問到新添加的屬性.

tips:

  1. 使用for...in循環(huán)枚舉一個對象的屬性時,確保不要修改該對象.
  2. 迭代一個對象時,如果對象的內(nèi)容在循環(huán)期間可能會改變,應(yīng)該用while循環(huán)或經(jīng)典for循環(huán)來代替for...in 循環(huán).
  3. 為了在不斷變化的數(shù)據(jù)結(jié)構(gòu)中能夠預(yù)測枚舉,考慮使用一個有序的數(shù)據(jù)結(jié)構(gòu),如數(shù)組,而不是字典對象.
49 - 數(shù)組迭代要優(yōu)先使用for循環(huán)而不是for...in循環(huán)
var scores = [98,74,85,77,93,100,89];
var total = 0;
for(var score in scores){
    total += score;
}
var mean = total / scores.length;//此時的total其實是00123456
console.log(mean);//17636.571428571428而不是88
//正確的操作方式
//且在一開始就計算出數(shù)組的長度,避免寫在中間,那樣每次迭代都會計算一次數(shù)組長度
for(var i = 0, n = scores.length; i < n; i++){
    total += scores[i];
}
50 - 迭代方法優(yōu)于循環(huán)

原因:常見的終止條件的錯誤.

循環(huán)相對于迭代函數(shù)的唯一優(yōu)勢: 有控制流操作,如break和continue.

如下:

for(var i = 0; i <= n; i++){...}//尾部多了一次循環(huán)
for(var i = 1; i < n; i++){...}//頭部少了第一次循環(huán)
for(var i = n; i >= 0; i--){...}//多了頭部的一次循環(huán)
for(var i = n - 1; i > 0; i--){...}//少了最后一次循環(huán)

常用模式1:Array.prototype.forEach

代碼簡單可讀并且消除了終止條件和任何數(shù)組索引

for(var i = 0, n = players.length; i < n; i++){
    players[i].score++;
}
//等效于
players.forEach(function(p){
    p.score++;
})

常用模式2: Array.prototype.map

對數(shù)組每個元素進行一些操作后建立一個新的數(shù)組

var trimmed = [];
for(var i = 0, n = input.length; i < n; i ++){
    trimmed.push(input[i].trim());
}
//等效于
var trimmed = input.map(function(s){
    return s.trim();
})

常用模式3:Array.prototype.filter

計算一個新的數(shù)組,數(shù)組中的元素為現(xiàn)有數(shù)組中的一部分

var newArr = listings.filter(function(listing){
    return listing.price >= min && listing.price <= max;//return true的值會被留下,返回false的就不存在返回的新數(shù)組中了
})

可以提前終止循環(huán)的模式:

Array.prototype.some

返回一個布爾值表示其回調(diào)函數(shù)對數(shù)組的任何一個元素是否返回了真值

一旦產(chǎn)生真值就返回

[1,10,100].some(function(x){return x > 5;});//true
[1,10,100].some(function(x){return x < 0;});//false

Array.prototype.every

返回一個布爾值表示其回調(diào)函數(shù)是否對所有元素返回了一個真值

一旦產(chǎn)生假值就返回

[1,2,3,4,5].every(function(x){return x > 0;}); //true
[1,2,3,4,5].every(function(x){return x < 3;}); //false

tips:

  1. 使用迭代方法替換for循環(huán)增加代碼可讀性,避免思考重復(fù)循環(huán)的邏輯控制(即邊界條件)
  2. 使用自定義迭代函數(shù)來抽象常見循環(huán)模式(注意猴子補丁的注意事項,見42)
  3. 在存在需要提前終止的情況下,仍推薦傳統(tǒng)的循環(huán)方式,避免不必要的循環(huán).另外,some和every也可用于提前退出.
51 - 在類數(shù)組對象上復(fù)用通用的數(shù)組方法

類數(shù)組對象的規(guī)則:

  1. 具有一個范圍在0~2^32-1的整型length屬性
  2. length屬性大于該對象的最大索引.索引是一個范圍在0~2^32-2的整數(shù),它的字符串表示的是該對象中的一個key

滿足上述兩個條件的類數(shù)組對象使用Array.prototype任一方法都可兼容(除了concat方法必須是數(shù)組對象才行).

使用方式:

[].(forEach/some/...).call(類數(shù)組對象, ...);

常見類數(shù)組

  1. 函數(shù)的arguments對象
  2. DOM的NodeList類,如:document.getElementsByTagName,document.querySelectorAll獲得的對象
  3. 自定義的簡單對象字面量 如:var arrayLike = {0: "a",1:"b",2:"c",length:3};
  4. 字符串

轉(zhuǎn)換類數(shù)組為真正的數(shù)組對象的方法

  1. [].slice.call(類數(shù)組對象)
  2. Array.from(類數(shù)組對象/可迭代對象Set,Map)
52 - 數(shù)組字面量優(yōu)于數(shù)組構(gòu)造函數(shù)
var a = [1,2,3,4,5];
//優(yōu)于
var a = new Array(1,2,3,4,5);

好處:

  1. 使用構(gòu)造函數(shù)的方式需要先確認Array沒被包裝過
  2. 使用構(gòu)造函數(shù)的方式需要確認沒人修改過全局的Array變量

//以上兩點應(yīng)該不太可能發(fā)生...

  1. 使用單個數(shù)組時,有明顯不同

    var a = [17];和var a = new Array(17);

    后者會創(chuàng)建一個17長度的數(shù)組!


庫和API設(shè)計

53 - 保持一致的約定

tips:

  1. 在變量命名和函數(shù)簽名中使用一致的約定.如width,height等
  2. 不要偏移用戶在他們開發(fā)平臺中很可能遇到的約定.如width,height的順序等等
54 - 將undefined看做"沒有值"(缺少某個特定的值)

出現(xiàn)undefined的情況

  1. 未賦值的變量初始值為undefined
  2. 訪問對象中不存在的屬性,會產(chǎn)生undefined
  3. 函數(shù)體尾使用未帶參數(shù)的return ;或者未使用return都會產(chǎn)生返回值undefined
  4. 未給函數(shù)提供實參則該函數(shù)參數(shù)值為undefined

可選參數(shù)的實現(xiàn)的常見做法(不要檢查arguments.length,下面兩種方式更健壯)

  1. 測試undefined

    function Server(port, hostname){
        if(hostname === undefined){
            hostname = "localhost";
        }
        hostname = String(hostname);
        // ...
    }
    
  2. 測試是否為真

    function Server(Port, hostname){
        hostname = String(hostname || "localhost");
        // ...
    }
    

    邏輯運算符或(||),當(dāng)?shù)谝粋€參數(shù)為真時返回第一個參數(shù),否則返回第二個參數(shù)

    不適合使用"測試是否為真"的情況是:

    ? 如果除了undefined的其他假值(false, 0, -0, null, NaN, "")可以為函數(shù)的合法值時,就不能使用這種方式.需要選用上一種方式

55 - 接收關(guān)鍵字參數(shù)的選項對象

tips:

  1. 使用選項對象使得API更具可讀性、更容易記憶;且參數(shù)都是可選的,調(diào)用者可提供任一可選參數(shù)的子集.

    • 原因: 當(dāng)參數(shù)過多時,位置參數(shù)的方式可讀性很差
  2. 所有通過選項對象提供的參數(shù)應(yīng)當(dāng)被視為可選的

    function Alert(parent, message, opts){//必選的parent和message需要抽離出來
        opts = opts || {};//如果不傳入,初始化一個空對象避免后面的操作報錯
        this.width = opts.width == undefined ? 320: opts.width;//數(shù)字參數(shù) 0一般是合法值,所以要用undefined判斷
        this.height = opts.height == undefined ? 240: opts.height;
        this.title = opts.title || "Alert";//字符串一般空串不會算作合法值,所以這里用或操作符,如果此處空串也為合法值,那應(yīng)該改為測試undefined
        this.modal = !!opts.modal;//布爾值,使用雙重否定將參數(shù)強制轉(zhuǎn)換成一個布爾值
    }
    
  3. 如果使用其他庫,可以使用extend函數(shù)抽象出從選項對象中提取值的邏輯(前提是庫/框架提供了這個函數(shù))

    //一個典型的extend實現(xiàn)
    function extend(target, source){
        if(source){
            for(var key in source){
                var val = source[key];
                if(typeof val !== "undefined"){
                    target[key] = val;
                }
            }
        }
        return target;
    }
    
    //經(jīng)過extend函數(shù)簡化
    function Alert(parent, message, opts){
        opts = extend({
            width:320,
            height:240
        });
        opts = extend({
            title: "Alert",
            modal: false
        }, opts);
        
        this.width = opts.width;
        this.height = opts.height;
        this.title = opts.title;
        this.modal = opts.modal;
        
        //如果options是整個復(fù)制到this對象,可以進一步簡化上面四行
        extend(this, opts);
    }
    
56 - 避免不必要的狀態(tài)(狀態(tài)有時候是必須的)

區(qū)別:無狀態(tài)函數(shù)或方法的行為只取決于輸入,與程序的狀態(tài)改變無關(guān)

好處: 更容易學(xué)習(xí)和使用,更清晰(簡潔),更不易出錯; 相比于有狀態(tài)的API,無狀態(tài)的API會自動重用默認值,我們就不用擔(dān)心默認值是否在前面某處被更改了.

tips:

  1. 盡可能地使用無狀態(tài)的API
  2. 如果API是有狀態(tài)的,需要標(biāo)示出每個操作與哪些狀態(tài)有關(guān)聯(lián)
57 - 使用結(jié)構(gòu)類型structural typing(鴨子類型duck typing)設(shè)計靈活的接口

區(qū)別于一般面向?qū)ο笳Z言推薦的使用類和繼承來結(jié)構(gòu)化程序, javaScript動態(tài)類型使用的方式更靈活.只要有預(yù)期的結(jié)構(gòu)即可.

Wiki.formats.MEDIAWIKI = function(source){
    //extract contents from source
    //...
    return {
        getTitle: function(){/* ... */},
        getAuthor: function(){/* ... */},
        toHTML: function(){/* ... */}
    }
}

我們完全可以根據(jù)每種格式的需要,混合和匹配功能

繼承有時候?qū)е卤人鉀Q的問題外更多的問題.

靜態(tài)語言迫使必須符合某個數(shù)據(jù)類型,增加了更多的代碼;而動態(tài)語言可以編寫更少的代碼,使代碼更簡潔,可以花更多的精力在業(yè)務(wù)邏輯上.缺點是無法保證變量類型,可能在運行時出錯.

鴨子類型是典型的面向接口編程,且不必借助超類的幫助.它只要正確的實現(xiàn)了我們需要的方法供我們調(diào)用即可.

tips:

  1. 使用結(jié)構(gòu)類型來設(shè)計靈活的對象接口
  2. 結(jié)構(gòu)接口更靈活,更輕量,所以應(yīng)該避免使用繼承
  3. 針對單元測試,使用mock對象即接口的替代實現(xiàn)來提供可復(fù)驗的行為.
58 - 區(qū)分?jǐn)?shù)組對象和類數(shù)組對象

tips:

  1. 絕不重載與其他類型有重疊的結(jié)構(gòu)類型
  2. 當(dāng)重載一個結(jié)構(gòu)類型與其他類型時,先測試其他類型.因為結(jié)構(gòu)類型沒有明確的信息標(biāo)記他們實現(xiàn)的結(jié)構(gòu)類型,沒有可靠的編程方法來檢測該信息.
  3. 當(dāng)重載其他對象類型時,接受真數(shù)組而不是類數(shù)組對象
  4. 文檔標(biāo)注你的API是否接受真數(shù)組或類數(shù)組值
  5. 使用ES5提供的Array.isArray方法測試真數(shù)組(比instance of操作符更可靠)
59 - 避免過度的強制轉(zhuǎn)換

tips:

  1. 避免強制轉(zhuǎn)換和重載混用
  2. 考慮防御性地監(jiān)視非預(yù)期的輸入
    • 防御性編程:試圖以額外的檢查來抵御潛在的錯誤
60 - 支持方法鏈(就是函數(shù)式編程)

tips:

  1. 使用方法鏈來連接無狀態(tài)的操作.
  2. 通過在無狀態(tài)的方法中返回新對象來支持方法鏈
  3. 通過在有狀態(tài)的方法中返回this來支持方法鏈

并發(fā)


61 - 不要阻塞I/O事件隊列

系統(tǒng)維護了一個按事件發(fā)生順序排列的內(nèi)部事件隊列,一次調(diào)用一個已注冊的回調(diào)函數(shù),在執(zhí)行過程中,系統(tǒng)會適時地查看處理進度,會在異步執(zhí)行結(jié)束后立刻調(diào)用回調(diào)函數(shù).

好處:系統(tǒng)的這種運行方式有時被稱為運行到完成機制擔(dān)保(run-to-completion),當(dāng)代碼運行時,你完全可以掌握應(yīng)用程序的狀態(tài),不用擔(dān)心一些變量和對象的屬性由于并發(fā)執(zhí)行代碼而超出你的控制.

不足:實際上這些異步執(zhí)行的代碼支撐著后續(xù)應(yīng)用程序的執(zhí)行(返回某些重要的參數(shù),又會作為后面代碼的實參等等).在客戶端,一個阻塞的事件處理程序會阻塞任何將被處理的其他用戶輸入,甚至阻塞頁面渲染;在服務(wù)器端,一個阻塞的事件處理程序可能會阻塞其他將被處理的網(wǎng)絡(luò)請求,導(dǎo)致服務(wù)器失去響應(yīng).

tips:

  1. 異步API使用回調(diào)函數(shù)來延緩處理代價高昂的操作以避免阻塞主應(yīng)用程序.
  2. JavaScript并發(fā)地接收事件,但會使用一個事件隊列按序地處理事件.
  3. 在應(yīng)用程序事件隊列中絕不要使用阻塞的I/O.
62 - 在異步序列中使用嵌套或命名的回調(diào)函數(shù)

為了保證存在依賴的異步代碼的執(zhí)行順序,使用嵌套或命名的回調(diào)函數(shù).

tips:

  1. 使用嵌套或命名的回調(diào)函數(shù)按順序地執(zhí)行多個異步操作.

  2. 嘗試在過多的嵌套的回調(diào)函數(shù)和尷尬的命名的非嵌套回調(diào)函數(shù)中取得平衡(后面會介紹解決該問題的ES6新特性Promise)

  3. 避免將可被并行執(zhí)行的操作順序化.

63 - 當(dāng)心丟棄錯誤

常見的異步處理錯誤:

//1.定義一個附加的錯誤處理回調(diào)函數(shù)(errbacks)
downloadAsync("http://example.com/file.txt",function(text){
    console.log(text);
}, function(error){
    console.log("Error:" + error);
})
//2.異步操作出錯則回調(diào)函數(shù)返回的第一個參數(shù)為真,沒出錯則為一個假值
//常用在Node.js平臺
function onError(error){
    console.log("Error:" + error);
}
downloadAsync("a.txt", function(error, a){
    if(error) return onError(error);
    downloadAsync("b.txt", function(error, b){
        if(error) return onError(error);
        console.log("contents:" + a + b);
    })
})

tips:

  1. 通過編寫共享的錯誤處理函數(shù)來避免賦值和粘貼錯誤處理代碼
  2. 確保明確地處理所有的錯誤條件以避免丟失錯誤.
64 - 對異步循環(huán)使用遞歸

tips:

  1. 循環(huán)不能是異步的.
  2. 使用遞歸函數(shù)在事件循環(huán)的單獨輪次中執(zhí)行迭代.
  3. 在事件循環(huán)的單獨輪次中執(zhí)行遞歸,并不會導(dǎo)致調(diào)用棧溢出.
65 - 不要在計算時阻塞事件隊列

tips:

  1. 避免在主事件隊列中執(zhí)行代價高昂的算法. (如:使用setTimeout的方式變?yōu)楫惒綀?zhí)行)
  2. 在支持Worker API 的平臺, 該API可以用在一個獨立的事件隊列中運行長計算程序.
  3. 在Worker API不可用或代價昂貴的環(huán)境中, 考慮將計算程序分解到事件循環(huán)的多個輪次中.
66 - 使用計數(shù)器來執(zhí)行并行操作

tips:

  1. 并行操作的結(jié)果順序是不可預(yù)測的,這個時候我們可以使用計算器去確定代碼的執(zhí)行進度.
67 - 絕不要同步地調(diào)用異步的回調(diào)函數(shù)

舉例:

//調(diào)用方代碼
downloadCachingAsync("file.txt", function(file){
    console.log("finished");//might happen first
})
console.log("starting");

//被調(diào)用方,定義的函數(shù)
function downloadCachingAsync(url, onsuccess, onerror){
    if(cache.has(url)){//模擬有緩存的情況,這種情況代碼走到了同步回調(diào)中
        onsuccess(cache.get(url));//此處同步調(diào)用了回調(diào)!!!
        return;
        //正確的做法
        //return setTimeout(onsuccess(cache.get(url),0));
    }
    return downloadAsync(url, function(file){
        cache.set(url, file);
        onsuccess(file);
    }, onerror);
}

存在的問題:

  1. 如上述代碼, 同步的調(diào)用異步的回調(diào)函數(shù),導(dǎo)致日志消息竟然出現(xiàn)了錯誤的順序("finish"先于"starting")
  2. 如64條所述, 異步回調(diào)本質(zhì)上以空的調(diào)用棧來調(diào)用, 因此異步的循環(huán)實現(xiàn)為遞歸函數(shù)時安全的.但是如果同步地調(diào)用不能保障這一點,會使得表面上是異步的循環(huán)耗盡棧空間.

tips:

  1. 即使可以立即得到數(shù)據(jù),也絕不要同步地調(diào)用異步回調(diào)函數(shù)
  2. 同步地調(diào)用異步回調(diào)函數(shù)擾亂了操作的序列,可能導(dǎo)致意想不到的交錯代碼
  3. 同步地調(diào)用異步的回調(diào)可能導(dǎo)致棧溢出或錯誤地處理異常
  4. 使用異步的API.如setTimeout函數(shù)異步回調(diào)函數(shù)
68 - 使用promise模式清潔異步邏輯

基于promise的API不接收回調(diào)函數(shù)作為參數(shù),而是返回一個promise對象,通過自身的then方法接收回調(diào)函數(shù).

tips:

  1. promise代表最終值,即并行操作完成時最終產(chǎn)生的結(jié)果.
  2. 使用promise組合不同的并行操作
  3. 使用promise模式的API避免數(shù)據(jù)競爭
  4. 在要求有意的競爭條件時使用select(也被稱為choose)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容