上一篇对继承进行了大量说明,回到我们的问题上,剩下的几个问题我们接下来看看
- 在JavaScript中,有接口吗?
- 在JavaScript中,如何使用函数这种特殊的对象?
- 在JavaScript中,闭包是什么?
- 在JavaScript中,有哪些奇技淫巧是Java所没有的?
JavaScript接口
在经典的 Java 面向对象语言中,可以用关键字 interface 来定义接口,用 implement 来实现接口,而 JavaScript 虽然也是面向对象语言,但是它并没有内置这些,不过由于 JavaScript 的灵活性,我们可以通过模拟来实现,方法是使用一个辅助类和辅助函数来协助完成这一过程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 辅助类
var Interface = function(name,methods){
if(arguments.length != 2){
throw new Error("参数数量不对,期望传入两个参数,但是只传入了"+arguments.length+"个参数");
}
this.name = name;
this.methods = [];
for(var i = 0, len = methods.length; i < len; i++){
if(typeof methods[i] !== "string"){
throw new Error("期望传入的方法名是以字符串的格式类型,而不是"+ (typeof methods[i]) + "类型");
}
this.methods.push(methods[i]);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // 辅助函数
Interface.ensureImplements = function(object){
if(arguments.length < 2){
throw new Error("期望传入至少两个参数,这里仅传入"+arguments.length+"个参数");
}
for(var i = 1; i < arguments.length; i++){
var interface = arguments[i];
if(!(interface instanceof Interface)){
throw new Error(arguments[i] + "不是一个接口");
}
for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++){
var method = interface.methods[j];
if(!object[method] || typeof object[method] !== "function"){
throw new Error("对象的方法 "+method+" 与接口 "+interface.name+" 定义的不一致");
}
}
}
}
|
1
2
3
| //定义接口
var RobotMouth = new Interface('RobotMouth',['eat','speakChinese','speakEnglish']);
var RobotEar = new Interface('RobotEar',['listen']);
|
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
27
28
29
30
31
32
33
| // 实现RobotMouth、RobotEar接口
// 构造函数
var Robot = function(){
};
Robot.prototype = {
// 实现RobotMouth接口
eat: function(){
console.log("I can eat");
},
speakChinese: function(){
console.log("I can speak Chinese");
},
speakEnglish: function(){
console.log("I can speak English");
},
// 实现RobotEar接口
listen: function(){
console.log("I can listening");
}
};
var miniRobot = new Robot();
function useRobot(robot){
Interface.ensureImplements(miniRobot,RobotMouth,RobotEar);
robot.eat();
robot.listen();
}
useRobot(miniRobot);
|
下面对这段代码进行讲解:
定义接口
1
2
| var RobotMouth = new Interface('RobotMouth',['eat','speakChinese','speakEnglish']);
var RobotEar = new Interface('RobotEar',['listen']);
|
我们定义了两个接口,通过 new Interface()
来定义接口,在 Interface 这个函数中:
- 第一个参数是接口名称
- 第二个参数是一个数组,数组是元素是字符串的格式,里面分别是接口定义的方法
上述的代码,可理解为做下面的这样的定义:
1
2
3
4
5
6
7
8
9
| interface RobotMouth(){
function eat();
function speakChinese();
function speakEnglish();
}
interface RobotEar(){
function listen();
}
|
现在我们来看一下 Interface() 这个构造函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
| var Interface = function(name,methods){
if(arguments.length != 2){
throw new Error("参数数量不对,期望传入两个参数,但是只传入了"+arguments.length+"个参数");
}
this.name = name;
this.methods = [];
for(var i = 0, len = methods.length; i < len; i++){
if(typeof methods[i] !== "string"){
throw new Error("期望传入的方法名是以字符串的格式类型,而不是"+ (typeof methods[i]) + "类型");
}
this.methods.push(methods[i]);
}
}
|
这个构造函数实现的是对传入的参数进行严格的校验,如果参数不对就会报错。这里首先是判断参数的个数,第二是判断传入的方法名是否是字符串的格式。
接口的实现
接口定义好后,通过一个类来实现,这里要注意的是,需要给出明确的注释,说明这个类实现的是哪个接口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| var Robot = function(){
};
Robot.prototype = {
// 实现RobotMouth接口
eat: function(){
console.log("I can eat");
},
speakChinese: function(){
console.log("I can speak Chinese");
},
speakEnglish: function(){
console.log("I can speak English");
},
// 实现RobotEar接口
listen: function(){
console.log("I can listening");
}
};
|
这里我们定义了一个Robot构造函数来实现以上两个接口,方法写在原型上,注意注释明确。
使用接口
1
2
3
4
5
6
7
8
9
| var miniRobot = new Robot();
function useRobot(robot){
Interface.ensureImplements(miniRobot,RobotMouth,RobotEar);
robot.eat();
robot.listen();
}
useRobot(miniRobot);
|
首先我们创建了一个实例叫 miniRobot ,然后做为参数传入 useRobot() 这个函数进行生产调用,在这个函数里的第一行代码 Interface.ensureImplements(miniRobot,RobotMouth,RobotEar);
是对传入的 miniRobot 进行严格的校验,校验不通过会抛出异常。它接受的参数必须大于等于两个:
- 第一个参数是一个实现了接口的对象
- 第二个参数是对象的一个接口
- 第三个参数同上(若存在的话)
我们来看一下这段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| Interface.ensureImplements = function(object){
if(arguments.length < 2){
throw new Error("期望传入至少两个参数,这里仅传入"+arguments.length+"个参数");
}
for(var i = 1; i < arguments.length; i++){
var interface = arguments[i];
if(!(interface instanceof Interface)){
throw new Error(arguments[i] + "不是一个接口");
}
for(var j = 0, methodsLen = interface.methods.length; j < methodsLen; j++){
var method = interface.methods[j];
if(!object[method] || typeof object[method] !== "function"){
throw new Error("对象的方法 "+method+" 与接口 "+interface.name+" 定义的不一致");
}
}
}
}
|
首先对传入的参数进行校验,必须传入两个或以上的参数。然后检查每个传入的接口是否已经定义,如果未定义,就抛出异常,表示不是一个接口。然后对每个接口定义的方法进行遍历,与第一个参数(实现接口的对象)进行比较,如果对象没有实现接口的方法,那么这段代码 throw new Error("对象的方法 "+method+" 与接口 "+interface.name+" 定义的不一致");
就会抛出异常。
闭包
闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
变量的作用域
Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。
1
2
3
4
5
| var n = 999;
function f1(){
alert(n);
}
f1();
|
另一方面,在函数外部自然无法读取函数内的局部变量。
1
2
3
4
| function f1(){
var n = 999;
}
alert(n); //error
|
函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!
1
2
3
4
5
| function f1(){
n = 999;
}
f1();
alert(n);
|
如何从外部读取局部变量
出于种种原因,我们有时候需要得到函数内的局部变量。但是,前面已经说过了,正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数。
1
2
3
4
5
6
| function f1(){
var n = 999;
function f2(){
alert(n); //999
}
}
|
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是Javascript语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
1
2
3
4
5
6
7
8
9
10
| function f1(){
var n = 999;
function f2(){
alert(n);
}
return f2;
}
var result = f1();
result();
|
闭包的概念
由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
换一个思路去理解为什么叫闭包, 上面代码被认为函数 f2()
在函数 f1()
的作用域上有一个访问封闭,也可以说有一个闭包。f2()
相对于在 f1()
中是封闭的的一个作用域空间,下面例子可以帮助你理解:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| function makeAdder(x){
function add(y){
return x + y;
};
return add;
}
var plusOne = makeAdder(1);
var plusTen = makeAdder(10);
pushOne.add(3); // 1 + 3 = 4
pushOne.add(41); // 1 + 41 = 42
pushTen.add(3); // 10 + 3 = 13
pushTen.add(41); // 10 + 41 = 51
|
你会发现 makeAdder()
函数对于 add()
函数来说就是一个闭包,里面的环境和参数是封闭的。
闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
怎么来理解这句话呢?请看下面的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
| function f1(){
var n = 999;
nAdd = function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result = f1();
result();
nAdd();
result();
|
在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
闭包的注意点
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。