編寫高質量JavaScript代碼之使用函數

參考書籍:《Effective JavaScript》git

使用函數

理解函數調用、方法調用及構造函數之間的不一樣

函數、方法和構造函數是單個構造對象的三種不一樣的使用模式。數組

  1. 函數調用數據結構

    function hello(username) {
        return 'hello, ' + username;
    }
    
    hello('Keyser Soze'); // hello, Keyser Soze
  2. 方法調用(JavaScript中的方法指的是對象的屬性剛好是函數)閉包

    var obj = {
        hello: function () {
            return 'hello, ' + this.username;
        },
        username: 'Hans Gruber'
    };
    
    obj.hello(); // hello, Hans Gruber

    在方法調用中由調用表達式自身來肯定this變量的綁定。綁定到this變量的對象被稱爲調用接收者(receiver)。表達式obj.hello()在obj對象中查找名爲hello的屬性,並將obj對象做爲接收者,而後調用該屬性。app

  3. 構造函數調用函數

    function User(name, passwordHash) {
        this.name = name;
        this.passwordHash = passwordHash;
    }
    
    var u = new User('sfalken', '0ef33ae791068ec64b502d6cb0191387');
    u.name; // sfalken

    使用new操做符來調用函數則視其爲構造函數。oop

    構造函數調用將一個全新的對象做爲this變量的值,並隱式返回這個新對象做爲調用結果。構造函數的主要職責是初始化該新對象。性能

提示:優化

  • 方法調用將被查找方法屬性的對象做爲調用接收者。
  • 函數調用將全局對象(處於嚴格模式下則爲undefined)做爲接收者。通常不多使用函數調用語法來調用方法。
  • 構造函數須要經過new運算符調用,併產生一個新的對象做爲接收者。

熟練掌握高階函數

高階函數指的是將函數做爲參數或返回值的函數。ui

[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]
var names = ['Fred', 'Wilma', 'Pebbles'],
    upper = names.map(function (name){
        return name.toUpperCase();
    });

upper; // ['FRED', 'WILMA', 'PEBBLES']

建立高階函數抽象有不少好處。實現中存在的一些棘手部分,好比正確地獲取循環邊界條件,它們能夠被放置在高階函數的實現中。這使得你能夠一次性地修復全部邏輯上的錯誤,而沒必要去搜尋散佈在程序中的該編碼模式的全部實例。若是你發現須要優化操做的效率,你也能夠僅僅修改一處。

當發現本身在重複地寫一些相同的模式時,學會藉助於一個高階函數可使代碼更簡潔、更高效和更可讀。

var aIndex = 'a'.charCodeAt(0),
    alphabet = '';

for (var i = 0; i < 26; i++) {
    alphabet += String.fromCharCode(aIndex + i);
}

alphabet; // 'abcdefghijklmnopqrstuvwxyz'

var digits = '';

for (var i = 0; i < 10; i++) {
    digits += i;
}

digits; // '0123456789'
function buildString(n, callback) {
    var result = '';

    for (var i = 0; i < n; i++) {
        result += callback(i);
    }

    return result;
}

var alphabet = buildString(26, function (i){
    return String.fromCharCode(aIndex + i);
});

alphabet; // 'abcdefghijklmnopqrstuvwxyz'

var digits = buildString(10, function (i) {
    return i;
});

digits; // '0123456789'

提示:

  • 高階函數時那些將函數做爲參數或返回值的函數。
  • 熟悉掌握現有庫中的高階函數。
  • 學會發現能夠被高階函數所取代的常見的編碼模式。

使用call方法自定義接收者的調用方法

一般,函數或方法的接收者(即綁定到特殊關鍵字this的值)是由調用者的語法決定的。然而,有時須要使用自定義接收者來調用函數,由於該函數可能並非指望的接收者對象的屬性。

幸運的是,函數對象具備一個內置的方法call來自定義接收者。

f.call(obj, arg1, arg2, arg3);

當調用的方法被刪除、修改或者覆蓋時,call方法就派上用場了。

