网络编程 
首页 > 网络编程 > 浏览文章

JavaScript函数的特性与应用实践深入详解

(编辑:jimmy 日期: 2024/11/17 浏览:3 次 )

本文实例讲述了JavaScript函数的特性与应用实践。分享给大家供大家参考,具体如下:

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

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' "htmlcode">
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();//以方法形式调用 helper
console.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);//7
console.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 "htmlcode">
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 的解法:

JavaScript函数的特性与应用实践深入详解

这个问题可以分解为 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' "htmlcode">
/**
 * 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' "htmlcode">
/**
 * 产生唯一字符串的对象(安全的对象)
 * 唯一字符串由 (前缀 + 序列号) 组成
 * @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

感兴趣的朋友还可以使用本站在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行结果。

更多关于JavaScript相关内容可查看本站专题:《JavaScript常用函数技巧汇总》、《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

上一篇:JavaScript继承的特性与实践应用深入详解
下一篇:Three.js实现3D机房效果
在去年的5月23日,借助Intel Bridge Technology以及Intel Celadon两项技术的驱动,Intel为PC用户带来了Android On Windows(AOW)平台,并携手国内软件公司腾讯共同推出了腾讯应用宝电脑版,将Windows与安卓两大生态进行了融合,PC的使用体验随即被带入到了一个全新的阶段。