JavasScript函数表达式

本章内容

  • 函数表达式的特征
  • 使用函数实现递归
  • 使用闭包定义函数私有变量

函数表达式是javascript 中的一个既强大又容易令人困惑的特性。定义函数的方式有两种:一种是函数声明,另一种是函数表达式。
函数声明如下:

function functionName(arg0, arg1){
  // 函数体
}

首选是function关键字,然后是函数名称,函数声明有一个重要的特征就是 函数声明提升,意思是在执行代码之前会先读取函数声明。即可以把函数声明放在调用它语句的后面。

sayH();
function sayHi() {
  console.log("Hi!");
}

上面例子不会报错,因为在代码执行之前会先读取函数声明。


本章内容讲解的是第二种——函数表达式。函数表达式有几种不同的语法形式。下面是最常见的一种:

var functionName = function(arg0, arg1){
  // 函数体
}

这种形式看起来好像是给常规变量赋值,即创建一个函数并将它赋值给functionName,这种形式创建的函数叫做 匿名函数 因为function关键字后面没有标识符。匿名函数的name属性为空字符串。
函数表达式和其他表达是一样,使用前必须先赋值。否则会报错:

sayHi();
var sayHi() {
  consoler.log("Hi!");
}

理解函数提升的关键,就会理解函数声明与函数表达式之间的区别。代码如下:

if(condition){
  function sayHi(){
    alert("Hi");
  }
}else{
  function sayHi(){
    alert("Yo hoo!")
  }
}

上面代码表面看是表示condition为true时,使用一个sayHi()的定义,否则就是用另一个定义。这在ECMAScript中属于无效语法,浏览器引擎会尝试修正此错误,但问题是浏览器修正错误的方法不一致。大多数浏览器会返回第二个声明,忽略condition,FireFox会在condition为true时,返回第一个。因此这种使用很危险,不应该被如此使用,但是函数表达式会解决这个问题:

var sayHi;
if(condition){
  sayHi = function() {
    alert("Hi");
  }
}else{
  sayHi = function() {
    alert("Yo hoo!");
  }
}

这段代码就没有什么问题,不同的函数会根据condition被赋值给sayHi

能够创建函数再赋值给变量,也就能够把函数作为其他函数的值返回

function createComparisonFunction(propertyName){
  return function(object1, object2){
    var value1 = object1[propertyName];
    var value2 = object2[propertyName];
    if(value1 < value2>{
      return -1;
    })else if(value1 > value2){
      return 1;
    }else {
      return 0;
    }
  }
}

createComparisonFunction()就返回了一个匿名函数。返回的函数可能会被赋值给一个变量,或者以其他方式被调用。不过在函数内部,它是匿名的,在把函数当成值来使用的情况下,都可以使用匿名函数。

递归

递归函数是在一个函数通过调用自身的情况下构成的,如下:

function factorial(num){
  if(num<= 1){
    return 1;
  }else{
    return factoraial(num-1)* num;
  }
}

这是一个经典的递归阶乘函数。虽然看起来没什么问题,但下面的代码却可能导致它出错。

var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)) // 出错

以上代码先把factorial()函数保存在变量anotherFactorial中,然后将factorial变量设置为null,结果指向原始函数的引用只剩下一个。但在接下来调用anotherFactorial()时,由于必须执行factorial(),而factorial已经不再是函数,所以导致错误。这种情况下,使用arguments.callee可以解决这个问题。

我们知道,arguments.callee是一个指向正在执行的函数的指针,因此可以使用它来实现对函数的递归调用,如下:

funtion factorial(num){
  if(num<=1){
    return 1
  }else{
    return num * arguments.callee(num - 1)
  }
}

上面代码和之前代码的区别就是使用arguments.callee代替函数名,可以确保无论怎样调用函数都不会出问题。因此在编写递归函数时,使用arguments.callee总是比使用函数名更保险。

但是在严格模式下,不能通过使用arguments.callee,访问这个属性会导致错误,不过可以使用命名的函数表达式来达成相同的结果。如下:

var factorial = (function f(num){
  if(num <= 1){
    return 1
  }else{
    return num * f(num-1)
  }
})

上面代码创建了一个名为f()命名函数表达式,然后将它赋值给变量factorial。即使赋值给另一个变量,函数的名字f仍然有效,所以递归调用照样能正确完成。这种方式在严格模式和非严格模式都正确。

闭包

闭包是指有权访问另一个函数作用域中变量的函数。创建闭包的方式,就是一个函数内部创建另一个函数,仍以前面的函createComparisonFunction()为例。如下:

function createComparisonFunction(propertyName){
  return function(object1, object2){
    var value1 = object1[propertyName]
    var value2 = object2[propertyName]

    if(value1 < value2){
      return -1;
    }else if(value1>value2){
      return 1;
    }else{
      return 0;
    }
  }
}

此例中,示例函数中有一个匿名函数,匿名函数中访问了外部函数的propertyName。即使这个内部函数被返回了,而且是在函数其他地方被调用了,但它仍然可以访问propertyName,z之所以能够访问,是因为内部函数的作用域链中包含在外部函数的作用域。

在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,......直至作为作用域链终点的全局执行环境。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,所以直到有绝对必要的时候才使用闭包。

闭包与变量

作用域链的这种配置机制引出了一个值得注意的副作用,即:闭包只能取得包含函数中任何变量的最后一个值。不要忘了闭包所保存的时针各变量对象,而不是某一个特殊变量。如下:

