參考書籍:《Effective JavaScript》git
函數、方法和構造函數是單個構造對象的三種不一樣的使用模式。數組
函數調用數據結構
function hello(username) { return 'hello, ' + username; } hello('Keyser Soze'); // hello, Keyser Soze
方法調用(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
構造函數調用函數
function User(name, passwordHash) { this.name = name; this.passwordHash = passwordHash; } var u = new User('sfalken', '0ef33ae791068ec64b502d6cb0191387'); u.name; // sfalken
使用new操做符來調用函數則視其爲構造函數。oop
構造函數調用將一個全新的對象做爲this變量的值,並隱式返回這個新對象做爲調用結果。構造函數的主要職責是初始化該新對象。性能
提示:優化
高階函數指的是將函數做爲參數或返回值的函數。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'
提示:
一般,函數或方法的接收者(即綁定到特殊關鍵字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);
提示:
函數對象配有一個相似的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())
。
提示:
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); }
提示:
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
提示:
迭代器(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
提示:
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
函數。
提示:
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函數的另外一個問題是,一些高性能的引擎很難優化字符串中的代碼,由於編譯器不能儘量早地得到源代碼來及時優化代碼。然而函數表達式在其代碼出現的同時就能被編譯,這使得它更適合標準化編譯。
提示:
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}
提示:
每一個arguments對象都包含兩個額外的屬性:arguments.callee
和arguments.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.caller
和arguments.callee
屬性,由於它們不具有良好的移植性。