0%

JavaScript 之闭包

在 JavaScript 中,每当一个函数被创建,该函数的闭包(Closure)就会被悄悄的创建,闭包给了我们在函数外访问函数内部变量的能力。闭包由两部分组成,函数本身和该函数对其词法环境的引用,在这个词法环境中有该函数可以访问到的变量。这是闭包的概述,第一次看的人很容易抓不住头脑,本文通过一个例子帮助理解闭包,之后再回看定义就可能悟道了。

例子

下面看一个构造函数 Circle 的例子,该构造函数会创建一个有 radiusdraw 成员的对象,其中 draw 成员引用一个函数,该函数试图访问构造函数 Circle 的局部变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Circle(radius) {
this.radius = radius;

let defaultLocation = { x: 0, y: 0 };

let computeOptimumLocation = function(factor) {
console.log("computed");
};

this.draw = function() {
computeOptimumLocation(0.1);
console.log(this.radius, defaultLocation);
};
}

const circle = new Circle(1);
circle.draw();

在内存中创建一个 circle 对象后,调用它的 draw 方法,为什么 draw 方法中的 computeOptimumLocation() 可以被成功调用呢,它难道不是构造函数 Circle 的内的局部变量吗,现在我们不是在 Circle 的作用域外吗?

因为 JavaScript 中闭包的特性,这样的事情可以实现。

circle 对象被创建后,其属性 draw 引用了一个匿名函数,这个函数的闭包中的词法环境有 computeOptimumLocationdefaultLocation 这两个对象,所以尽管我们离开了这两个函数内部对象被定义时作用域(即 Circle 函数体),但由于闭包的存在,这两个变量会被保存在内存中,并且维持状态,可以被修改。

私有成员

其实上面的 Circle 构造函数是在利用闭包实现私有成员,JavaScript 中没有提供类似 Java 中 private 这样的原生实现私有成员的方法,但我们可以利用闭包来模拟。

在函数中声明函数本地变量,这样他的作用域只在函数体中,当使用关键字 new 调用这个函数后,这些本地变量自然不会成为被创建对象的成员(所以我们只是在模拟私有成员,因为这个本地变量实际上并不属于被创建的对象)。

在 JavaScript 中,由于闭包的存在,如果我们在一个函数中定义了一个函数,内部的这个函数是可以访问到它父函数中定义的变量的,举一个例子,我们可以用这个机制来实现 getter

1
2
3
4
5
6
7
function Circle() {
let defaultLocation = { x: 0, y: 0 };

this.getDefaultLocation = function() {
return defaultLocation; // closure comes in
};
}

这个构造函数中 getDefaultLocation 是被创建对象的成员,它可以借助闭包访问到其父函数的内部成员 defaultLocation

Object.defineProperty()

其实我们还可以做的更好!不用通过 getXXX() 来获取“私有”成员的值,我们可以直接通过“私有”成员的名字来获取它的值,就像它真的可以被访问一样!

Object.defineProperty() 可以定义一个对象成员,以及他的元信息(metadata):

1
2
3
4
5
Object.defineProperty(
objToAddProperty, // use this if inside constructor
nameOfAddedProperty,
attributeOfAddedProperty // such as get and set
);

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Circle() {
let defaultLocation = { x: 0, y: 0 };

// this.getDefaultLocation = function() {
// return defaultLocation;
// };

Object.defineProperty(this, "defaultLocation", {
get: function() {
return defaultLocation; // closure
}
});
}

const circle = new Circle();
console.log(circle.defaultLocation);

类似 getter,我们也可以为属性定义 setter,例子如下:

1
2
3
4
5
6
7
8
9
Object.defineProperty(this, "defaultLocation", {
get: function() {
return defaultLocation; // closure
},
set: function(value) {
// do some value validation here
defaultLocation = value;
}
});
奥里给,老铁们,干了!