第4章 函数
函数用于指定对象的行为。一般来说,所谓编程就是将一组需求分解成一组函数与数据结构的技能。
4.1 函数对象
在JS中函数就是对象。对象是“名/值”对的集合并拥有一个连到原型对象的隐藏连接。对象字面量产生的对象连接到 Object.prototype。函数对象连接到 Function.prototype。每个函数在创建时附有两个附加的隐藏属性:函数的上下文和实现函数行为的代码。
函数是对象,但与众不同之处在于它们可以被调用。
4.2 函数字面量
函数可以通过函数字面量来创建:
var add = function (a, b) {
return a + b;
};
函数字面量包括四个部分:
一、保留字 function。
二、函数名,若省略则为匿名函数。
三、参数,可无。
四、函数主体。
函数字面量可以出现在任何允许表达式出现的地方。函数也可以被定义在其他函数中。一个内部函数自然可以访问自己的参数和变量,同时它也能方便地访问它被嵌套在其中的那个函数的参数与变量。
通过函数字面量创建的函数对象包含一个连到外部上下文的连接。这被称为闭包。它是JS强大表现力的根基。
4.3 调用
JS中一共有四种调用模式:方法调用、函数调用、构造器调用、apply调用。这些模式在如何初始化关键参数 this 上存在差异。
1. 方法调用
当一个函数被保存为对象的一个属性时,我们称它为一个方法。当一个方法被调用时,this 被绑定到该对象。如果一个调用表达式包含一个属性存取表达式(即一个 . 点表达式或 [subscript] 下标表达式),那么它被当作一个方法来调用。
//创建myObject。它有一个value属性和一个increment(增加)方法。
//increment方法接受一个可选的参数。如果参数不是数字,那么默认使用数字1.
var myObject = {
value: 0,
increment: function (inc) {
this.value += typeof inc === 'number' ? inc : 1;
}
};
myObject.increment();
document.writeln(myObject.value); //1
myObject.increment(2);
document.writeln(myObject.value); //3
方法可以使用 this 去访问对象,所以它能从对象中取值或修改对象。通过 this 可取得它们所属对象的上下文的方法称为公共方法。
2. 函数调用
如果在一个函数前面带上 new 来调用,那么将创建一个隐藏连接到该函数的 prototype 成员的新对象,同时 this 将会绑定到那个
当一个函数并非一个对象的属性时,那么它被当作一个函数来调用。
var sum = add ( 3, 4);
当函数以此模式调用时,this 被绑定到全局对象,这是语言设计的错误,应该绑定到外部函数的 this 变量。这个设计错误的后果是方法不能利用内部函数来帮助工作。但有一个解决办法,如果该方法定义一个变量并给它赋值为 this,那么内部函数就可以通过那个变量访问到 this。
//给myObject添加一个double方法。
myObject.double = function () {
var that = this; //解决方法
var helper = function () {
that.value = add(that.value, that.value);
};
helper(); //以函数形式调用helper
}
//以方法形式调用double
myObject.double();
document.writeln(myObject.value); //6
3. 构造器调用模式
var Quo = function (string) {
this.status = string;
};
Quo.prototype.get_status = function() {
return this.status;
};
var myQuo = new Quo('confused');
document.writeln(myQuo.get_status()); //confused
这种方法并不推荐。
4. apply 调用
apply 方法让我们构建一个参数数组并用其去调用函数。它也允许我们选择 this 的值。apply 方法接受两个参数,第一个是将绑定给 this 的值。第二个就是一个参数数组。
//构造一个包含两个数字的数组,并将它们相加。
var array = [3, 4];
var sum = add.apply(null, array); //sum的值为7
//构造一个包含status成员的对象。
var statusObject = {
status: 'A-OK'
};
var status = Quo.prototype.get_status.apply(statusObject);
//status值为'A-OK'
4.4 参数
无须指定参数个数的情况
//注意该函数内部定义的变量sum不会与函数外部定义的sum产生冲突
//该函数只会看到内部的那个变量
var sum = function () {
var sum = 0;
for (var i = 0; i < arguments.length; i++) {
sum += arguments[i];
}
return sum;
};
document.writeln(sum(4, 8, 15, 16, 23, 42));
但这并不是一个特别有用的模式。
arguements并不是一个真正的数组,它只是拥有 length 属性,但缺少所有数组的方法。
4.5 返回
如果函数以在前面加上 new 前缀的方式来调用,且返回值不是一个对象,则返回 this (该新对象)。
4.6 异常
var add = function (a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw {
name: 'TypeError',
message: 'add needs numbers'
};
}
return a + b;
};
//构造一个try_it函数,用不正确的方式调用之前的add函数
var try_it = function () {
try {
add('seven');
}
catch(e) {
document.writeln(e.name + ': ' + e.message);
}
};
try_it();
4.7 给类型增加方法
通过给 Function.prototype 增加方法来使得该方法对所有函数可用(要先确定没有该方法时才添加它)。
Function.prototype.method = function(name, func) {
if (!this.prototype[name]) {
this.prototype[name] = func;
}
return this;
};
只提取数字中的整数
Number.method('integer', function () {
return Math[this < 0 ? 'ceiling' : 'floor'](this);
});
document.writeln(-3.3.integer()); //-3
移除字符末端空白的方法
String.method('trim', function () {
return this.replace(/^\s+|\s+$/g, '');
});
document.writeln('"' + " neat ".trim() + '"'); //"neat"
4.8 递归
//定义walk_the_DOM函数,它从某个给定的节点开始,按HTML源码中的顺序
//访问该树的每个节点。
//它会调用一个函数,并依次传递每个节点给它。walk_the_DOM调用自身去处理
//每一个节点。
var walk_the_DOM = function walk(node, func) {
func(node);
node = node.firstChild;
while(node) {
walk(node, func);
node = node.nextSibling;
}
};
//定义getElementsByAttribute函数,它取得一个属性名称字符串
//和一个可选 的匹配值
//它调用walk_the_DOM,传递一个用来查找节点属性名的函数。
//匹配的节点会累积到一个结果数组中。
var getElementsByAttribute = function (attr, value) {
var results = [];
walk_the_DOM(document.body, function (node) {
var actual = node.nodeType === 1 && node.getAttribute(att);
if (typeof actual === 'string' &&
(actual === value || typeof value !== 'string') ) {
results.push(node);
}
});
return results;
};
阶乘
var factorial = function factorial(i, a) {
a = a || 1;
if (i < 2) {
return a;
}
return factorial(i - 1, a * i);
}
document.writeln(factorial(4)); //24
4.9 闭包
var myObject = function () {
var value = 0;
return {
increment: function (inc) {
value += typeof inc === 'number' ? inc : 1;
},
getValue: function () {
return value;
}
}
}();
我们是把调用函数的结果赋值给 myObject,注意最后一行的()。该函数返回一个包含两个方法的对象,并且这些方法继续享有访问value变量的特权。
下面的 quo 函数被设计成无须在前面加上 new 来使用,所以名字也没有首字母大写。
//创建一个quo的构造函数。
//它构造出带有get_status方法和status私有属性的一个对象。
var quo = function (status) {
return {
get_status: function () {
return status;
}
};
};
//构造一个quo实例
var myQuo = quo('amazed');
document.writeln(myQuo.get_status());
下面的例子更有用,注意要等 document 加载完成后再调用此函数。
// 定义一个函数,它设置一个DOM节点为黄色,然后把它渐变为白色
var fade = function (node) {
var level = 1;
var step = function () {
var hex = level.toString(16);
node.style.backgroundColor = '#FFFF' + hex + hex;
if (level < 15) {
level += 1;
setTimeout(step, 100);
}
};
setTimeout(step, 100);
};
fade(document.body);
理解内部函数能访问外面函数的实际变量而无须复制:
//糟糕的例子
var add_the_handlers = function (nodes) {
var i;
for (i = 0; i < nodes.length; i++) {
nodes[i].onclick = function (i) {
alert(i);
}(i);
}
};
//结束糟糕的例子
//更好的例子
var add_the_handlers = function (nodes) {
var i;
for (i = 0; i < nodes.length; i++) {
nodes[i].onclick = function (i) {
return function () {
alert(i);
};
}(i);
}
};
add_the_handler函数目的是给每个时间处理器一个唯一值(i)。糟糕的例子未能达到目的是因为事件处理器函数绑定了变量 i ,而不是函数在构造时变量 i 的值。
更好的例子中,我们定义了一个函数并立即传递i进去执行,而不是把一个函数赋值给 onclick。那个函数将返回一个事件处理器函数。这个事件处理器函数绑定的是传递进去的 i 的值,而不是定义在add_the_handlers 函数里的 i 的值。那个被返回的函数被赋值给 onclick。
4.11 回调
<span style="color:#000000;">//同步的请求,不好,会导致客户端进入假死状态。
request = prepare_the_request();
response = send_request_synchronously(request);
display(response);
//异步的情况,好,提供一个当服务器的响应到达时将被调用的回调函数。
//异步的函数立即返回,这样客户端不会阻塞。
request = prepare_the_request();
send_request_asynchronously(request, function (response) {
display(response);
});</span>
4.12 模块
String.method('deentityify', function () {
//字符实体表,它映射字符实体的名字到对应的字符
var entity = {
quot: '"',
lt: '<',
gt: '>',
};
//返回deentityify方法。
return function () {
//这才是deentityify方法,它调用字符串的replace方法。
//查找'&'开头和';'结束的子字符串,如果这些字符可以在字符实体表中找到。
//那么就将该字符实体替换为映射表中的值,它用到了一个正则表达式
return this.replace(/&([^&;]+);/g,
function (a, b) {
var r = entity[b];
return typeof r === 'string' ? r : a;
}
);
};
}());
再构造一个产生序列号的方法。
var serial_maker = function () {
var prefix = '';
var seq = 0;
return {
set_prefix: function (p) {
prefix = String(p);
},
set_seq: function (s) {
seq = s;
},
gensym: function () {
var result = prefix + seq;
return result;
}
};
};
var seqer = serial_maker();
seqer.set_prefix('Q');
seqer.set_seq(1000);
var unique = seqer.gensym();
document.writeln(unique);
seqer包括的方法都没用到 this 或 that。因此没有办法损害 seqer。只能调用方法才能改变prefix 和 seq 的值。
4.14 套用
Function.method('curry', function () {
var slice = Array.prototype.slice;
var args = slice.apply(arguments);
var that = this;
return function () {
return that.apply(null, args.concat(slice.apply(arguments)));
};
});
var add4 = add.curry(4);
document.writeln(add4(6));
4.15 记忆
//调用次数太多
var fibonacci = function (n) {
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
};
//调用次数较少
var fibonacci = 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++) {
document.writeln('//' + i + ': ' + fibonacci(i) + '<br>');
}
一般情况
var memoizer = function (memo, fundamental) {
var shell = function (n) {
var result = memo[n];
if (typeof result !== 'number') {
result = fundamental(shell, n);
memo[n] = result;
}
return result;
};
return shell;
};
var fibonacci = memoizer([0, 1], function (shell, n) {
return shell(n - 1) + shell(n - 2);
});
var factorial = memoizer([1, 1], function (shell, n) {
return n * shell(n - 1);
})
for (var i = 0; i < 10; i++) {
document.writeln('//' + i + ': ' + fibonacci(i) + '<br>');
}
for (var i = 0; i < 10; i++) {
document.writeln('//' + i + ': ' + factorial(i) + '<br>');
}