参考书籍:《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
属性,由于它们不具有良好的移植性。