第四章 挥舞函数
在 Web 程序开发中,函数式编程风格是我们所要做事情的重要核心。为了不必要的函数名称污染全局命名空间,将创建大量小型函数进行传递。利用匿名函数进行函数式编程,可以解决在 JavaScript 开发时所面临的很多挑战。
递归
当函数调用自身,或调用另外一个函数,但这个函数的调用树种的某个地方又调用了自己时,递归就发生了。
利用递归检测回文
- 单个和零个字符都是一个回文。
- 如果字符串的第一个字符和最后一个字符相同,并且除了两个字符以为剩余的其它字符也是一个回文的话,原字符串是一个回文。
function isPalindrome(text) {
if (typeof text === "null" || typeof text === "undefined") return false;
var text = text.toString();
if (text.length <= 1) return true;
if (text.charAt(0) != text.charAt(text.length - 1)) return false;
return isPalindrome(text.substr(1, text.length - 2));
}
函数递归的两个条件:引用自身,并且有终止条件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>使用命名函数发出啾啾声</title>
<style>
li.pass{color: green;}
li.fail{color: red;text-decoration:line-through; }
</style>
</head>
<body>
<ul id="results"></ul>
<script>
function chirp(n){
return n>1 ? chirp(n-1)+"-chirp" : "chirp";
}
assert(chirp(3) === "chirp-chirp-chirp", "Calling the named function comes naturally.");
// 创建断言函数
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById("results").appendChild(li);
}
</script>
</body>
</html>
方法中的递归
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>对象中的方法递归</title>
<style>
li.pass{color: green;}
li.fail{color: red;text-decoration:line-through; }
</style>
</head>
<body>
<ul id="results"></ul>
<script>
var ninja = {
chirp: function(n){
return n>1 ? ninja.chirp(n-1)+"-chirp" : "chirp";
}
}
assert(ninja.chirp(3) === "chirp-chirp-chirp", "An object property isn't too confusing,erither.");
// 创建断言函数
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById("results").appendChild(li);
}
</script>
</body>
</html>
我们在函数上使用了非直接引用——也就是ninja
对象的chirp
属性——所以才能进行递归。但这会有问题。
修改一下代码,添加一个新的对象samurai
,该对象也引用了ninja
对象上的匿名递归函数。
var ninja = {
chirp: function(n){
return n>1 ? ninja.chirp(n-1)+"-chirp" : "chirp";
}
}
var samurai = {chirp: ninja.chirp};
// 重新定义ninja对象,去除所有属性
ninja = {};
try{
assert(samurai.chirp(3) === "chirp-chirp-chirp", "Is this going to work?");
}catch(e){
assert(false, "Uh,this isn't good! where'd ninja.chirp go?");
}
上述代码中,重新给ninja
定义一个空对象,但匿名函数仍然存在,而且可以通过samurai.chirp
属性进行引用,但是ninja.chirp
属性却已不存在。修复这个问题,可以在匿名函数中不再使用显式的ninja
引用,而是使用函数上下文this
进行引用,如下所示:
var ninja = {
chirp: function(n){
// return n>1 ? ninja.chirp(n-1)+"-chirp" : "chirp";
// 修复引用丢失 使用 this 进行引用
return n>1 ? this.chirp(n-1)+"-chirp" : "chirp";
}
}
不管是不是作为递归进行调用,当一个函数作为方法被调用时,函数上下文 this 指的是调用该方法的那个对象。
内联命名函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>使用内联函数进行递归</title>
<style>
li.pass{color: green;}
li.fail{color: red;text-decoration:line-through; }
</style>
</head>
<body>
<ul id="results"></ul>
<script>
var ninja = {
chirp: function signal(n){ // 内联函数
return n>1 ? signal(n-1)+"-chirp" : "chirp";
}
}
assert(ninja.chirp(3) === "chirp-chirp-chirp", "Works as we would expect it to!");
// 创建新对象
var samurai = {chirp: ninja.chirp};
// 重新定义ninja对象,去除所有属性
ninja = {};
assert(samurai.chirp(3) === "chirp-chirp-chirp", "The method correctly calls itself.");
// 创建断言函数
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById("results").appendChild(li);
}
</script>
</body>
</html>
还可以将内联函数用于普通的变量赋值:
// 声明一个命名内联函数,并将其赋值给一个变量
var ninja = function myNinja(){
// 在内联函数中,验证两个名字是等价的
assert(ninja === myNinja, "This function is named two things at once!");
};
ninja(); // 调用函数执行内部验证
assert(typeof myNinja === "undefined", "But myNinja isn't defined outside of the function.");
上述代码展示了内联函数最重要的一点:尽管可以给内联函数进行命名,但这些名称只能在自身函数内部可见。内联函数的名称和变量名称有点像,它们的作用域仅限于声明它们的函数。
这就是为什么要将全局函数作为 window 的方法进行创建的原因。不使用 window 的属性,没有办法引用这些函数。
已经被移除的callee属性
警告:ECMAScript 5 禁止在严格模式中使用
arguments.callee()
。当一个函数必须调用自身的时候,假如它是函数表达式则给它命名,或者使用函数声明,避免使用arguments.callee()
。
var ninja = {
chirp: function(n){
return n>1 ? arguments.callee(n-1)+"-chirp" : "chirp";
}
};
将函数视为对象
有时候,我们可能需要存储一组相关又独立的函数,事件回调管理是最明显的例子。向这个集合添加函数时,面临的挑战是要确定哪些函数在集合中不存在,应该添加,哪些函数已经存在而不需要添加。
利用函数的属性特性,给函数添加一个附加属性从而实现上述目的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>存储一组独立的函数</title>
<style>
li.pass{color: green;}
li.fail{color: red;text-decoration:line-through; }
</style>
</head>
<body>
<ul id="results"></ul>
<script>
var store = {
nextId: 1,
cache: {}, // 创建一个对象作为缓存,存储函数
// 向缓存中添加函数,但只有缓存不存在的情况下才能添加成功
add: function(fn){
if(!fn.id){
fn.id = store.nextId++;
return !!(store.cache[fn.id] = fn); // !!可以将任意js表达式转化为bool值
}
}
};
function ninja(){}
assert(store.add(ninja), "Function was safely added.");
assert(!store.add(ninja), "But it wwas only added once.");
// 创建断言函数
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById("results").appendChild(li);
}
</script>
</body>
</html>
另一有用的技巧是,通过暴露函数属性,可以对函数自身进行修改。缓存记忆是构建函数的过程,这种函数能够记住先前计算的结果。以下以判断素数作为演示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>记忆之前计算过的值</title>
<style>
li.pass{color: green;}
li.fail{color: red;text-decoration:line-through; }
</style>
</head>
<body>
<ul id="results"></ul>
<script>
function isPrime(value){
if(!isPrime.answers) isPrime.answers = {}; // 创建缓存
// 检查缓存过的值
if(isPrime.answers[value] != null){
return isPrime.answers[value];
}
var prime = value != 1;
for(var i=2; i<value; i++){
if(value % i == 0){
prime = false;
break;
}
}
// 保存计算出的值
return isPrime.answers[value] = prime;
}
assert(isPrime(5), "5 is prime!");
assert(isPrime.answers[5], "The answer was cached!");
// 创建断言函数
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById("results").appendChild(li);
}
</script>
</body>
</html>
缓存记忆DOM元素:
function getElements(name){
if(!getElements.cache) getElements.cache = {};
return getElements.cache[name] = getElements.cache[name] || document.getElementsByTagName(name);
}
伪造数组方法
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>模拟类似数组的方法</title>
<style>
li.pass{color: green;}
li.fail{color: red;text-decoration:line-through; }
</style>
</head>
<body>
<ul id="results"></ul>
<input type="text" id="first">
<input type="text" id="second">
<script>
var elems = {
length: 0, // 保存元素个数
// 添加元素
add: function(elem){
Array.prototype.push.call(this,elem);
},
// 根据id查找元素并添加到集合中
gather: function(id){
this.add(document.getElementById(id));
}
};
elems.gather("first");
assert(elems.length == 1 && elems[0].nodeType, "Verify that we have an element in our stash");
elems.gather("second");
assert(elems.length == 2 && elems[1].nodeType, "Verify The other insertion");
// 创建断言函数
function assert(value,desc){
var li = document.createElement('li');
li.className = value ? 'pass' : 'fail';
li.appendChild(document.createTextNode(desc));
document.getElementById("results").appendChild(li);
}
</script>
</body>
</html>
可变长度的参数列表
使用apply()
将数组作为一个可变长度的参数列表,实现判断数组的最大值与最小值。
function smallest(array){
return Math.min.apply(Math,array);
}
function largest(array){
return Math.max.apply(Math,array);
}
使用arguments
实现函数重载:
function merge(root){
// 跳过第一个参数,索引从1开始
for(var i=1; i<arguments.length; i++){
for(var key in arguments[i]){
root[key] = arguments[i][key];
}
}
return root;
}
对arguments列表进行切片:
function multiMax(multi){
// 由于arguments并不是数组,所以不能直接使用slice
return multi * Math.max.apply(Math, Array.prototype.slice.call(arguments,1));
}
函数的 length 属性
函数的 length 属性和 arguments 的 length 属性不同。该属性的值等于该函数声明时所需要传入的形参数量。
对于一个函数,在参数方面可以确定两件事:
- 通过
length
属性,可以知道形参数量。 - 通过
arguments.length
,可以知道实参数量。
利用参数个数进行函数重载
函数重载的方法有:
- 根据传入参数的类型执行不同的操作。
- 通过某些特定参数是否存在来进行判断。
- 通过传入参数的个数进行判断。
/**
* 定义addMethod方法接收三个参数:
* 1. 要绑定方法的对象
* 2. 绑定发放所用的属性名
* 3. 要绑定的方法
*/
function addMethod(object, name, fn) {
// 保存原有的函数,调用的时候可能不匹配传入的参数个数
var old = object[name];
object[name] = function () {
// 如果该匿名函数的形参个数和实参个数匹配,就调用该函数
if (fn.length == arguments.length) {
return fn.apply(this, arguments);
} else if (typeof old === 'function') {
return old.apply(this, arguments);
}
}
}
var ninja = {};
addMethod(ninja, 'whatever', function () {/**/ });
addMethod(ninja, 'whatever', function (a) {/**/ });
addMethod(ninja, 'whatever', function (a, b) {/**/ });
函数判断
function ifFunction(fn){
return Object.prototype.toString.call(fn) === "[object Function]";
}
访问Object.prototype
的内部toString()
方法。在默认情况下,这个特殊方法是用来返回表示一个对象的内部描述的字符串(如Function或String)。利用该方法,我们可以在任何对象上调用它,从而获得对象真正的类型。这种技术不仅可以判断是不是函数,还可以判断 String、RegExp、Date或其他对象。
上述代码中不直接调用 fn.toString()
的原因有两个:
- 不同的对象可能有自己的
toString()
方法实现。 - JavaScript中的大多数类型都已经有一个预定义的
toString()
方法覆盖了Object.prototype
提供的toString()
方法。