编写高质量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属性,由于在包含所有栈信息方面,它是不可靠的。
相关文章
相关标签/搜索