zl程序教程

您现在的位置是:首页 >  前端

当前栏目

JavaScript的模块化:封装(闭包),继承(原型)介绍

JavaScript封装继承 介绍 原型 闭包 模块化
2023-06-13 09:15:03 时间

虽然JavaScript天生就是一副随随便便的样子,但是随着浏览器能够完成的事情越来越多,这门语言也也越来越经常地摆出正襟危坐的架势。在复杂的逻辑下,JavaScript需要被模块化,模块需要封装起来,只留下供外界调用的接口。闭包是JavaScript中实现模块封装的关键,也是很多初学者难以理解的要点。最初,我也陷入迷惑之中。现在,我自信对这个概念已经有了比较深入的理解。为了便于理解,文中试图封装一个比较简单的对象。

我们试图在页面上维护一个计数器对象ticker,这个对象维护一个数值n。随着用户的操作,我们可以增加一次计数(将数值n加上1),但不能减少n或直接改变n。而且,我们需要时不时查询这个数值。

门户大开的JSON风格模块化

一种门户大开的方式是:

复制代码代码如下:

varticker={
   n:0,
   tick:function(){
       this.n++;
   },
};

这种方式书写自然,而且确实有效,我们需要增加一次计数时,就调用ticker.tick()方法,需要查询次数时,就访问ticker.n变量。但是其缺点也是显而易见的:模块的使用者被允许自由地改变n,比如调用ticker.n--或者ticker.n=-1。我们并没有对ticker进行封装,n和tick()看上去是ticker的“成员”,但是它们的可访问性和ticker一样,都是全局性的(如果ticker是全局变量的话)。在封装性上,这种模块化的方式比下面这种更加可笑的方式,只好那么一点点(虽然对有些简单的应用来说,这一点点也足够了)。

复制代码代码如下:


varticker={};
vartickerN=0;
vartickerTick=function(){
   tickerN++;
}

tickerTick();

值得注意的是,在tick()中,我访问的是this.n——这并不是因为n是ticker的成员,而是因为调用tick()的是ticker。事实上这里写成ticker.n会更好,因为如果调用tick()的不是ticker,而是其他什么东西,比如:

复制代码代码如下:
varfunc=ticker.tick;
func();

这时,调用tick()的其实是window,而函数执行时会试图访问window.n而出错。

事实上,这种“门户大开”型的模块化方式,往往用来组织JSON风格的数据,而不是程序。比如,我们可以将下面这个JSON对象传给ticker的某个函数,来确定ticker从100开始计数,每次递进2。

复制代码代码如下:
varconfig={
   nStart:100,
   step:2
}

作用域链和闭包
来看下面的代码,注意我们已经实现了传入config对ticker进行自定义。

复制代码代码如下:
functionticker(config){
   varn=config.nStart;
   functiontick(){
       n+=config.step;
   }
}
console.log(ticker.n);//->undefined

你也许会疑惑,怎么ticker从对象变成了函数了?这是因为JavaScript中只有函数具有作用域,从函数体外无法访问函数内部的变量。ticker()外访问ticker.n获得undefined,而tick()内访问n却没有问题。从tick()到ticker()再到全局,这就是JavaScript中的“作用域链”。

可是还有问题,那就是——怎么调用tick()?ticker()的作用域将tick()也掩盖了起来。解决方法有两种:

•1)将需要调用方法作为返回值,正如我们将递增n的方法作为ticker()的返回值;
•2)设定外层作用域的变量,正如我们在ticker()中设置getN。

复制代码代码如下:
vargetN;
functionticker(config){
   varn=config.nStart;
   getN=function(){
       returnn;
   };
   returnfunction(){
       n+=config.step;
   };
}

vartick=ticker({nStart:100,step:2});
tick();
console.log(getN());//->102

请看,这时,变量n就处在“闭包”之中,在ticker()外部无法直接访问它,但是却可以通过两个方法来观察或操纵它。

在本节第一段代码中,ticker()方法执行之后,n和tick()就被销毁了,直到下一次调用该函数时再创建;但是在第二段代码中,ticker()执行之后,n不会被销毁,因为tick()和getN()可能访问它或改变它,浏览器会负责维持n。我对“闭包”的理解就是:用以保证n这种处在函数作用域内,函数执行结束后仍需维持,可能被通过其他方式访问的变量不被销毁的机制。

可是,我还是觉得不大对劲?如果我需要维持两个具有相同功能的对象ticker1和ticker2,那该怎么办?ticker()只有一个,总不能再写一遍吧?

new运算符与构造函数
如果通过new运算符调用一个函数,就会创建一个新的对象,并使用该对象调用这个函数。在我的理解中,下面的代码中t1和t2的构造过程是一样的。

复制代码代码如下:
functionmyClass(){}
vart1=newmyClass();
vart2={};
t2.func=myClass;
t2.func();
t2.func=undefined;

t1和t2都是新构造的对象,myClass()就是构造函数了。类似的,ticker()可以重新写成。

