深入理解 JavaScript 函数的特性与最佳实践

来源:互联网 发布:淘宝客服日常用语 编辑:程序博客网 时间:2024/06/05 21:12

函数用于指定对象的行为。所谓的编程,就是将一组需求分解为一组函数和数据结构的技能。

1 函数对象

JavaScript 函数就是对象。对象是名值对的集合,它还拥有一个连接到原型对象的链接。对象字面量产生的对象连接到 Object.prototype,而函数对象连接到 Function.prototype(这个对象本身连接到 Object.prototype)。每个函数在创建时会附加两个隐藏属性:函数的上下文以及实现函数的代码。

函数对象在创建后会有一个 prototype 属性,它的值是一个拥有 constructor 属性、且值既是该函数的对象。

因为函数是对象,所以可以被当做参数传递给其他函数。它也可以再返回函数。

2 函数字面量

函数可以通过字面量进行创建:

var add = function (a, b) {    return a + b;}

这里没有给函数命名,所以称它为匿名函数。

一个内部函数除了可以访问自己的参数和变量之外,还可以访问它的父函数的参数和变量。通过函数字面量创建的函数对象包含一个连接到外部上下文的连接,这被称为闭包。它是 JavaScript 强大表现力的来源。

3 调用

调用一个函数会暂停当前函数的执行,它会传递控制权和参数给这个被调用的函数。

当函数的实际参数的个数与形式参数的个数不匹配时,不会导致运行时错误。如果实际参数的个数过多,那么超出的参数会被忽略;如果实际参数的个数过少,那么缺失的值会是 undefined。不会对参数类型进行检查,所以任何类型的值都可以被传递给任何参数。

3.1 方法调用模式

当一个函数被保存为对象的一个属性时,就称它为方法。当方法被调用时,this 被绑定到这个对象。如果调用表达式包含一个提取属性的动作(即包含一个”.” 点表达式或 “[]” 下标表达式),那么它就是被当做一个方法被调用。

