本篇内容
  • 递归
  • 闭包及其this
  • 私有变量

函数声明此前已经说过(6-引用类型Function),关于函数声明,它的一个重要特征就是函数声明提升,是函数声明跟函数表达式的重要区别之一。

递归
递归函数是在一个函数通过名字调用自身的情况下构成的。此前说过,arguments.callee是一个指向正在执行的函数的指针,可以用它来实现对函数的递归调用并且避免了指向错误的问题。如

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

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

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

以上代码创建了一个名为f()的命名函数表达式,然后将它赋值给变量factorial。

闭包

闭包(closure)是指有权访问另一个函数作用域中变量的函数。
创建闭包的常见方式,就是在一个函数内部创建另一个函数。如

1
2
3
4
5
6
7
8
9
10
function createComparsionFunction (propertyName) {

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

即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量propertyName。
无论什么时候在函数中访问一个变量时,就会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域。但是闭包的情况又有所不同,在另一个函数内部定义的函数会将包含函数(外部函数)的活动对象添加到它的作用域链中。因此,在createComparsionFunction()函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数createComparsionFunction()的活动对象。更为重要的,createComparsionFunction()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparsionFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparsionFunction()的活动对象才会被销毁。如

1
2
3
4
5
var compareNames = createComparsionFunction('name');

console.log(compareNames({ name: 'Micheal' }, { name: 'Jack' }));

compareNames = null;

闭包的最大用处,一个是可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多。

1.闭包与变量

作用域链的这种配置引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function createFunctions () {
var result = [];

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

return result;
}

var result = createFunctions();
result[0](); // 10
result[1](); // 10
result[2](); // 10
result[3](); // 10
result[4](); // 10
result[5](); // 10
result[6](); // 10
result[7](); // 10
result[8](); // 10
result[9](); // 10

因为每个函数的作用域链中都保存着createFunction()函数的活动对象,所以它们引用的都是同一个变量i。当createFunction()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象。

我们可以通过一个匿名函数强制让闭包行为符合预期。如

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
function createFunctions () {
var result = [];

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

return result
}

var result = createFunctions();
result[0](); // 0
result[1](); // 1
result[2](); // 2
result[3](); // 3
result[4](); // 4
result[5](); // 5
result[6](); // 6
result[7](); // 7
result[8](); // 8
result[9](); // 9

2.关于this对象

匿名函数的执行环境具有全局性,因此其this对象通常指向window。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。如

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = 'the window';

var obj = {
name: 'the obj',

getNameFunc: function () {
return function () {
return this.name
}
}
};

obj.getNameFunc()(); // 'the window'

每个函数在被调用的时候都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中这两个变量。

可以把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var name = 'the window';

var obj = {
name: 'the obj',

getNameFunc: function () {
var THIS = this;
return function () {
return THIS.name
}
}
};

obj.getNameFunc()(); // "the obj"

特殊情况下,this会发生改变,如

1
2
3
4
5
6
7
8
9
10
11
12
13
var name = 'the window';

var obj = {
name: 'the obj',

getNameFunc: function () {
return this.name
}
};

obj.getName(); // 'the obj'
(obj.getName) (); // 'the obj'
(obj.getName = obj.getName) () // 'the window'

最后执行的this发生改变,因为这个赋值表达式的值是函数本身,所以this的值得不到维持。

模仿块级作用域

ES5中没有块级作用域的概念,这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。
匿名函数可以用来模仿块级作用域并避免这个问题。语法:

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

这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。通过创建私有作用域,每个开发人员既可以使用自己的变量,又不用担心搞乱全局作用域,还可以减少闭包占用的内存问题。

私有变量

严格来讲,JavaScript中没有私有成员的概念;所有属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。如

1
2
3
4
function add (num1, num2) {
var sum = num1 + num2;
return sum;
}

这个函数内部的变量num1、num2、和sum。函数内部可以访问这几个变量,但在函数外部则不能访问它们。如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。

我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。创建特权方法的方式:

  • 在构造函数中定义特权方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function MyObject() {
    var privateVariable = 10;

    function privateFunction () {
    return false;
    }

    // 特权方法
    this.publicMethod = function () {
    privateVariable++;
    return privateFunction();
    }
    }

利用私有和特权成员,可以隐藏那些不应该被直接修改的数据。如

1
2
3
4
5
6
7
8
9
function Person(name) {
this.getName = function () {
return name;
}

this.setName = function (value) {
name = value;
}
}

1.静态私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(function () {
var privateVariable = 10;

function privateFunction () {
return false;
}

MyObject = function () {};

MyObject.prototype.publicMethod = function () {
privateVariable++;
return privateFunction();
}
}) ();

MyObject在声明时没有使用var关键字,所以成了一个全局变量(严格模式下报错)。这个模式与在构造函数中定义特权方法的主要区别,在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。

多查找作用域链中的一个层次,就会在一定程度上影响查找速度。这是使用闭包和私有变量的一个显明的不足之处。

2.模块模式

为单例创建私有变量和特权方法。所谓单利,指的就是只有一个实例的对象。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var singleton = function () {
var privateVariable = 10;

function privateFunction () {
return false;
}

return {
publicProperty: true,

publicMethod: function () {
privateVariable++;
return privateFunction();
}
}
}

这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。

3.增强的模块模式

即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var singleton = function () {
var privateVariable = 10;

function privateFunction () {
retrun false;
}

var object = {};
object.publicProperty = true;
object.publicMethod = function () {
privateVariable++;
return privateFunction();
}

return object
}

温习:

  • 函数表达式的递归;
  • 闭包及其this;
  • 匿名函数模仿块级作用域;
  • 静态私有变量、模块模式;

(完)