# 高级函数

函数是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"

最后更新时间: 2/21/2022, 3:29:09 PM