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


Posted in Javascript onJuly 22, 2013

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

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

门户大开的 JSON 风格模块化

一种门户大开的方式是:

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

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

var ticker = {};
var tickerN = 0;
var tickerTick = function(){
    tickerN++;
}
tickerTick();

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

var func = ticker.tick;
func();

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

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

var config = {
    nStart:100,
    step:2
}

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

function ticker(config){
    var n = config.nStart;
    function tick(){
        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 。

var getN;
function ticker(config){
    var n = config.nStart;
    getN = function(){
        return n;
    };
    return function(){
        n += config.step;
    };
}
var tick = 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 的构造过程是一样的。

function myClass(){}
var t1 = new myClass();
var t2 = {};
t2.func = myClass;
t2.func();
t2.func = undefined;

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

function TICKER(config){
    var n = config.nStart;
    this.getN = function(){
        return n;
    };
    this.tick = function(){
        n += config.step;
    }
}
var ticker1 = new TICKER({nStart:100,step:2});
ticker1.tick();
console.log(ticker1.getN()); // ->102
var ticker2 = new TICKER({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 了。

你也许会这样做:

function TICKER(config){
    var n = config.nStart;
}
TICKER.prototype.getN = function{
    // attention : invalid implementation
    return n;
};
TICKER.prototype.tick = function{
    // attention : invalid implementation
    n += config.step;
};

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

function TICKER(config){
    var n = config.nStart;
    this.getN = function(){
        return n;
    };
    this.tick = function(){
        n += config.step;
    };
}
TICKER.prototype.tickTimes = function(n){
    while(n>0){
        this.tick();
        n--;
    }
};
var ticker1 = new TICKER({nStart:100,step:2});
ticker1.tick();
console.log(ticker1.getN()); // ->102
var ticker2 = new TICKER({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

Javascript 相关文章推荐
JQuery通过Ajax提交表单并返回结果
Jul 31 Javascript
JS链式调用的实现方法
Mar 07 Javascript
jQuery实现dialog设置focus焦点的方法
Jun 10 Javascript
详解JavaScript基于面向对象之创建对象(2)
Dec 10 Javascript
Node.js+Express配置入门教程
May 19 Javascript
JQuery手速测试小游戏实现思路详解
Sep 20 Javascript
Javascript this 函数深入详解
Dec 13 Javascript
基于js实现二级下拉联动
Dec 17 Javascript
微信小程序methods中定义的方法互相调用的实例代码
Aug 07 Javascript
详解微信小程序的不同函数调用的几种方法
May 08 Javascript
JS 实现发送短信验证码的“59秒后重新发送验证短信”功能
Aug 23 Javascript
JavaScript实现捕获鼠标坐标
Apr 12 Javascript
JS判定是否原生方法
Jul 22 #Javascript
js图片延迟加载的实现方法及思路
Jul 22 #Javascript
js添加table的行和列 具体实现方法
Jul 22 #Javascript
JS中eval函数的使用示例
Jul 21 #Javascript
JS中prototype关键字的功能介绍及使用示例
Jul 21 #Javascript
原生JS实现表单checkbook获取已选择的值
Jul 21 #Javascript
jquery animate实现鼠标放上去显示离开隐藏效果
Jul 21 #Javascript
You might like
Apache2 httpd.conf 中文版
2006/11/17 PHP
PHP中获取内网用户MAC地址(WINDOWS/linux)的实现代码
2011/08/11 PHP
php定时计划任务的实现方法详解
2013/06/06 PHP
PHP 解决session死锁的方法
2013/06/20 PHP
PHP网页游戏学习之Xnova(ogame)源码解读(三)
2014/06/23 PHP
php使用正则表达式去掉html中的注释方法
2016/11/03 PHP
ExtJS 2.0实用简明教程 之Ext类库简介
2009/04/29 Javascript
JavaScript 字符编码规则
2009/05/04 Javascript
JQuery EasyUI 加载两次url的原因分析及解决方案
2014/08/18 Javascript
JavaScript变量声明详解
2014/11/27 Javascript
JS实现超炫网页烟花动画效果的方法
2015/03/02 Javascript
一个简单的JavaScript Map实例(分享)
2016/08/03 Javascript
jquery做个日期选择适用于手机端示例
2017/01/10 Javascript
angular实现表单验证及提交功能
2017/02/01 Javascript
微信小程序实现换肤功能
2018/03/14 Javascript
Node.js应用设置安全的沙箱环境
2018/04/23 Javascript
Vue中的$set的使用实例代码
2018/10/08 Javascript
Vue中实现权限控制的方法示例
2019/06/07 Javascript
express框架下使用session的方法
2019/07/31 Javascript
如何利用JS将手机号中间四位变成*号
2020/09/29 Javascript
使用Python进行稳定可靠的文件操作详解
2013/12/31 Python
python 处理dataframe中的时间字段方法
2018/04/10 Python
Python登录注册验证功能实现
2018/06/18 Python
详解python中init方法和随机数方法
2019/03/13 Python
Python函数递归调用实现原理实例解析
2020/08/11 Python
python爬虫scrapy框架之增量式爬虫的示例代码
2021/02/26 Python
毕业生自我鉴定
2013/11/05 职场文书
护理学应聘自荐书范文
2014/02/05 职场文书
2014年医院科室工作总结
2014/12/20 职场文书
2015年节能减排工作总结
2015/05/14 职场文书
2015年教务主任工作总结
2015/07/22 职场文书
2016预备党员培训心得体会
2016/01/08 职场文书
执行力心得体会范文
2016/01/11 职场文书
2016年党员创先争优承诺书
2016/03/25 职场文书
【海涛七七解说】DCG第二周:DK VS 天禄
2022/04/01 DOTA
springcloud整合seata
2022/05/20 Java/Android