Secrets of the JavaScript Ninja"/>
翻译 Secrets of the JavaScript Ninja
翻译 Secrets of the JavaScript Ninja (JavaScript忍者禁术)
第四章.挥舞函数(4.Wielding functions)
本章重点:
1.为什么匿名函数如此重要
2.函数中的递归
3.函数可以被引用后再调用
4.如何为函数缓存索引
5.利用函数的能力来实现记忆
6.利用函数上下文
7.处理参数长度
8.判断一个对象是否为函数
在上一章我们了解到函数作为自然类型的对象(first-order objects),并且了解到什么是函数式编程。在本章,我们会利用函数来解决一些问题,也许以后做web开发时候可以用到。
我们展示的例子并不会直接解决你开发中的问题,那岂不成了另一个什么指南之类的书了。
我们知道,本书的本质目的是让你能够真正的了解这门语言的精髓。
4.1 匿名函数(Anonymous functions)
不知道你是否已经熟悉了匿名函数,匿名函数的确是一个十分重要的概念需要我们来理解,如果你还在为JavaScript的忍者头巾而奋斗。他们是是否重要的特点,并且是一个函数化语言的灵魂,例如Scheme
匿名函数通常会被用于后续使用,例如存储一个变量,作为一个对象的方法,用于回调函数(例如timeout或者事件处理)
Listing 4.1: Common examples of using anonymous functions
window.onload= function(){ assert(true, 'power!');};
var ninja = { shout: function(){ assert(true, "Ninja"); }
}
ninja.shout(); setTimeout(function(){ assert(true, 'Forever!')}, 500)
我们将会在本书的后面看到大量的匿名函数,因为是否能将JavaScript使用的很有力量,取决于你是否将它作为函数式语言在使用。所以我们将会在后面加入很重的函数式的代码。
函数式编程专注于:小,每段小代码只做一个事。
4.2 递归(Recursion)
递归是是否有用的技术。你可能以为递归只是用于数学计算。很多情况下是这样的。
但是他同样适合于其他事情,例如遍历一个树,包括DOM本身,我们在web开发中经常遇到。
所以递归是个很有用的概念,通过它我们可以更加深刻的理解函数如何在JavaScript中作用。
让我们开始通过最简单的方式来看看递归
4.2.1 递归在普通函数中(Recursion in named functions)
我们来写一个例子,这个例子是判断一个字符串是否为对称
我们的实现如下:
function isPalindrome(text){ 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));
} Listing 4.2: Chirping using a named function
funcction chirp(n){ return n > 1? chirp(n -1) + "-chirp": "chirp";
} assert(chirp(3) == "chirp-chirp-chirp", "Calling the named function comes naturally.")
上面的例子都是在用实名函数在实现递归,让我们下面看看如何利用匿名函数实现递归。
4.2.2 递归在对象的方法中(Recursion with object methods)
我们对上面的例子做个改造,让一个匿名函数赋予在一个对象的属性上。
Listing 4.3: Method recursion within an object
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, either.”)
4.2.3 引用的丢失问题(The pilfered reference problem)
我们对上个例子继续改造一下,我们增加一个新的对象,samurai,让它继续引用ninja对象的匿名函数。
Listing 4.4 Recursion using a missing function reference
var ninja = { chirp: function(n){ return n > 1? ninja.chirp(n - 1) + "-chirp" : "chirp"; }
} var samurai = { chirp : ninja.chirp};
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?");
}
这里的引用关系是,samurai和ninja同事引用了一个函数,如图4.2:
这并不是问题所在,问题在于这个共同的函数引用了ninja自身,他并不在乎是谁调用的他。
我们可以修复这个问题,相比于通过在匿名函数中引用ninja对象,不如通过函数上下文(this)来搞定这个问题:
var ninja = { chirp: funciton(n){ return n > 1 ? this.chirp(n - 1) + "-chirp" : "chirp"; }
}
还记得函数作为方法被调用的时候函数上下文就是对象本身吗?
所以当samurai.chirp()调用的时候,this就指向了samurai。
4.2.4 内部实名函数(Inline named functions)
Listing 4.5: Using a inline function in a recursive fashion
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 = {}; assert(samurai.chirp(3) == "chirp-chirp-chirp", "The method correctly calls itself."); Listing 4.6: Verifying the identity of an inline function
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.")
4.2.5 callee属性(The callee property)
我们来看另外的一个函数的概念:arguments参数的属性callee
Listing 4.7: Using arguments.callee to reference the calling function var ninja={ chirp: function(n){ return n>1? arguments.callee(n-1) + "-chirp" : "chirp"; }
} assert(ninja.chirp(3) == "chirp-chirp-chirp", "arguments.callee is the function itself.")
arguments有一个属性叫做callee,这个属性指向的是当前执行的函数。这个属性可以一直用于在函数内部获取到函数自身。
4.3 函数作为对象(Fun with function as objects)
在JavaScript中的函数并不像其他语言中的函数,JavaScript给予了函数很多的能力,不只是被当作自然对象(first-class objects)
我们已经看到,函数可以拥有属性,可以拥有方法,可以被赋到变量和属性上,但是最牛的一个能力是他们可以被调用(callable)
首先让我们看一下将函数缓存在一个集合中,然后要研究“memoizing“这个技术。
4.3.1 存储函数(Storing functions)
很多时候,我们会想要存储一些具有唯一标识的函数。
当我们想添加一个函数到一个集合中的时候,我们需要判断这个函数之前是否已经被添加到这个集合中了。
之前我们用的普通的方法是将函数添加到一个数组里,每次添加新的函数的时候都要遍历一次数组进行比较,我们下面要做得更好一些。
我们可以利用函数自身的属性来达到这个目的。
Listing 4.8: Storing a collection of unique functions
var store = { nextId: 1, cache: {}, add: function(fn){ if (!fn.id){ fn.id = store.nextId++; return !!(store.cache[fn.id] = fn); } }
} function ninja(){}; assert(store.add(ninja), "Function was safely added.");
assert(!store.add(ninja), "But it was only added once.");
这里要注意:!!是一个很简单的方式,让任意JavaScript表达式变成Boolean的方式,
例如:!!”hello” === true and !!0 === false
4.3.2 自身缓存函数(Self-memoizing functions)
Memoization是让函数具有记忆能力的技术,可以让函数记住上一次计算的结果。他可以提升效率。
MEMOIZING EXPENSIVE COMPUTATIONS
如果是负责计算,这里的情况很适合。我们会构建一个”answer cache” ,它会存储上一次运行的结果。
Listing 4.9: Meemoizing previously-compued values
function isPrime(value){ if (!isPrime.anwers) isPrime.answers={}; if (isPrime.answers[value] != null){ return isPrime.answers[value]; } var prime = value != 1; // 1 can never be prime 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!");
MEMOIZING DOM ELEMENTS
function getElements(name){ if (!getElements.cache) getElements.cache = {};
return getElements.cache[name] = getElements.cache[name] || document.getElementsByTagName(name);
}
4.3.3 Faking array methods
我们来构造自定义的一个类似Array的对象
Listing 4.10: Simulating array-like methods
<input id="first"/>
<input id="second"/>
var elems = { length: 0, add: function(elem){ Array.prototype.push.call(this, elem); }, find: function(id){ this.add(document.getElementById(id)); }
} elems.find("first");
assert(elems.length == 1 && elems[0].nodeType, "Verify that we have an element in our stash"); elems.find("second");
assert(elems.length == 2 && elems[1].nodeTpe, "Verify the other insertion");
4.4 Variable-length argument lists
JavaScript是一门很灵活的语言.其中具体的一个特点是他可以让函数接受任意长度的arguments。
我们下面要看一些列子来如何应用这个特性来增强他们的能力,通过这些例子我们可以学到:
1.如何支持任意数量的arguments
2.如何利用variable-length argument列表来实现函数的重载
3.如何认识和使用argument列表的length属性
让我们来利用apply()来搞定这些
4.4.1 Using apply() to supply variable arguments
如果我们求一个集合的最大值,我们会想到Math
例如:
var biggest = Math.max(1,2);
var biggest = Math.max(1,2,3);
var biggest = Math.max(1,2,3,4);
但是,我们不能这么做:
var biggest = Math.max(listp[0], list[1], list[2]);
如果搞定这个问题?通过apply()或者call()
Listing 4.11: Generic min and max funcctions for arrays
function smallest(array){ return Math.min.apply(Math, array);
} function largest(array){ return Math.max.apply(Math, array);
} assert(smallest([0,1,2,3]) == 0, "Located the smallest value.");
assert(largest([0,1,2,3]) == 3, "Located the largest value.");
这里通过apply将数组的参数转换成了正常参数:
Math.min(0,1,2,3);
4.4.2 Function overloading
在3.2章节,我们知道argments参数是隐性的传递给被调用的函数的,现在让我们详细的看一下这它。
所有函数都接受到这个隐性的参数,它给了我们力量来处理未知数目的参数。
让我们来看看如何通过它来实现函数的重载(function overloading)
DETECTING AND TRAVERSING ARGUMENTS
在大部分面向对象的语言中,重载一般是通过定义同样的名字的函数,通过定义不同的参数来实现区别。但是在JavaScript不是这样,在JavaScript中我们可以只用一个函数,在函数内部通过判断参数的个数来实现逻辑划分。
在下面的例子中,我们要merge两个对象的属性到一个root对象中。
Listing 4.12: Traversing variable-length argument lists
function merge(root){ for (var i = 1; i < arguments.length; i++){ for (var key in arguments[i]){ root[key] = arguments[i][key]; } } return root;
} var merged = merge( {name: "Batou"}, {city: "Niihama"}); assert(merged.name == "Batou", "The original name is intact.");
assert(mmerged.city == "Niihama", "And the city has been copied over.");
SLICING AND DICING AN ARGUMENTS LIST
我们要利用arrays的slice()方法来忽略arguments的第一个参数
让我们来看看例4.13
Listing 4.13: Slicing the arguments list
function multiMax(multi){ return multi*Math.max.apply(Math, arguments.slice(1));
} assert(multiMax(3,1,2,3) == 9, "3*3 =9(First arg, by largest.");
运行之后报错,因为argument不是Array,所以他没有slice方法。
让每重写一下这段代码
Listing 4.14: Slicing the arguments list - successfully this time
function multiMax(multi){ return multi * Math.max.apply(Math, Array.prototype.slice.call(arguments, 1));
} assert(multiMax(3,1,2,3) == 9, "3*3=9 (First arg, by largest.");
我们利用Array的slice()方法,让arguments看起来也是一个array,尽管它自身不是。
FUNCTION OVEERLOADING APPROACHES
普通的做法是根据参数的不同在函数内部写if-then-else-if代码块,但是这样又太不简洁。
我们可以利用一个不常用的函数的属性来实现代码的简洁化。
THE FUNCTION’S LENGTH PROPERTY
函数中有一个我们很少知道的属性,但是它能告诉我们这个函数是如何被定义的,它就是length属性。
请不要把这个属性和arguments的length属性混淆,它是专指,被显性定义的函数的参数的个数。
因此,如果我们只是定义了一个入参的函数,那么这个属性值就是1。
代码示例如下:
function makeNinja(name){}
function makeSamurai(name, rank){}
assert(makeNinja.length == 1, “Only expecting a single argument”);
assert(makeSamurai.length == 2, “Two arguments expected”);
所以针对一个函数,我们可以断定2个事情:
1.通过函数length属性,我们可以知道他的显性参数的数目
2.通过arguments的length属性,我们可以知道调用的时候真正传递进去的参数个数。
让我们看看如同运用这个属性来实现函数的重载
OVERLOADING FUNCTIONS BY ARGUMENT COUNT
这里有很多方式来根据参数实现函数的重载。
一个普遍的做法是根据参数的类型,另一种是根据参数的个数。
让我们看看如何通过参数的个数来实现:
var ninja = { whatever: function(){ switch(arguments.length){ case 0: /* do something */ break; case 1: /* do something else */ case 2: /* do yet something else */ break; // and so on... } }
}
在这段代码中,每个case对应的是argument实际传入的数量。
但是不够简洁,不够忍者。
让我们再写一段代码,如果我们想让重载的逻辑在调用的时候:
var ninja = {}
addMethod(ninja, ‘whatever’, function(){/* do something */});
addMethod(ninja, ‘whatever’, function(a){/* do something else */});
addMethod(ninja, ‘whatever’, function(a,b){/* yet something else */});
通过这种方式,我们在调用的时候才定义重载的逻辑,漂亮并且简洁吧。
但是我们还没有定义addMethod这个函数,下面我们来实现它。
让我们来看例4.15
Listing 4.15 A method overloading function
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); }
}
我们的addMethod()函数接受了三个参数:
1.一个对象,作为载体
2.一个要被绑定的方法名字
3.要被绑定的方法的声明
让每看例4.16来测试一下我们的新函数:
Listing 4.16 Testing the addMethod function var ninjas = { values: ["Dean Edwards", "Sam Stephenson", "Alex Russell"]
} addMethod(ninjas, "find", function(){ return this.values;
}) addMethod(ninjas, "find", function(name){ var ret = []; for (var i = 0; i < this.values.length; i++) if (this.valuespi].indexOf(name) == 0) ret.push(this.values[i]); return ret;
}) addMethod(ninjas, "find", function(first, last){ var ret = []; for(var i = 0; i < this.values.length; i++) if (this.values[i] == (first + " " + last)) ret.push(this.values[i]); return ret;
}) assert(ninjas.find().length == 3, "Found all ninjas");
assert(ninjas.find("Sam").length == 1, "Found ninja by first name");
assert(ninjas.find("Dean", "Edwards").length == 1, "Found ninja by first and last name");
assert (ninjas.find("Alex", "X", "Russell") == null, "Found nothing");
这段代码很整洁,因为我们没有将函数真正存储在一个明显的数据结构中。我们是通过闭包(closures)来实现的,我们会在下一个章节来讨论闭包
本章到此为止,我们学习了一个函数如何被当作一个自然对象来使用,我们还要知道如何判断一个对象是否是一个函数。
4.5 Checking for functions
大多数浏览器都可以通过typeof来判断一个对象的类型,
例如:
function ninja(){}
assert(typeof ninja == “function”, “Functions have a type of function”);
当然有些浏览器是不支持这么写。这就要设计到浏览器兼容性问题
4.6 Summary
1.匿名函数可以让代码更简洁
2.递归函数让我们知道函数如何被引用:
1)通过名字
2)通过方法
3)通过变量
4)通过arguments的callee属性
3.函数拥有一些属性,我们可以利用这些属性来存储信息,包括:
1)存储函数,可以后续来调用和引用
2)利用函数的属性来实现缓存(memoization)
4.通过控制函数上下文,我们可以让一个对象的方法不再为这个对象服务,可以利用Array和Math拥有的方法来为我们所用,来计算我们自己的数据。
5.函数可以根据参数的不同而执行不同的逻辑(function overloading).
6.利用typeof关键字我们可以检查一个对象实例是否为函数.这里要考虑浏览器兼容性问题。
(转载本文章请注明作者和出处 Yann (yannhe),请勿用于任何商业用途)
更多推荐
翻译 Secrets of the JavaScript Ninja
发布评论