var hasOwnProperty = {}.hasOwnProperty;
dict.foo = 1;
delete dict.hasOwnProperty;
hasOwnProperty.call(dict, 'foo'); // true
hasOwnProperty.call(dict, 'hasOwnProperty'); // false

定義高階函數時call方法也特別實用。

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];
            f.call(thisArg, entry.key, entry.value, i);
        }
    }
};

上述例子容許table對象的使用者將一個方法做爲table.forEach的回調函數f,併爲該方法提供一個合理的接收者。例如,能夠方便地將一個table的內容複製到另外一箇中。

table1.forEach(table2.addEntry, table2);

提示:

  • 使用call方法自定義接收者來調用函數。
  • 使用call方法能夠調用在給定的對象中不存在的方法。
  • 使用call方法定義高階函數容許使用者給回調函數指定接收者。

使用apply方法經過不一樣數量的參數調用函數

函數對象配有一個相似的apply方法。

var scores = getAllScores();
average.apply(null, scores);

若是scores有三個元素,那麼以上代碼的行爲與average(scores[0], scores[1], scores[2])一致。

apply方法也可用於可變參數方法。

var buffer = {
    state: [],
    append: function () {
        for (var i = 0, n = arguments.length; i < n; i++) {
            this.state.push(arguments[i]);
        }
    }
};

藉助於apply方法的this參數,咱們能夠指定一個可計算的數組調用append方法:buffer.append.apply(buffer, getInputString())

提示:

  • 使用apply方法指定一個可計算的參數數組來調用可變參數的函數。
  • 使用apply方法的第一個參數給可變參數的方法提供一個接收者。

使用arguments建立可變參數的函數

function averageOfArray(a) {
    for (var i = 0, sum = 0, n = a.length; i < n; i++) {
        sum += a[i];
    }

    return sum / n;
}

averageOfArray([2, 7, 1, 8, 2, 8, 1, 8]); // 4.625

JavaScript給每一個函數都隱式地提供了一個名爲arguments的局部變量。arguments對象給實參提供了一個相似數組的接口。它爲每一個實參提供了一個索引屬性,還包含一個length屬性用來指示參數的個數。

function average() {
    for (var i = 0, sum = 0, n = arguments.length; i < n; i++) {
        sum += arguments[i];
    }

    return sum / n;
}

average([2, 7, 1, 8, 2, 8, 1, 8]); // 4.625

可變參數函數提供了靈活的接口。可是,若是使用者想使用計算的數組參數調用可變參數的函數,只能使用apply方法。好的經驗法是,若是提供了一個便利的可變參數的函數,也最好提供一個須要顯式指定數組的固定元數的版本。咱們能夠編寫一個輕量級的封裝,並委託給固定元數的版原本實現可變參數的函數

function average() {
    return averageOfArray(arguments);
}

提示:

  • 使用隱式地arguments對象實現可變參數的函數。
  • 考慮對可變參數的函數提供一個額外的固定元數的版本,從而使得使用者無需藉助apply方法。

永遠不要修改arguments對象