var myObject = {    value: 0,//属性    increment: function (inc) {//方法        this.value += typeof inc === 'number' ? inc : 1;    }};myObject.increment();console.log(myObject.value);//1myObject.increment(2);console.log(myObject.value);//3

这里可以使用 this 来访问自己所属的对象。通过 this 可以取得它们所属对象的上下文的方法被称为公共方法。

3.2 函数调用模式

var add = function (a, b) {    return a + b;}var sum = add(3, 4);//7;this 被绑定到全局对象

这里的 this 被绑定到全局对象,这其实是语言设计上的失误!如果设计正确,那么当内部函数被调用时,this 应该被绑定到外部函数的 this 变量才是。可以这样解决:为这个方法定义一个变量并给它赋值为 this,这样内部函数就可以通过这个变量访问到 this 啦,一般把这个变量命名为 that:

myObject.double = function () {    var that = this;//让内部函数可以通过这个变量访问到 this (myObject)    var helper = function () {        that.value = add(that.value, that.value);    };    helper();//以函数形式调用 helper};myObject.double();//以方法形式调用 helperconsole.log(myObject.value);//6

3.3 构造器调用模式

JavaScript 是基于原型继承的语言,所以对象可以从其他对象继承它们的属性。

如果在函数之前加上 new ,那么 JavaScript 就会创建一个连接到该函数的 prototype 属性的新对象,而 this 会绑定到这个新对象。

/** * 构造器调用模式(不推荐) */var Quo = function (string) {//定义构造器函数;按照约定,变量名首字母必须大写    this.status = string;//属性};/** * 为 Quo 的所有实例提供一个名为 get_status 的公共方法 * @returns {*} */Quo.prototype.get_status = function () {    return this.status;};var myQuo = new Quo("confused");//定义一个 Quo 实例console.log(myQuo.get_status());//"confused"

按照约定,构造器函数被保存在以大写字母命名的变量中。因为如果调用构造器函数时没有加上 new,问题很大,所以才以大写字母的命名方式让大家记住调用时要加上 new。

3.4 Apply 调用模式

因为 JavaScript 是函数式的面向对象语言,所以函数可以拥有方法。

apply 方法可以构建一个参数数组,然后再传递给被调用的函数。这个方法接收两个参数:要绑定给 this 的值以及参数数组。

//相加var array = [3, 4];var sum = add.apply(null, array);//7console.log(sum);//调用 Quo 的 get_status 方法,给 this 绑定 statusObject 上下文var statusObject = {    status: 'A-OK'};var status = Quo.prototype.get_status.apply(statusObject);console.log(status);//'A-OK'

4 参数

当函数被调用时,会有一个 arguments 数组。它是函数被调用时,传递给这个函数的参数列表,包含那些传入的、多出来的参数。可以利用这一点,编写一个无须指定参数个数的函数:

//构造一个能够接收大量参数,并相加的函数var sum = function () {    var i, sum = 0;    for (i = 0; i < arguments.length; i += 1) {        sum += arguments[i];    }    return sum;};console.log(sum(4, 5, 6, 7, 8, 9));//39

arguments 不是一个真正的数组,它只是一个类数组的对象,它拥有 length 属性,但没有数组的相关方法。

5 返回

return 语句可以让函数提前返回。return 被执行时,函数会立即返回。

一个函数总会返回一个值,如果没有指定这个值,它就会返回 undefined。

如果使用 new 前缀来调用一个函数,那么它的返回值是:创建的一个连接到该函数的 prototype 属性的新对象。

6 异常

异常是干扰程序正常流程的事故。发生事故时,我们要抛出一个异常:

var add = function (a, b) {    if (typeof a !== 'number' || typeof b !== 'number') {        throw{            name: 'TypeError',            message: 'add needs numbers'        };    }    return a + b;}

throw 语句会中断函数的执行,它要抛出一个 exception 对象,这个对象包含一个用来识别异常类型的 name 属性和一个描述性的 message 属性。也可以根据需要,扩展这个对象。

这个 exception 对象会被传递到 try 语句的 catch 从句:

var try_it = function () {    try {        add("seven");    } catch (e) {        console.log(e.name + ": " + e.message);    }};try_it();

一个 try 语句只会有一个捕获所有异常的 catch 从句。所以如果处理方式取决于异常的类型,那么我们就必须检查异常对象的 name 属性,来确定异常的类型。

7 扩充类型的功能

可以给 Function.prototype 增加方法来使得这个方法对所有的函数都可用:

/** * 为 Function.prototype 新增 method 方法 * @param name 方法名称 * @param func 函数 * @returns {Function} */Function.prototype.method = function (name, func) {    if (!this.prototype[name])//没有该方法时,才添加        this.prototype[name] = func;    return this;};

通过这个方法,我们给对象新增方法时,就可以省去 prototype 字符啦O(∩_∩)O~

有时候需要提取数字中的整数部分,我们可以为 Number.prototype 新增一个 integer 方法:

Number.method('integer', function () {    return Math[this < 0 ? 'ceil' : 'floor'](this);});

它会根据数字的正负来决定是使用 Math.ceiling 还是 Math.floor。

然后再为 String 添加一个移除字符串首尾空白的方法:

String.method('trim', function () {    return this.replace(/^\s+|\s+$/g, '');});

这里使用了正则表达式。

通过为基本类型增加方法,可以极大地提高 JavaScript 的表现力。因为原型继承的动态本质,新的方法立刻被赋予所有的对象实例上(甚至包括那些在方法被增加之前的那些对象实例)

基本类型的原型是公用的,所以在使用其他类库时要小心。一个保险的做法是:只在确定没有该方法时才添加它。

Function.prototype.method = function (name, func) {    if (!this.prototype[name])//没有该方法时,才添加        this.prototype[name] = func;    return this;};

8 递归

递归函数是会直接或间接地调用自身的函数。它会把一个问题分解为一组相似的子问题,而每一个子问题都会用一个寻常的解来解决。

汉诺塔的游戏规则是:塔上有 3 根柱子和一套直径不相同的空心圆盘。开始时,源柱子上的所有圆盘都是按照从小到大的顺序堆叠的。每次可以移动一个圆盘到另一个柱子,但不允许把较大的圆盘放置在娇小的圆盘之上。最终的目标是把一堆圆盘移动到目标柱子上。我们可以用递归解决这个问题:

/** * 汉若塔 * @param disc 圆盘编号 * @param src 源柱子 * @param aux 辅助用的柱子 * @param dst 目的柱子 */var hanoi = function (disc, src, aux, dst) {    if (disc > 0) {        hanoi(disc - 1, src, dst, aux);        console.log('Move disc ' + disc + ' from ' + src + ' to ' + dst);        hanoi(disc - 1, aux, src, dst);    }};hanoi(3, 'Src', 'Aux', 'Dst');

这里演示了如果圆盘数为 3 的解法:

这个问题可以分解为 3 个子问题。首先移动一对圆盘中较小的圆盘到辅助的柱子上,从而露出下面较大的圆盘;然后再移动下面的圆盘到目标柱子。最后再将较小的圆盘从辅助柱子再移动到目标柱子上。通过递归调用自身来处理圆盘的移动,就可以解决这些子问题。

上面这个函数最终会以一个不存在的圆盘编号被调用,但它不执行任何操作,所以不会导致死循环。

递归函数可以非常高效地操作树型结构。比如文档对象模型(DOM),我们可以在每次递归调用时处理指定树的一小段:

/** * 从某个节点开始,按照 HTML 源码中的顺序,访问该树的每一个节点 * @param node 开始的节点 * @param func 被访问到的每一个节点,会作为参数传入这个函数,然后这个函数被调用 */var walk_the_DOM = function walk(node, func) {    func(node);    node = node.firstChild;    while (node) {        walk(node, func);        node = node.nextSibling;    }};
/** * 查找拥有某个属性的元素 * @param att 属性名称字符串 * @param value 匹配值(可选) * @return 匹配的元素数组 */var getElementsByAttributes = function (att, value) {    var results = [];    walk_the_DOM(document.body, function (node) {        var actual = node.nodeType === 1 && node.getAttribute(attr);        if (typeof actual === 'string' && (actual === value) || typeof value != 'string') {            results.push(node);        }    });    return results;};

注意: 深度递归的函数会因为堆栈溢出而运行失败。比如一个会返回自身调用函数结果的函数,它被称为尾递归函数。

/** * 求阶乘(带尾递归的函数) * * JavaScript 当前没有对尾递归进行优化,所以如果递归过深会导致堆栈溢出 * @param i * @param a * @returns {*} 返回自身调用的结果 */var factorial = function factorial(i, a) {    a = a || 1;    if (i < 2) {        return a;    }    return factorial(i - 1, a * i);};console.log(factorial(4));

9 作用域

作用域控制着变量和参数的可见性以及生命周期,它减少了名称冲突,而且提供自动内存管理机制。

var foo = function () {    var a = 3, b = 5;    var bar = function () {        var b = 7, c = 11;        console.log("a:" + a + ";b:" + b + ";c:" + c);//a:3;b:7;c:11        a += b + c;        console.log("a:" + a + ";b:" + b + ";c:" + c);//a:21;b:7;c:11    };    console.log("a:" + a + ";b:" + b);//a:3;b:5    bar();    console.log("a:" + a + ";b:" + b);//a:21;b:5};foo();

JavaScript 支持函数作用域,但要注意一点,就是在一个函数内部的任何位置定义的变量,都这个函数的任何地方都是可见的!

**注意:**JavaScript 不支持块级作用域。所以最好的做法是在函数体的顶部,声明函数中可能会用到的所有变量。

10 闭包

作用域的好处是:内部函数可以访问定义它们外部函数的参数和变量(除了 this 和 arguments)。

注意:内部函数拥有比它的外部函数更长的生命周期。

var myObject = (function () {    var value = 0;//只对 increment 与 getValue 可见    return {        increment: function (inc) {            value += typeof inc === 'number' ? inc : 1;        },        getValue: function () {            return value;        }    }}());

通过一个函数的形式来初始化 myObject 对象,它会返回一个对象字面量。函数定义了一个 value 变量,这个变量对 increment 和 getValue 方法总是可见的,但它的函数作用域却使得这个变量对其他的代码来说是不可见的!

** * quo 构造函数 * @param status 私有属性 * @returns {{get_status: Function}} */var quo = function (status) {    return {        get_status: function () {//方法            return status;        }    };};var myQuo = quo("amazed");console.log(myQuo.get_status());//amazed

当我们调用 quo 时,它会返回一个包含 get_status 方法的新对象,它的引用保存在 myQuo 中。所以即使 quo 函数已经返回了,但 get_status 方法仍然享有访问 quo 对象的 status 属性的特权。get_status 方法访问的可是 status 属性本身,这就是闭包哦O(∩_∩)O~

再看一个例子:

/** * 设置一个 DOM 节点为黄色,然后渐变为白色 * @param node */var fade = function (node) {    var level = 1;    var step = function () {        var hex = level.toString(16);//转换为 16 位字符        node.style.backgroundColor = '#FFFF' + hex + hex;        if (level < 15) {            level += 1;            setTimeout(step, 100);        }    };    setTimeout(step, 100);};fade(document.body);

fade 函数在最后一行被调用后已经返回,但只要 fade 的内部函数又需要,它的变量就会持续保留。

注意:内部函数能够访问外部函数的实际变量:

var add_the_handlers_error = function (nodes) {    var i;    for (i = 0; i < nodes.length; i += 1) {        nodes[i].onclick = function (e) {            alert(i);//绑定的是变量 i 本身,而不是函数在构造时的变量 i 的值!!!        }    }};

add_the_handlers_error 函数的本意是:想传递给每个事件处理器一个唯一的 i 值,但因为事件处理器函数绑定了变量 i 本身,而不是它的值!

/** * 给数组中的节点设置事件处理程序(点击节点,会弹出一个显示节点序号的对话框) * @param nodes */var add_the_handlers = function (nodes) {    var helper = function (i) {//辅助函数,绑定了当前的 i 值        return function (e) {            alert(i);        };    };    var i;    for (i = 0; i < nodes.length; i += 1) {        nodes[i].onclick = helper(i);    }};

我们在循环之外向构造一个辅助函数,让这个函数返回一个绑定了当前 i 值的函数,这样就可以解决问题啦O(∩_∩)O~

11 回调

假设用户触发了一个请求,浏览器向服务器发送这个请求,然后最终显示服务器的响应结果:

request = prepare_the_request();response = send_request_synchronously(request);display(response);

这种方式的问题在于,网络上的同步请求可能会导致客户端进入假死状态。

所以建议使用异步请求,并为服务端的响应创建一个回调函数。这个异步请求函数会立即返回,这样我们的客户端就不会被阻塞啦:

request = prepare_the_request();send_request_asynchronously(request, function(response){  display(response);)};

一旦接收到服务端的响应,传给 send_request_asynchronously 的匿名函数就会被调用啦O(∩_∩)O~

12 模块模式

模块是一个提供接口但却隐藏状态与实现的函数。可以使用函数和闭包来构建模块。通过函数来生成模块,就可以不用全局变量啦。

假设我们想给 String 增加一个 deentityify 方法。它会寻找字符串中的 HTML 字符,并把它们替换为对应的字符。这就需要在对象中保存字符实体的名字和它对应的字符。不能用全局变量,因为它是魔鬼!如果定义在函数内部,那么就会带来运行时的损耗,因为每次执行函数时,这个字面量就会被求值一次。所以理想的方式是把它放入闭包:

String.method('deentityify', function () {    //字符实体表:映射字符实体的名字到对应的字符    var entity = {        quot: '"',        lt: '<',        gt: '>'    };    //返回 deentityify 方法    return function () {        /**         * 返回以'&'开头 和 以';'结尾的子字符串         */        return this.replace(/&([^$;]+);/g, function (a, b) {            var r = entity[b];//b:映射字符实体名字            return typeof r === 'string' ? r : a;//a:原始字符串        });    };}());

注意:最后一行是 (),所以我们是立即调用刚刚创建的函数!

模块模式利用函数的作用域和闭包来创建被绑定对象和私有成员的关联。这个例子中只有 deentityify 方法才有权访问 entity (字符实体表)。

模块模式是一个定义了私有变量和函数的函数。先利用闭包创建一个可以访问私有变量和函数的特权函数,最后返回这个函数,或者把它保存到一个可以被访问到的地方。

模块模式做到了信息隐藏,是一种优秀的设计实践,特别适合于封装应用程序或者构造单例哦O(∩_∩)O~

模块模式还可以产生安全对象。比如我们想构造一个能够产生序列号的对象:

/** * 产生唯一字符串的对象(安全的对象) * 唯一字符串由 (前缀 + 序列号) 组成 * @returns {{set_prefix: Function, set_seq: Function, gensym: Function}} */var serial_number = function () {    var prefix = '';    var seq = 0;    return {        /**         * 设置前缀         * @param p         */        set_prefix: function (p) {            prefix = String(p);        },        /**         * 设置序列号         * @param s         */        set_seq: function (s) {            seq = s;        },        /**         * 产生唯一的字符串         * @returns {string}         */        gensym: function () {            var result = prefix + seq;            seq += 1;            return result;        }    };};var seqer = serial_number();seqer.set_prefix('Q');seqer.set_seq(1000);var unique = seqer.gensym();console.log(unique);//Q1000

sequer 包含的方法没有用到 this 或 that,所以很安全。sequer 是一组函数的集合,只有那些特权函数才能够获取或修改私有属性哦O(∩_∩)O~

如果把 sequer.gensym 作为值传递给第三方函数,那么那个函数可以使用它产生唯一的字符串,但却不能通过这个函数改变 prefix 或 seq 的值。因为这个函数的功能只是“产生唯一的字符串”呀!

13 级联

如果我们让某些方法返回 this,那么就会启动级联。在级联中,我们可以在单条语句中依次调用同一个对象的多个方法,最著名的例子就是 jQuery 哦O(∩_∩)O~

形如:

getElement('myDiv')  .move(350,150)  .width(100);

级联可以产生出极富表现力的接口。

14 柯里化

柯里化指的是:把函数与传递给它的参数结合,产生出新的函数:

Function.method('curry', function () {    var slice = Array.prototype.slice,        args = slice.apply(arguments),//创建一个真正的数组        that = this;    return function () {        return that.apply(null, args.concat(slice.apply(arguments)));    };});var add1 = add.curry(1);console.log(add1(6));//7

因为 arguments 不是真正的数组,所以没有 concat 方法,因此我们使用 slice 创建出了真正的数组。

15 记忆

有时候可以把先前计算过的结果记录在某个对象中,避免重复计算。

假设我们想通过递归来计算 Fibonacci 数列。一个 Fibonacci 数字是之前的两个 Fibonacci 数字之和,最前面的两个数字是 0 和 1:

var fiboncci = function () {    var memo = [0, 1];//存储结果(已经计算过的值)    var fib = function (n) {        var result = memo[n];        if (typeof result !== 'number') {            result = fib(n - 1) + fib(n - 2);            memo[n] = result;        }        return result;    };    return fib;}();for (var i = 0; i <= 10; i += 1) {    console.log(i + ": " + fiboncci(i));}

我们把计算结果存储在 memo 数组中,它被隐藏在闭包里。函数被调用时,它会先检查计算结果是否已存在,如果存在就立即返回。

我们对这个例子进行扩展,构造一个带记忆功能的函数:

/** * 带记忆功能的函数 * @param memo 初始的 memo 数组 * @param formula 公式函数 * @returns {Function} */var memoizer = function (memo, formula) {    var recur = function (n) {        var result = memo[n];        if (typeof result != 'number') {            result = formula(recur, n);            memo[n] = result;        }        return result;    };    return recur;};

这个函数会返回一个可以管理 meno 存储参数和在需要时调用 formula 函数的 recur 函数。recur 函数和它的参数会被传递给 formula 函数(就是公式)。是不是感觉有点绕,我们来看看实例就会清楚啦。

现在我们使用 memoizer 函数来重新定义 fibonacci 函数:

/** * 斐波那契 * @type {Function} */var fibonacci = memoizer([0, 1], function (recur, n) {    return recur(n - 1) + recur(n - 2);});console.log(fibonacci(10));//55

这样清楚了吧,使用这个函数可以极大地减少我们的工作量哦,比如下面的这个阶乘函数:

/** * 阶乘 * @type {Function} */var factorial = memoizer([1, 1], function (recur, n) {    return n * recur(n - 1);});console.log(factorial(10));//3628800
阅读全文
0 0