2

参考书籍:《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
113 声望2 粉丝