function callMethod(obj, method) {
    var shift = [].shift;
    
    // 移除arguments的前兩個元素
    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對象並非函數參數的副本。特別是,全部的命名參數都是arguments對象中對應索引的別名。所以,即便經過shift方法移除arguments對象中的元素以後,obj仍然是arguments[0]的別名,method仍然是arguments[1]的別名。

在ES5嚴格模式下,函數參數不支持對其arguments對象取別名。

function strict(x) {
    "use strict";
    arguments[0] = 'modified';

    return x === arguments[0];
}

function nonstrict(x) {
    arguments[0] = 'modified';

    return x === arguments[0];
}

strict('unmodified'); // false
nonstrict('unmodified'); // true

所以,永遠不要修改arguments對象。經過一開始複製參數中的元素到一個真正的數組的方式,能夠避免修改arguments對象。

function callMethod(obj, method) {
    /* 當不適用額外的參數調用數組的slice方法時,它會複製整個數組,其結果是一個真正的標準Array類型實例 */
    var args = [].slice.call(arguments, 2);

    return obj[method].apply(obj, args);
}

var obj = {
    add: function (x, y) {
        return x + y;
    }
};

callMethod(obj, 'add', 17, 25); // 42

提示:

  • 永遠不要修改arguments對象。
  • 使用[].slice.call(arguments)將arguments對象複製到一個真正的數組中再進行修改。

使用變量保存arguments的引用

迭代器(iterator)是一個能夠順序存取數據集合的對象。其一個典型的API是next方法,該方法得到序列中的下一個值。假設咱們編寫一個函數,它能夠接收任意數量的參數,併爲這些值創建一個迭代器。

function values() {
    var i = 0, n = arguments.length;

    return {
        hasNext: function () {
            return i < n;
        },
        next: function () {
            if (i >= n) {
                throw new Error('end of iteration');
            }

            return arguments[i++]; // wrong arguments
        }
    }
}

var it = values(1, 4, 1, 4, 2, 1, 3, 5, 6);
it.next(); // undefined
it.next(); // undefined
it.next(); // undefined

一個新的arguments變量被隱式地綁定到每一個函數體內。咱們感興趣的arguments對象是與values函數相關的那個,可是迭代器的next方法含有本身的arguments。因此當返回arguments[i++]時,咱們訪問的是it.next的參數,而不是values函數中的參數。

解決方案只需在咱們感興趣的arguments對象做用域綁定一個新的局部變量,並確保嵌套函數只能引用這個顯式命名的變量。

function values() {
    var i = 0, n = arguments.length, a = arguments;

    return {
        hasNext: function () {
            return i < n;
        },
        next: function () {
            if (i >= n) {
                throw new Error('end of iteration');
            }

            return a[i++]; 
        }
    }
}

var it = values(1, 4, 1, 4, 2, 1, 3, 5, 6);
it.next(); // 1
it.next(); // 4
it.next(); // 1

提示:

  • 當引用arguments時小心函數嵌套層級。
  • 綁定一個明確做用域的引用到arguments變量,從而能夠在嵌套的函數中引用它。

使用bind方法提取具備肯定接收者的方法

var buffer = {
    entries: [],
    add: function (s) {
        this.entries.push(s);
    },
    concat: function () {
        return this.entries.join('');
    }
};

var source = ['867', '-', '5309'];
source.forEach(buffer.add); // error: entries is undefiend

上述例子中,對象的方法buffer.add被提取出來做爲回調函數傳遞給高階函數Array.prototype.forEach。可是buffer.add的接收者並非buffer對象。事實上,forEach方法的實現使用全局對象做爲默認的接收者。

所幸,forEach方法運行調用者提供一個可選的參數做爲回調函數的接收者。

var source = ['867', '-', '5309'];
source.forEach(buffer.add, buffer);
buffer.join(); // 867-5309

函數對象的bind方法須要一個接收者對象,併產生一個以該接收者對象的方法調用的方式調用原來的函數的封裝函數。

var source = ['867', '-', '5309'];
source.forEach(buffer.add.bind(buffer));
buffer.join(); // 867-5309

記住,buffer.add.bind(buffer)建立了一個新函數而不是修改了buffer.add函數。

提示:

  • 要注意,提取一個方法不會將方法的接收者綁定到該方法的對象上。
  • 當給高階函數傳遞對象方法時,使用匿名函數在適當的接收者上調用該方法。
  • 使用bind方法建立綁定到適當接收者的函數。

使用bind方法實現函數柯里化

TODO...

使用閉包而不是字符串來封裝代碼

function f() {}

function repeat(n, action) {
    for (var i = 0; i < n; i++) {
        eval(action);
    }
}

function benchmark() {
    var start = [], end = [], timings = [];

    repeat(1000, 'start.push(Date.now()); f(); end.push(Date.now())');

    for (var i = 0, n = start.length; i < n; i++) {
        timings[i] = end[i] - start[i];
    }

    return timings;
}

benchamrk(); // Uncaught ReferenceError: start is not defined

上述代碼會致使repeat函數引用全局的start和end變量。

更健壯的API應該接受函數而不是字符串。

function repeat(n, action) {
    for (var i = 0; i < n; i++) {
        action();
    }
}

function benchmark() {
    var start = [], end = [], timings = [];

    repeat(1000, function (){
        start.push(Date.now()); 
        f(); 
        end.push(Date.now())
    });

    for (var i = 0, n = start.length; i < n; i++) {
        timings[i] = end[i] - start[i];
    }

    return timings;
}

eval函數的另外一個問題是,一些高性能的引擎很難優化字符串中的代碼,由於編譯器不能儘量早地得到源代碼來及時優化代碼。然而函數表達式在其代碼出現的同時就能被編譯,這使得它更適合標準化編譯。

提示:

  • 當將字符串傳遞給eval函數以執行它們的API時,毫不要在字符串中包含局部變量引用。
  • 接受函數調用的API優於使用eval函數執行字符串的API。

不要信賴函數對象的toSting方法

JavaScript函數有一個非凡的特性,即將其源代碼重現爲字符串的能力。

(function(x) {
    return x + 1;
}).toString(); // function (x) {\n return x + 1; \n}

可是使用函數對象的toString方法有嚴重的侷限性。

(function(x) {
    return x + 1;
}).bind(16).toString(); // function () { [native code] }
(function(x) {
    return function(y) {
        return x + y;
    }
})(42).toString(); // function (y) {\n return x + y; \n}

提示:

  • 當調用函數的toString方法時,並無要求JavaScript引擎可以精確地獲取到函數的源代碼。
  • 因爲在不一樣的引擎下調用toString方法的結果可能不一樣,因此毫不要信賴函數源代碼的詳細細節。
  • toString方法的執行結果並不會暴露存儲在閉包中的局部變量值。
  • 一般狀況下,應該避免使用函數對象的toString方法。

避免使用非標準的棧檢查屬性

每一個arguments對象都包含兩個額外的屬性:arguments.calleearguments.caller。前者指向使用該arguments對象被調用的函數,後者指向調用該arguments對象的函數。

arguments.callee除了容許匿名函數遞歸地引用其自身以外,無更多用途了。

var factorial = function (n) {
    return (n <= 1) ? 1 : (n * arguments.callee(n - 1));
};

可是這並非頗有用,由於更直接的方式是使用函數名來引用函數自身。

var factorial = function (n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));
};

