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

參考書本:《Effective JavaScript》

運用函數

明白函數挪用、要領挪用及組織函數之間的差別

函數、要領和組織函數是單個組織對象的三種差別的運用形式。

  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對象作為吸收者,然後挪用該屬性。

  3. 組織函數挪用

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

    運用new操縱符來挪用函數則視其為組織函數。

    組織函數挪用將一個全新的對象作為this變量的值,並隱式返回這個新對象作為挪用效果。組織函數的主要職責是初始化該新對象。

提醒:

  • 要領挪用將被查找要領屬性的對象作為挪用吸收者。
  • 函數挪用將全局對象(處於嚴厲形式下則為undefined)作為吸收者。平常很少運用函數挪用語法來挪用要領。
  • 組織函數須要經由過程new運算符挪用,併發生一個新的對象作為吸收者。

熟練控制高階函數

高階函數指的是將函數作為參數或返回值的函數。

[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屬性,因為在包括悉數棧信息方面,它是不可靠的。
    原文作者:3santiago3
    原文地址: https://segmentfault.com/a/1190000014531807
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