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

在复杂的逻辑下, JavaScript 需要被模块化,模块需要封装起来,只留下供外界调用的接口。闭包是 JavaScript 中实现模块封装的关键,也是很多初学者难以理解的要点
 

虽然 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

时间: 2024-09-20 23:33:17

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

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

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

简单理解JavaScript中的封装与继承特性_基础知识

JavaScript中的封装封装简单地说就是让外界只能访问对象的共有变量和函数,隐藏细节和数据. js中有三种方法创建对象,分别为门户大开型.用命名规范区分私有变量.闭包创建真正的私有变量三种. 1.门户大开型,是实现对象的最基础的方法,所有方法与变量都是共有的外界可以访问. var Book = function(name){ if(this.check(name)){ console.log("error"); throw new Error("name null&quo

javascript的几种继承方法介绍_基础知识

1.原型链继承:构造函数.原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针.确认原型和实例之间的关系用instanceof. 原型链继承缺点:字面量重写原型会中断关系,使用引用类型的原型,并且子类型还无法给超类型传递参数 function Parent(){ this.name='mike'; } function Child(){ this.age=12; } //儿子继承父亲(原型链) Child.prototype

javascript 面向对象封装与继承_javascript技巧

整理一下js面向对象中的封装和继承. 1.封装 js中封装有很多种实现方式,这里列出常用的几种. 1.1 原始模式生成对象 直接将我们的成员写入对象中,用函数返回. 缺点:很难看出是一个模式出来的实例. 代码: 复制代码 代码如下:        function Stu(name, score) {             return {                 name: name,                 score: score             }       

【javascript基础】7、继承

原文:[javascript基础]7.继承 前言 由于本人水平有限,所以有些高手觉得现在写的内容偏容易,要一点点来嘛,今天和大家学习或者复习一下javascript的继承.我也就是尽量写吧······ 继承 javascript的继承其实主要就是通过原型链来实现的,原型链我们之前已经和大家一起学习过,这里就不浪费大家的时间了.javascript连类都没有,还说啥继承呢,这还是模拟类的继承.<javascript高级程序设计>上分成了几个方式,有的书上分为类式继承,原型式继承,这就是模拟其他语

Javascript中的几种继承方式对比分析_基础知识

开篇从'严格'意义上说,javascript并不是一门真正的面向对象语言.这种说法原因一般都是觉得javascript作为一门弱类型语言与类似java或c#之类的强型语言的继承方式有很大的区别,因而默认它就是非主流的面向对象方式,甚至竟有很多书将其描述为'非完全面向对象'语言.其实个人觉得,什么方式并不重要,重要的是是否具有面向对象的思想,说javascript不是面向对象语言的,往往都可能没有深入研究过javascript的继承方式,故特撰此文以供交流. 为何需要利用javascript实现继

了解Javascript的模块化开发

 这篇文章主要介绍了了解Javascript的模块化开发,本文讲解了为什么需要模块化开发,模块化开发的形成原因等内容,需要的朋友可以参考下     小A是某个创业团队的前端工程师,负责编写项目的Javascript程序. 全局变量冲突 根据自己的经验,小A先把一些常用的功能抽出来,写成函数放到一个公用文件base.js中: 代码如下: var _ = { $: function(id) { return document.getElementById(id); }, getCookie: fun

几句话带你理解JS中的this、闭包、原型链_javascript技巧

原型链 所有对象都是基于Object.prototype,Object.prototype就是JavaScript的根对象,在Object.prototype中定义的方法都可以被其它对象访问到,当然也可以被重写了,所以直接在Object.prototype上调用的是原始功能的toString()方法,该方法会放回参数对象的内置属性[[class]]的值,这个值是个字符串,比如'[Object String]' 要理解原型链机制的话,首先得知道根本原因:JavaScript中的对象都有一个内置属性

javascript学习笔记(五)原型和原型链详解_基础知识

私有变量和函数 在函数内部定义的变量和函数,如果不对外提供接口,外部是无法访问到的,也就是该函数的私有的变量和函数. 复制代码 代码如下: <script type="text/javascript">     function Test(){         var color = "blue";//私有变量         var fn = function() //私有函数         {         }     } </script