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 相关文章推荐
利用js获取服务器时间的两个简单方法
Jan 08 Javascript
clientX,pageX,offsetX,x,layerX,screenX,offsetLeft区别分析
Mar 12 Javascript
使用JS进行目录上传(相当于批量上传)
Dec 05 Javascript
LABjs、RequireJS、SeaJS的区别
Mar 04 Javascript
AngularJS基础学习笔记之指令
May 10 Javascript
无阻塞加载js,防止因js加载不了影响页面显示的问题
Dec 18 Javascript
jquery实现手机端单店铺购物车结算删除功能
Feb 22 Javascript
Vue.js 中的 $watch使用方法
May 25 Javascript
React-Router如何进行页面权限管理的方法
Dec 06 Javascript
JS随机数产生代码分享
Feb 24 Javascript
vue2.0项目集成Cesium的实现方法
Jul 30 Javascript
详解react组件通讯方式(多种)
May 06 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
11个PHP 分页脚本推荐
2011/08/15 PHP
PHP使用Alexa API获取网站的Alexa排名例子
2014/06/12 PHP
PHP函数import_request_variables()用法分析
2016/04/02 PHP
PHP中单例模式与工厂模式详解
2017/02/17 PHP
解决Laravel无法使用COOKIE和SESSION的问题
2019/10/16 PHP
符合标准的js表单提交的代码
2007/09/13 Javascript
JavaScript中也使用$美元符号来代替document.getElementById
2010/06/19 Javascript
PHP 与 js的通信(via ajax,json)
2010/11/16 Javascript
jQuery实用函数用法总结
2014/08/29 Javascript
javascript学习笔记(三)BOM和DOM详解
2014/09/30 Javascript
轻量级javascript 框架Backbone使用指南
2015/07/24 Javascript
js实现仿京东2级菜单效果(带延时功能)
2015/08/27 Javascript
jquery实现图片预加载
2015/12/25 Javascript
Vue.js动态添加、删除选题的实例代码
2016/09/30 Javascript
微信小程序 后台https域名绑定和免费的https证书申请详解
2016/11/10 Javascript
bootstrap按钮插件(Button)使用方法解析
2017/01/13 Javascript
JavaScript中在光标处插入添加文本标签节点的详细方法
2017/03/22 Javascript
Angular 2父子组件数据传递之@Input和@Output详解 (上)
2017/07/05 Javascript
微信小程序wx:for循环的实例详解
2018/10/07 Javascript
详解基于iview-ui的导航栏路径(面包屑)配置
2019/02/22 Javascript
小程序实现订单倒计时功能
2019/04/23 Javascript
微信小程序向Java后台传输参数的方法实现
2020/12/10 Javascript
Vue实现图书管理案例
2021/01/20 Vue.js
python自然语言编码转换模块codecs介绍
2015/04/08 Python
Python通过RabbitMQ服务器实现交换机功能的实例教程
2016/06/29 Python
python flask中静态文件的管理方法
2018/03/20 Python
Python中类的创建和实例化操作示例
2019/02/27 Python
python GUI库图形界面开发之PyQt5窗口控件QWidget详细使用方法
2020/02/26 Python
python 轮询执行某函数的2种方式
2020/05/03 Python
Python读取xlsx数据生成图标代码实例
2020/08/12 Python
美国第二大团购网站:LivingSocial
2016/07/24 全球购物
希尔顿酒店中国网站:Hilton中国
2017/03/11 全球购物
大学毕业的自我鉴定
2013/10/08 职场文书
口腔医学技术应届生求职信
2013/11/09 职场文书
医学实习生自我鉴定
2013/12/12 职场文书
个人工作总结怎么写?
2019/04/09 职场文书