function createFunctions(){
  var result = new Array();
  for(var i=0; i<10; i++){
    result[i] = function(){
      return i;
    }
  }
  return result;
}

这个函数会返回一个数组。表面上看,似乎每个函数都应该返回自己的索引值,即位置0到位置9,但实际上,每个函数都返回第10个元素位置,即位置9。因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。当createFunctions返回后,变量i的值是9,此时每个函数痘印用着保存变量i的同一个变量对象,所以每个函数内部i的值都是9.但是我们可以通过创建领一个匿名函数强制让闭包的行为符合预期,如下:

function createFunctions(){
  var result = new Array();
  for(var i=0; i<10; i++){
    result[i] = function(num){
      return function(){
        return num
      }
    }(i)
  }
  return result;
}

重写了前面的createzfunctions()函数,每个函数就会返回各自不同的索引号了。我们没有把闭包赋值给数组,而是定义了一个匿名函数,并立即执行该匿名函数,将结果赋值给数组。这里匿名函数有一个参数num,也就是最终的函数想要的返回值。在调用匿名函数时,传入变量i,由于函数参数是按值传递的,所以就会将变量i的当前值赋值给参数num,而在这个匿名函数内部,又创建并返回了一个访问num的闭包。如此一来,result数组中的每个函数都有自己num变量的一个副本,因此可以反悔不同的数值了。

关于this对象

在闭包中使用this对象也可能会导致一些问题。我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。但有时由于编写闭包的方式不同,这一点可能不会那么明显,下面:

var name = 'gruguy';
var object = {
  name: 'new Name',
  getNameFunc: function(){
    return function(){
      return this.name
    }
  }
}

alert(object.getNameFunc()()); // "gruguy" (非严格模式下)

上面代码县创建了一个全局变量 name,又创建了一个包含name属性的对象。这个对象还包含一个方法——getNameFunc(),它返回了一个匿名函数,而匿名函数又返回了this.name。由于getNameFunc()返回了一个函数,因此调用object.getNameFunc()()就会立即调用他返回的函数,结果就是返回一个字符串。然而,这个例子返回的字符串是“gruguy”,即全局变量name的值。但是,为什么匿名函数没有去的其包含作用于的this对象呢?

每个函数被调用时,其活动对象都回自动获取两个特殊变量: this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了,如下:

var name = "gruguy";

var object = {
  name: "new Name",
  getNameFunc: function(){
    var that = this;
    return function(){
      return that.name;
    }
  }
}
alert(object.getNameFunc()()); // “new name”

上面例子中,我们把this对象赋值给了一个名叫that的变量。而在定义了闭包之后,闭包也可以访问到这个变量,因为他是我们包含函数中特意声明的一个变量。即使在函数返回之后,that也仍然饮用者object,所以调用object.getNameFunc()()就返回了“new name”。

this 和 arguments存在同样的问题。如果想访问作用于中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问到的变量中。

内存泄漏

由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾回收。因此闭包在IE的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域中保存这一个HTML元素,那么就意味着该元素将无法被销毁。如下:

function assignHandler(){
  var element = document.getElementById("someElement");
  element.onclick = function(){
    alert(element.id);
  }
}

以上代码创建了了一个作为element元素事件处理程序的闭包,而这个闭包则又创建了一个循环引用。由于匿名函数保存了一个对assignHandler()的活动对象的引用,因此就会导致无法减少element的引用数量。只要匿名函数存在,element的引用数量至少也是1,所以他所占用的内存就永远不会被回收。那么如何做才能处理呢?如下:

function assignHandler(){
  var element = document.getElementById("someElement");
  var id = element.id;
  element.onclick = function(){
    alert(id);
  }
  element = null;
}

上面代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。最后将element变量设置为null,这样就能消除对DOM对象的引用,顺利的减少其引用数量。

模仿块级作用域

JavaScript中没有块级作用域,这意味着子块级语句中定义的变量,实际上是在包含函数中而非语句中创建的,如下:

function outputNumbers(count){
  for(var i=0; i<count; i++){
    alert(i);
  }
  alert(i); //计数
}

上面函数中定义了一个for循环,而变量i的初始值是0。但是在javascript中,变量i是定义在outputNumbers()的活动对象中,因此从他有定义开始,就可以在函数内部随处访问它。及时向下面这样错误的重新声明同一个变量,也不会改变它的值。

function outoutNumbers(count){
  for(var i=0; i<count; i++){
    alert(i);
  }
  var i; // 重新声明
  alert(i); //计数
}

javascript 不会告诉你是否多次声明了一个变量,遇到此情况,他只会对后续的生命视而不见。匿名函数可以用来魔方块级作用域并避免这个问题

(function(){
  //这里是块级作用域
})();

上面代码定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。紧随其后的一对圆括号会立即调用这个函数。它其实相当于下面的代码:

var someFunction = function(){
  //这里是块级作用域
}
someFunction();

先定义了一个函数,然后调用它。为什么不能在函数声明后面直接写一对圆括号调用,而要加上一对圆括号?是因为JavaScript将function关键字当做函数生命的开始,函数声明后面不能跟原括号,而函数表达式后面可以跟圆括号。所以加上圆括号是为了将函数声明转换为函数表达式。

原文地址:https://www.cnblogs.com/gruguy/p/12642692.html