# 函数表达式

# 一、函数的特征

引用类型#Function 类型里介绍过定义函数的三种方式:函数声明、函数表达式、构造函数

其中 函数声明 有个重要的特征就是 函数声明提升,意思是在执行代码之前会 先读取函数声明。这就意味着可以把函数声明放在调用它的语句的 后面

foo(); // 输出数值3,因为(1)和(3)会“函数声明提升”,并且最后(3)会覆盖(1)
function foo() {
  // (1) 函数声明提升
  console.log(1);
}
var foo = function () {
  // (2) 变量声明提升,但是函数声明提升会优先。并且还是同名,那就不会进行变量声明提升了
  console.log(2);
};
function foo() {
  // (3) 函数声明提升
  console.log(3);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

函数表达式 ,看起来像是常规的变量赋值语句,即创建一个函数并将它赋值给变量。这种情况下创建的函数叫 匿名函数(拉姆达函数),因为 function 关键字后面没有标识符。理解 函数声明提升 就要要区分函数声明和函数表达式之间的区别,例:

sayHi(); // 错误:函数还不存在
var sayHi = function () {
  console.log("Hi!");
};

/*********也不要这样做****************/
if (condition) {
  function sayHi() {
    console.log("Hi!");
  }
} else {
  function sayHi() {
    console.log("Yo!");
  }
}
/*********可以这样做******************/
var sayHi;
if (condition) {
  sayHi = function () {
    console.log("Hi!");
  };
} else {
  sayHi = function () {
    console.log("Yo!");
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

引用类型#作为值的函数里提过函数 作为值来使用,比如作为函数的返回值:

function comparisonFunction(propertyName) {
  return function (object1, object2) {
    // 作为了comparisonFunction的返回值来使用
    var v1 = object1[propertyName];
    var v2 = object2[propertyName];
    if (v1 < v2) {
      return -1;
    } else if (v1 > v2) {
      return 1;
    } else {
      return 0;
    }
  };
}
var values = [{ age: 3 }, { age: 10 }, { age: 15 }, { age: 1 }, { age: 5 }];
values.sort(comparisonFunction("age"));
console.log(values); // [ { age: 1 }, { age: 3 }, { age: 5 }, { age: 10 }, { age: 15 } ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 二、递归

一个函数通过名字调用自身,这样的结构是 递归函数

// 经典的递归阶乘函数
function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}
1
2
3
4
5
6
7
8

这个函数表面看起来没什么问题,但下面的代码可能导致它出错:

var anotherFactorial = factorial;
factorial = null; // anotherFactorial还指引着原函数,而factorial引进断开了
console.log(anotherFactorial(4)); // 出错,到factorial(num - 1);时发现factorial已经不存在了
1
2
3

arguments.callee是一个指向正在执行的函数的指针,可以使用来它来解决这个问题:

function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1);
  }
}
1
2
3
4
5
6
7

严格模式下不能使用arguments.callee,可以使用命名函数表达式来达成相同的结果:

var factorial = function f(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * f(num - 1);
  }
};
1
2
3
4
5
6
7

# 三、闭包

在“变量、作用域和内存问题”里介绍过执行环境及作用域,可以简单回顾一下函数的创建以及调用。

  • 在某个环境(全局或某个函数内部)中执行时,假如遇到了“创建函数”的语句。创建 这个新函数时,会复制环境里的 作用域链 并保存到新函数的[[Scope]]属性中。
  • 当这个函数被 调用 时,会初始化内部的环境(变量对象、作用域链、this)。用 this 绑定函数调用处的上下文对象;初始化函数内部的变量保存到 变量对象 中;复制函数的[[Scope]]属性作为函数内部的 作用域链,并将变量对象相关的指针也保存在这个作用域链上。

其实 闭包 这个结构存在于上面的过程中了,创建函数时 函数本身和它所处的环境 构成了一个闭包结构,这个闭包涵盖了 嵌套外层函数的环境全局环境 里所有的 变量对象 以及它们的指针也就是 作用域链

var a = 1; // (1)
function foo() {
  // (2)
  var b = 2; // (3)
  function bar() {
    // (4)
    console.log(a); // (5),可以访问全局变量a,输出1
    console.log(b); // (6),可以访问嵌套外层变量b,输出2
    console.log(c); // (7),可以访问嵌套外层变量c,输出3
  } // (8)
  var c = 3; // (9)
  bar(); // (10)
} // (11)
foo(); // (12)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

分析一下上面这个例子,bar 函数在创建时,bar 与 bar 所处的环境(foo 的内部环境和 foo 外部的全局环境)一起构成了一个 闭包 结构。在(10)调用 bar 函数,在(5)(6)(7)里可以访问a b c三个变量。但严格地说,这三个变量不是通过闭包访问的,其实是单纯地通过 作用域链 访问的,这个作用域链其实也是闭包的一部分。这样不好观察闭包,下面这个例子可以很清晰地观察闭包。