arguments.caller在大多數環境中已經被移除了,但許多JavaScript環境也提供了一個類似的函數對象屬性——非標準但廣泛適用的caller屬性,它指向函數最近的調用者。

function revealCaller() {
    return revealCaller.caller;
}

function start() {
    return revealCaller();
}

start() === start; // true

使用函數的caller屬性來獲取棧跟蹤(stack trace)是頗有誘惑力的。棧跟蹤是一個提供當前調用棧快照的數據結構。

function getCallStack() {
    var stack = [];
    
    for (var f = getCallStack.caller; f; f = f.caller) {
        stack.push(f);
    }

    return stack;
}

function f1() {
    return getCallStack();
}

function f2() {
    return f1();
}

var trace = f2();
trace; // [f1, f2]

可是若是某個函數在調用棧中出現了不止一次,那麼棧檢查邏輯將會陷入循環。

function f(n) {
    return n === 0 ? getCallStack() : f(n - 1);
}

var trace = f(1); // infinite loop

在ES5的嚴格模式下,棧檢查屬性是禁止使用的。

function f() {
    "use strict";

    return f.caller;
}

f(); // Uncaught TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them

提示:

  • 避免使用非標準的arguments.callerarguments.callee屬性,由於它們不具有良好的移植性。
  • 避免使用非標準的函數對象caller屬性,由於在包含所有棧信息方面,它是不可靠的。
相關文章
相關標籤/搜索