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 相关文章推荐
javascript multibox 全选
Mar 22 Javascript
JQuery扩展插件Validate 5添加自定义验证方法
Sep 05 Javascript
jQuery中dequeue()方法用法实例
Dec 29 Javascript
常用的js验证和数据处理总结
Aug 02 Javascript
Node.js的环境安装配置(使用nvm方式)
Oct 11 Javascript
微信小程序 实战程序简易新闻的制作
Jan 09 Javascript
javascript中的面向对象
Mar 30 Javascript
AngularJS1.X学习笔记2-数据绑定详解
Apr 01 Javascript
javascript将list转换成树状结构的实例
Sep 08 Javascript
基于vue.js组件实现分页效果
Dec 29 Javascript
JavaScript实现左右滚动电影画布
Feb 06 Javascript
js实现弹幕墙效果
Dec 10 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
一致性哈希算法以及其PHP实现详细解析
2013/08/24 PHP
浅谈php错误提示及查错方法
2015/07/14 PHP
PHP合并discuz用户脚本的方法
2015/08/04 PHP
WordPress中用于创建以及获取侧边栏的PHP函数讲解
2015/12/29 PHP
PHP读取word文档的方法分析【基于COM组件】
2017/08/01 PHP
ThinkPHP3.2.3框架邮件发送功能图文实例详解
2019/04/23 PHP
javascript下过滤数组重复值的代码
2007/09/10 Javascript
js 目录列举函数
2008/11/06 Javascript
判断控件是否已加载完成的代码
2010/02/24 Javascript
jQuery getJSON 处理json数据的代码
2010/07/26 Javascript
passwordStrength 基于jquery的密码强度检测代码使用介绍
2011/10/08 Javascript
thinkphp中常用的系统常量和系统变量
2014/03/05 Javascript
JavaScript结合AJAX_stream实现流式显示
2015/01/08 Javascript
javascript简单实现类似QQ头像弹出效果的方法
2015/08/03 Javascript
jQuery实现折叠、展开的菜单组效果代码
2015/09/16 Javascript
domReady的实现案例
2016/11/23 Javascript
js实现横向拖拽导航条功能
2017/02/17 Javascript
Python迭代器和生成器介绍
2015/03/06 Python
Windows 7下Python Web环境搭建图文教程
2018/03/20 Python
Linux下python3.7.0安装教程
2018/07/30 Python
wxPython:python首选的GUI库实例分享
2019/10/05 Python
python 模拟登陆163邮箱
2020/12/15 Python
pycharm配置QtDesigner的超详细方法
2021/01/25 Python
详解background属性的8个属性值(面试题)
2020/11/02 HTML / CSS
ORACLE十问
2015/04/20 面试题
咖啡店的创业计划书,让你hold不住
2014/01/03 职场文书
家长对小学生的评语
2014/01/28 职场文书
2014机关党员干部“正风肃纪”思想汇报
2014/09/15 职场文书
推荐信范文大全
2015/03/27 职场文书
大客户经理岗位职责
2015/04/09 职场文书
农民工工资保障承诺书
2015/05/04 职场文书
导游词之昭君岛
2020/01/17 职场文书
Python爬虫数据的分类及json数据使用小结
2021/03/29 Python
vue点击弹窗自动触发点击事件的解决办法(模拟场景)
2021/05/25 Vue.js
Java 数组的使用
2022/05/11 Java/Android
MySQL数据库之存储过程 procedure
2022/06/16 MySQL