var a = 1; // (1)
function foo() {
  // (2)
  var b = 2; // (3)
  function bar() {
    // (4)
    console.log(a); // (5),可以访问全局变量a,输出1
    console.log(b); // (6),可以访问嵌套外层变量b,输出2
    console.log(c); // (7),可以访问嵌套外层变量b,输出2
  } // (8)
  var c = 3; // (9)
  return bar; // (10),返回给了非定义时的环境,这里是返回给了全局环境
} // (11)
var baz = foo(); // (12)
baz(); // (13)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这个例子中,bar 与 bar 所处的环境一起构成了一个 闭包 结构,在(10)将 bar 函数作为值返回给了外部(非定义时的环境)。其实 foo 已经执行完了,foo 内的作用域链也已经销毁了,但是 foo 内的变量对象还没有销毁,因为是 bar 的闭包阻挡了它销毁,bar 的闭包涵盖了 foo 函数的环境以及全局环境里 所有的变量对象 以及它们的指针也就是 作用域链。然后在(13)调用 bar 函数,bar 就可以通过闭包访问a b c三个变量。即使不在非定义时的环境里调用,都能通过闭包来访问被闭包涵盖到的变量。

# 3.1 闭包与变量

函数只能从它闭包中访问到变量的最后有效值

function createFunction() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = function () {
      return i;
    };
  }
  return result;
}
1
2
3
4
5
6
7
8
9

返回的 result 是个函数数组,数组里的每个函数都有自己的闭包,它们的闭包里都有指向i的指针,并且由于 i 使用var这种没有块级作用域的定义关键词来定义的,导致 i 是在 createFunction 里是同一个变量,那么所有的闭包都共享它,最后 createFunction 执行完,i 是 10,那么函数数组中每个函数最后通过闭包访问的 i 都是 10。

function createFunction() {
  var result = new Array();
  for (var i = 0; i < 10; i++) {
    result[i] = (function (num) {
      return function () {
        return num;
      };
    })(i);
  }
  return result;
}
1
2
3
4
5
6
7
8
9
10
11

上面例子使用了一个匿名函数存储每一次 for 循环的 i 值,函数数组里每一个函数的闭包优先访问外层匿名函数的 num 也就是每次循环的 i 值。其实在 es6 中引入 let 后直接将原先的例子中的var i改为let i,这个效果跟这个匿名函数包裹的效果一样。

# 3.2 闭包与 this 对象

函数作为一个值被 return 到定义时环境之外,这个函数不属于谁,它的 this 一般会绑定到 window 对象(非严格模式),那么在闭包中使用 this 的话就会得到 window 对象。如果想访问外层函数里的 this,可以将外层函数里的 this 使用 其他变量保存arguments 跟 this 一样,也需要特殊处理

var name = "The Window";
var obj = {
  name: "My Object",
  getNameFunc: function () {
    var that = this;
    return function () {
      return that.name; // 如果不使用that,此时这个匿名函数内的this就是指向window对象的
    };
  },
};
1
2
3
4
5
6
7
8
9
10

# 3.3 内存泄漏

闭包涵盖了 嵌套外层函数的环境全局环境 里所有的 变量对象 以及它们的指针也就是 作用域链。如果函数被作为值被 return 到定义时环境之外,使用完后需要尽快将函数的引用置为 null,否则会让闭包里的对象一直占用着内存。当然,如果直接将闭包里的对象置为 null 也可以减少内存的占用。

# 四、模仿块级作用域

js 没有块级作用域(es6 有了),但可以通过匿名函数来模仿块级作用域。

将行数声明包含在一对圆括号中表示它是一个函数表达式,再在其后添加另一对圆括号会立即调用这个函数。

function outputNumbers(count) {
  (function () {
    for (var i = 0; i < count; i++) {
      console.log(i);
    }
  })();
  console.log(i); // 导致一个错误,i定义在一个匿名函数内部,在函数外部就访问不了
}
1
2
3
4
5
6
7
8

# 五、私有变量

对象属性都是公有的,因为用 this.xxx 给实例绑定的,而函数内部可定义私有变量,不使用 this,直接在内部定义一个变量,外部就访问不到了。

可以在函数内部创建一个闭包,那么闭包可以访问那些是有变量,并且这个闭包还可以用 this 设置为公有方法供外部使用,这种有权访问私有变量和私有函数的公有方法称为特权方法。

function MyObject() {
  var privateVariable = 10; // 私有变量
  function privateFunction() {
    // 私有方法
    return false;
  }
  this.publicMethod = function () {
    // 特权方法
    privateVariable++;
    return privateVariable;
  };
}
1
2
3
4
5
6
7
8
9
10
11
12