复制代码代码如下:
functionTICKER(config){
   varn=config.nStart;
   this.getN=function(){
       returnn;
   };
   this.tick=function(){
       n+=config.step;
   }
}

varticker1=newTICKER({nStart:100,step:2});
ticker1.tick();
console.log(ticker1.getN());//->102
varticker2=newTICKER({nStart:20,step:3});
ticker2.tick();
ticker2.tick();
console.log(ticker2.getN());//->26

习惯上,构造函数采用大写。注意,TICKER()仍然是个函数,而不是个纯粹的对象(之所以说“纯粹”,是因为函数实际上也是对象,TICKER()是函数对象),闭包依旧有效,我们无法访问ticker1.n。

原型prototype与继承
上面这个TICKER()还是有缺陷,那就是,ticker1.tick()和ticker2.tick()是互相独立的!请看,每使用new运算符调用TICKER(),就会生成一个新的对象并生成一个新的函数绑定在这个新的对象上,每构造一个新的对象,浏览器就要开辟一块空间,存储tick()本身和tick()中的变量,这不是我们所期望的。我们期望ticker1.tick和ticker2.tick指向同一个函数对象。

这就需要引入原型。

JavaScript中,除了Object对象,其他对象都有一个prototype属性,这个属性指向另一个对象。这“另一个对象”依旧有其原型对象,并形成原型链,最终指向Object对象。在某个对象上调用某方法时,如果发现这个对象没有指定的方法,那就在原型链上一次查找这个方法,直到Object对象。

函数也是对象,因此函数也有原型对象。当一个函数被声明出来时(也就是当函数对象被定义出来时),就会生成一个新的对象,这个对象的prototype属性指向Object对象,而且这个对象的constructor属性指向函数对象。

通过构造函数构造出的新对象,其原型指向构造函数的原型对象。所以我们可以在构造函数的原型对象上添加函数,这些函数就不是依赖于ticker1或ticker2,而是依赖于TICKER了。

你也许会这样做:

复制代码代码如下:
functionTICKER(config){
   varn=config.nStart;
}
TICKER.prototype.getN=function{
   //attention:invalidimplementation
   returnn;
};
TICKER.prototype.tick=function{
   //attention:invalidimplementation
   n+=config.step;
};

请注意,这是无效的实现。因为原型对象的方法不能访问闭包中的内容,也就是变量n。TICK()方法运行之后无法再访问到n,浏览器会将n销毁。为了访问闭包中的内容,对象必须有一些简洁的依赖于实例的方法,来访问闭包中的内容,然后在其prototype上定义复杂的公有方法来实现逻辑。实际上,例子中的tick()方法就已经足够简洁了,我们还是把它放回到TICKER中吧。下面实现一个复杂些的方法tickTimes(),它将允许调用者指定调用tick()的次数。

复制代码代码如下:
functionTICKER(config){
   varn=config.nStart;
   this.getN=function(){
       returnn;
   };
   this.tick=function(){
       n+=config.step;
   };
}
TICKER.prototype.tickTimes=function(n){
   while(n>0){
       this.tick();
       n--;
   }
};
varticker1=newTICKER({nStart:100,step:2});
ticker1.tick();
console.log(ticker1.getN());//->102
varticker2=newTICKER({nStart:20,step:3});
ticker2.tickTimes(2);
console.log(ticker2.getN());//->26

这个TICKER就很好了。它封装了n,从对象外部无法直接改变它,而复杂的函数tickTimes()被定义在原型上,这个函数通过调用实例的小函数来操作对象中的数据。

所以,为了维持对象的封装性,我的建议是,将对数据的操作解耦为尽可能小的单元函数,在构造函数中定义为依赖于实例的(很多地方也称之为“私有”的),而将复杂的逻辑实现在原型上(即“公有”的)。

最后再说一些关于继承的话。实际上,当我们在原型上定义函数时,我们就已经用到了继承!JavaScript中的继承比C++中的更……呃……简单,或者说简陋。在C++中,我们可能会定义一个animal类表示动物,然后再定义bird类继承animal类表示鸟类,但我想讨论的不是这样的继承(虽然这样的继承在JavaScript中也可以实现);我想讨论的继承在C++中将是,定义一个animal类,然后实例化了一个myAnimal对象。对,这在C++里就是实例化,但在JavaScript中是作为继承来对待的。

JavaScript并不支持类,浏览器只管当前有哪些对象,而不会额外费心思地去管,这些对象是什么class的,应该具有怎样的结构。在我们的例子中,TICKER()是个函数对象,我们可以对其赋值(TICKER=1),将其删掉(TICKER=undefined),但是正因为当前有ticker1和ticker2两个对象是通过new运算符调用它而来的,TICKER()就充当了构造函数的作用,而TICKER.prototype对象,也就充当了类的作用。

以上就是我所了解的JavaScript模块化的方法,如果您也是初学者,希望能对您有所帮助。如果有不对的地方,也劳驾您指出。

作者:一叶斋主人
出处:www.cnblogs.com/yiyezhai