JavaScript函数的特性与应用实践深入详解
本文实例讲述了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);//63.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));//39arguments 不是一个真正的数组,它只是一个类数组的对象,它拥有 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 的解法:
这个问题可以分解为 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)。
注意:内部函数拥有比它的外部函数更长的生命周期。
上一篇:JavaScript继承的特性与实践应用深入详解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);//Q1000sequer 包含的方法没有用到 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程序设计有所帮助。
下一篇:Three.js实现3D机房效果