# 高级函数
函数是JavaScript 中最有趣的部分之一。它们本质上是十分简单和过程化的,但也可以是非常复杂 和动态的。一些额外的功能可以通过使用闭包来实现。此外,由于所有的函数都是对象,所以使用函数 指针非常简单。这些令JavaScript 函数不仅有趣而且强大。以下几中场景描绘了几种在JavaScript 中使用函数 的高级方法。
# 安全的类型检测
Javascript 内置的类型检测机制并非完全可靠.事实上,发生错误否定及错误肯定的情况也不在少数。
比如 在 Safari(直至第4版)在对 正则表达式
使用 typeof
操作符时会返回 function
,因此很难确定某个值是不是函数,
还有 typeof null === 'object
这个争议
如下场景:
var isArray = value instanceof Array
// 以上 代码要想 返回 true ,value 必须是一个数组,而且还必须与 Array 构造函数再同一个全局作用域中
// 但是 Array 是 window 的属性,所以如果 value 是在另一个 frame 中定义的数组,那么以上代码也不会返回 true,而是返回 false
在检测某个对象到底是原生对象还是开发人员自定义的对象的时候,也会有问题。出现这个问题的 原因是浏览器开始原生支持JSON 对象了。因为很多人一直在使用Douglas Crockford 的JSON 库,而该 库定义了一个全局JSON 对象。于是开发人员很难确定页面中的JSON 对象到底是不是原生的。
针对上述问题的解决方法都一样:由于在任何值上调用
Object
原生的toString()
方法,都会返回一个[object NativeConstructorName]
格式的字符串。每个类在内部 都有一个[[class]]
属性,这个属性中就指定了上述字符串中的构造函数名。例如:
value = [1,2,3]
alert(Object.prototype.toString.call(value)) // [object Array]
由于原生数组的构造函数名与全局作用域无关,因此使用toString()
就能保证返回一致的值。因此利用这一点,就可以来检测 一个变量值 的类型。
function isArray(value){
return Object.prototype.toString.call(value) === "[object Array]"
}
function isFunction(value){
return Object.prototype.toString.call(value) === "[object Function]"
}
function isRegExp(value){
return Object.prototype.toString.call(value) === "[object RegExp]"
}
# 作用域安全的构造函数
当使用 new
操作符来调用一个构造函数时,会创建一个新的实例,同时会给这个实例分配属性和方法。但当没有使用 new
操作符而是直接调用构造函数时,由于 this
对象是在运行时绑定的,所以直接调用构造函数(没有使用 new
操作符), this
会映射到全局对象 window
上,导致 错误对象属性的意外增加。例如:
function Person(name,age,job){
this.name = name
this.age = age
this.job = job
}
var person = Person("zs",18,"programmer")
alert(window.name) // "zs"
alert(window.age) // 18
alert(window.job) // "programmer"
这里,原本针对Person 实例的三个属性被加到window 对象上,因为构造函数是作为普通函数调 用的,忽略了new 操作符。这个问题是由this 对象的晚绑定造成的,在这里this 被解析成了window 对象。由于window 的name 属性是用于识别链接目标和frame 的,所以这里对该属性的偶然覆盖可能 会导致该页面上出现其他错误。 这个问题的解决方法就是创建一个作用域安全的构造函数。
作用域安全的构造函数在进行任何更改前,首先确认
this
对象是正确类型的实例。如果不是,那么会创建新的实例并返回
function Person(name,age,job){
if(this instanceof Person){
this.name = name
this.age = age
this.job = job
}else{
return new Person(name,age,job)
}
}
var p1 = Person("zs",18,"teacher")
alert(p1.name) // "zs"
alert(window.name) // ""
var p2 = new Person("ls",19,"student")
alert(p2.name) // "ls"
function Polygon(sides){
if (this instanceof Polygon) {
this.sides = sides;
this.getArea = function(){
return 0;
};
} else {
return new Polygon(sides);
}
}
function Rectangle(width, height){
Polygon.call(this, 2);
this.width = width;
this.height = height;
this.getArea = function(){
return this.width * this.height;
};
}
var rect = new Rectangle(5, 10);
alert(rect.sides); //undefined
上面这段重写的代码中,一个Rectangle 实例也同时是一个Polygon 实例,所以Polygon.call() 会照原意执行,最终为Rectangle 实例添加了sides 属性。 多个程序员在同一个页面上写JavaScript 代码的环境中,作用域安全构造函数就很有用了。届时, 对全局对象意外的更改可能会导致一些常常难以追踪的错误。除非你单纯基于构造函数窃取来实现继 承,推荐作用域安全的构造函数作为最佳实践。
# 惰性载入函数
因为 浏览器之间的 行为差异,多数 Javascrip 代码包含了 大量 的 if
语句,将执行引导到正确的代码中。比如 为了实现 某个 api 的封装对浏览器的能力检测,能力检测过后,进入 else 的代码块 在某一个浏览器中永远都不会去执行。 比如 下面的 http能力的封装
function createXHR(){
if(typeof XMLHttpRequest != "undefined"){
return new XMLHttpRequest()
}else if(typeof ActiveXObject != "undefined"){
if(typeof arguments.callee.activeXString != "string"){
var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"];
var i , len;
for(i=0;len=versions.length;i<len;i++){
try{
new ActiveXObject(version[i]);
arguments.callee.activeXString = versions[i];
break;
}catch(ex){
// 跳过
}
}
}
return new ActiveXObject(arguments.callee.activeXString)
}else{
throw new Error("No XHR object available")
}
}
每次调用createXHR()的时候,它都要对浏览器所支持的能力仔细检查。首先检查内置的XHR, 然后测试有没有基于ActiveX 的XHR,最后如果都没有发现的话就抛出一个错误。每次调用该函数都是 这样,即使每次调用时分支的结果都不变:如果浏览器支持内置XHR,那么它就一直支持了,那么这 种测试就变得没必要了。即使只有一个if 语句的代码,也肯定要比没有if 语句的慢,所以如果if 语 句不必每次执行,那么代码可以运行地更快一些。解决方案就是称之为惰性载入的技巧。
惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式
- 第一种: 在函数被调用时在处理函数。在第一次调用的过程中,该函数会被覆盖为另外一个按合适方式执行的函数,这样任何对原函数的调用都不用在金国执行的分支了。
- 第二种:在声明函数时就指定适当的函数。这样第一次调用函数时就不会损失性能了,而在代码首次加载时会损失一点性能。
function createXHR(){
if(typeof XMLHttpRequest != "undefined"){
creatXHR = function(){
return new XMLHttpRequest()
}
}else if(typeof ActiveXObject != "undefined"){
creatXHR = function(){
if(typeof arguments.callee.activeXString != "string"){
var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"];
var i , len;
for(i=0;len=versions.length;i<len;i++){
try{
new ActiveXObject(version[i]);
arguments.callee.activeXString = versions[i];
break;
}catch(ex){
// 跳过
}
}
}
return new ActiveXObject(arguments.callee.activeXString)
}
}else{
creatXHR = function(){
throw new Error("No XHR object available")
}
}
return createXHR()
}
在这个惰性载入的createXHR()中,if 语句的每一个分支都会为createXHR 变量赋值,有效覆 盖了原有的函数。最后一步便是调用新赋的函数。下一次调用createXHR()的时候,就会直接调用被 分配的函数,这样就不用再次执行if 语句了。
var createXHR = (function(){
if(typeof XMLHttpRequest != 'undefined'){
return function(){
return new XMLHttpRequest()
}
}else if(typeof ActiveXObject != 'undefined'){
return function(){
if(typeof arguments.callee.activeXString != "string"){
var versions = ["MSXML2.XMLHttp.6.0","MSXML2.XMLHttp.3.0","MSXML2.XMLHttp"];
var i , len;
for(i=0;len=versions.length;i<len;i++){
try{
new ActiveXObject(version[i]);
arguments.callee.activeXString = versions[i];
break;
}catch(ex){
// 跳过
}
}
}
return new ActiveXObject(arguments.callee.activeXString)
}
}else {
return function(){
throw new Error("No XHR object available")
}
}
})()
这个例子中使用的技巧是创建一个匿名、自执行的函数,用以确定应该使用哪一个函数实现。实际 的逻辑都一样。不一样的地方就是第一行代码(使用var 定义函数)、新增了自执行的匿名函数,另外 每个分支都返回正确的函数定义,以便立即将其赋值给createXHR()。 惰性载入函数的优点是只在执行分支代码时牺牲一点儿性能。至于哪种方式更合适,就要看你的具 体需求而定了。不过这两种方式都能避免执行不必要的代码。
# 函数绑定
创建一个函数,可以在特定的this
环境中以 知道你参数 调用 另一个函数,这个过程就称为 函数绑定。
函数绑定通常与 回调函数 和 事件处理程序 一起使用,以便在 将函数作为变量传递的同时保留代码的执行 环境。
var handler = {
message: 'Event hadnled',
handleClick: fucntion(event){
console.log(this.message)
}
}
var btn = document.getElementById('myBtn')
var btn2 = document.getElementById('mybtn2')
EventUtil.addHandler(btn,"click",handler.handleClick)
EventUtil.addHandler(btn2,"click",function(event){
handler.handleClick(event)
})
btn.click() // undedined
btn2.click() // Event hadnled
上面的例子中 ,btn
点击后,console出来的是 undefined
是由于没有被保存 handler.handleClick() 的 环境,所以 this
对象最终指向了 DOM
对象btn
按钮而非handler
对象,而 btn2
在进行绑定事件处理程序时利用 闭包 修正了这个问题,利用在 闭包中直接调用 handle.handleClick
保存了 hadnler.handleClick
的执行环境及this
指针人仍指向 handler
对象。
上面的例子很容易让我们想到 bind
函数, 作用是将 函数绑定 到 指定环境。
一个简单的bind
函数一般接收一个 函数 和 一个环境,并返回一个在 给定环境中的调用给定函数 的函数,并且将所有参数原封不动的传递过去
function myBind(fn,context){
return function(){
return fn.apply(context,arguments)
}
}
var handler = {
message:"Event hadnled",
handleClick:function(event){
alert(this.message)
alert(event)
}
}
var btn = document.getElementById('btn')
EventUtil.addHandler(btn,'click',myBind(handler.handleClick,hadnler))
// 等同于
// EventUtil.addHandler(btn,'click',handler.handleClick.bind(hadnler))
btn.click() // "Event hadnled" event对象
上面的例子中,我们使用 bind
函数 创建了一个保持了 执行环境的 函数,并将其传给 EventUtil.addHandler()。 并且 连 event 对象 也被原封不动的传递给了 该函数。
# 函数柯里化
在编码过程中,身为码农的我们本质上所进行的工作就是 -- 将复杂问题分解未多个可编程的小问题。
函数柯里化为实现 多参函数提供了一个递归降解的实现思路---把接收多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数。
与函数绑定紧密相关的主题是函数柯里化(fucntion currying),它用于创建已经设置好了一个或多个参数的函数。函数柯里化的基本方法和函数绑定是一样的:使用闭包返回一个函数。两者的区别在于,当函数被调用时,返回的函数还需要设置一些传入的参数。请看以下例子
function add(num1,num2){
return num1 + num2
}
function curriedAdd(num2){
return add(5,num2)
}
alert(add(2,3)) // 5
alert(curriedAdd(3)) // 8
这段代码定义了两个函数: add() 和cu rri ed.Add ()。后者本质上是在任何情况下第一个参数为5 的add () 版本。尽管从技术上来说curried.Add{ J 并非柯里化的函数,但它很好地展示了其概念。 柯里化函数通常由以下步骤动态创建:调用另一个函数并为它传人要柯里化的函数和必要参数。下 面是创建柯里化函数的通用方式。
// curry() 函数的主要工作就是将被返回函数的参数进行排序
// 传入 的 fn 是 要进行柯里化的函数,其他参数是要传入的值
function curry(fn){
var args = Array.prototype.slice.call(arguments,1); // args 是 要传入给 柯里化函数的参数
return function (){
var innerArgs = Array.prototype.slice.call(arguments) // 存放所有传入的参数
var finalArgs = args.concat(innerArgs)
return fn.apply(null,finalArgs)
}
}
function add(num1,num2){
return num1 + num2
}
var curriedAdd = curry(add,5)
console.log(curriedAdd(3)) // 8
var curriedAdd2 = curry(add,5,12)
console.log(curriedAdd2()) // 17
函数柯里化还常常作为函数绑定的一部分包含在其中,构造出更为复杂的bind()
函数。例如:
function myBind(fn,context){
var args = Array.prototype.slice.call(arguments)
return function(){
var innerArgs = Array.prototype.slice.call(arguments)
var finalArgs = args.concat(innerArgs)
return fn.apply(context,finalArgs)
}
}
var handler = {
message:"Event handled",
handleClick:function(name,event){
console.log(this.message + ':' + name + ':' + event.type)
}
}
var btn = document.getElementById('my-btn')
EventUtil.addHandler(btn,'click',bind(handler.hadnleClick,handler,'my-btn'))
// 等同于
// EventUtil.addHandler(btn,'click',handler.handleClick.bind(handler,'my-btn'))
btn.click() // "Event handled:my-btn:click"