JavaScript 是一种非常灵活的面向对象程序设计语言,它与传统的强类型的面向对象程序设计语言(如 C++,Java,C# 等)有很大不同,所以要实现如 C++、java、C# 当中的一些特性就需要换一种思考方式来解决。今天主要讨论如何在 JavaScript 脚本中实现数据的封装(encapsulation)。
数据封装说的简单点就是把不希望调用者看见的内容隐藏起来。它是面向对象程序设计的三要素之首,其它两个是继承和多态,关于它们的内容在后面再讨论。
关于数据封装的实现,在 C++、Java、C# 等语言中是通过 public、private、static 等关键字实现的。在 JavaScript 则采用了另外一种截然不同的形式。在讨论如何具体实现某种方式的数据封装前,我们先说几个简单的,大家所熟知却又容易忽略的 JavaScript 的概念。
1 几个基本概念
1.1 变量定义
在 JavaScript 语言中,是通过 var 关键字来定义变量的。
但是如果我们直接给一个没有使用 var 定义的变量赋值,那么这个变量就会成为全局变量。
一般情况下,我们应该避免使用没有用 var 定义的变量,主要原因是它会影响程序的执行效率,因为存取全局变量速度比局部变量要慢得多。
但是这种用法可以保证我们的变量一定是全局变量。
另外,为了保证速度,我们在使用全局变量时,可以通过 var 定义一个局部变量,然后将全局变量赋予之,由此可以得到一个全局变量的局部引用。
1.2 变量类型
没有定义的变量,类型为 undefined。
变量的值可以是函数。
函数在 JavaScript 中可以充当类的角色。
1.3 变量作用域
变量作用域是指变量生存周期的有效范围。
单纯用 { } 创建的块不能创建作用域。
with 将它包含的对象作用域添加到当前作用域链中,但 with 不创建新的作用域。with 块结束后,会将对象作用域从当前作用域链中删除。
try-catch 中,catch 的错误对象只在 catch 块中有效,但 catch 块中定义的变量属于当前作用域。
其它如 if、for、for-in、while、do-while、switch 等控制语句创建的块不能创建作用域。
用 function 创建的函数,会创建一个新的作用域添加到当前作用域中。
2 封装
下面我们就来讨论具体的封装。首先说一下大家最熟悉的几种封装:私有实例成员、公有实例成员和公有静态成员。最后会讨论一下大家所不熟悉的私有静态成员和静态类的封装办法。因为下面要讨论的是面向对象编程,所有当函数作为类来定义和使用时,我们暂且将其成为类。
2.1 私有实例成员
私有实例成员在 JavaScript 中实际上可以用函数内的局部变量来实现,它相当于类的私有实例成员。例如:
- class1 = function () {
- // private fields
- var m_first = 1 ;
- var m_second = 2 ;
- // private methods
- function method1 () {
- alert ( m_first ) ;
- }
- var method2 = function () {
- alert ( m_second ) ;
- }
- // constructor
- {
- method1 () ;
- method2 () ;
- }
- }
- var o = new class1 () ;
- // error
- alert ( o . m_first ) ;
- o . method1 () ;
这里 m_first 和 m_second 是 class1 的两个私有实例字段,method1 和 method2 是两个私有实例方法。他们只能在该类的对象内部被使用,在对象外无法使用。
这里大家会发现创建私有方法有两种方式,一种是直接在类中定义方法,另一种是先定义一个局部变量(私有实例字段),然后定义一个匿名方法赋值给它。
直接在类中定义方法,则该方法的作用域就是这个类,因此这个方法在此类外不能够被访问,而它又可以存取类中所有的私有实例字段,这就保证了这是个私有实例方法。
第二种创建私有实例方法的方式跟第一种方式的效果是一样的,但是第二种方式更灵活一些。
你应该还会注意到,class1 中把构造器代码用 { } 括起来了,这样做虽然没有必要,但是代码看上去更加清晰。
关于这段构造器代码,还有两点需要说明的地方:
1、构造器代码必须放在整个类定义的最后,这样做是为了保证在它当中被调用的方法都已经被定义了。因为 JavaScript 是解释型语言,所以,它会按照从上到下的顺序执行,因此,如果构造器代码放在其它方法定义的前面,则执行到调用语句时找不到要调用的方法,就会出错。
2、我们已经知道 { } 创建的块不会改变作用域,因此如果在这样的构造器代码中创建局部变量,实际上是在整个类中创建私有实例成员,所以,如果需要用到局部变量,应当定义一个私 有实例方法,例如可以命名为 constructor(),在 constructor() 这个私有实例方法中定义局部变量和原来 { } 构造器中要执行的代码,然后在类的最后直接调用它就可以了。所以更好的写法是这样的:
- class1 = function () {
- // private fields
- var m_first = 1 ;
- var m_second = 2 ;
- // private methods
- function constructor () {
- method1 () ;
- method2 () ;
- }
- function method1 () {
- alert ( m_first ) ;
- }
- var method2 = function () {
- alert ( m_second ) ;
- }
- constructor () ;
- }
- var o = new class1 () ;
- // error
- alert ( o . m_first ) ;
- o . method1 () ;
最后,你可能还会发现 class1 的定义我们没有用 var,这样做我们就可以保证它是个全局的类了。
2.2 公有实例成员
公有实例成员可以通过两种方式来创建,我们先来看下面这个例子:
- class2 = function () {
- // private fields
- var m_first = 1 ;
- var m_second = 2 ;
- // private methods
- function method1 () {
- alert ( m_first ) ;
- }
- var method2 = function () {
- alert ( m_second ) ;
- }
- // public fields
- this . first = " first " ;
- this . second = [ ' s ' , ' e ' , ' c ' , ' o ' , ' n ' , ' d ' ] ;
- // public methods
- this . method1 = method2 ;
- this . method2 = function () {
- alert ( this . second ) ;
- }
- // constructor
- {
- method1 () ;
- method2 () ;
- }
- }
- // public method
- class1 . prototype . method3 = function () {
- alert ( this . first ) ;
- }
- var o = new class2 () ;
- o . method1 () ;
- o . method2 () ;
- o . method3 () ;
- alert ( o . first ) ;
我们发现这个例子是在 class1 的例子上做了一些补充。给它添加了公有实例字段和公有实例方法,我们把它们通称为公有实例成员。
我们应该已经发现,创建公有实例成员其实很简单,一种方式是通过在类中给 this.memberName 来赋值,如果值是函数之外的类型,那就是个公有实例字段,如果值是函数类型,那就是公有实例方法。另外一种方式则是通过给className .prototype.memberName 赋值,可赋值的类型跟 this.memberName 是相同的。
到底是通过 this 方式定义好呢,还是通过 prototype 方式定义好呢?
其实它们各有各的用途,它们之间不是谁比谁更好的关系。在某些情况下,我们只能用其中特定的一种方式来定义公有实例成员,而不能够使用另一种方式。原因在于它们实际上是有区别的:
1、prototype 方式只应该在类外定义。this 方式只能在类中定义。
2、prototype 方式如果在类中定义时,则存取私有实例成员时,总是存取最后一个对象实例中的私有实例成员。
3、prototype 方式定义的公有实例成员是创建在类的原型之上的成员。this 方式定义的公有实例成员,是直接创建在类的实例对象上的成员。
基于前两点区别,我们可以得到这样的结论:如果要在公有实例方法中存取私有实例成员,那么必须用 this 方式定义。
关于第三点区别,我们后面在讨论继承时再对它进行更深入的剖析。这里只要知道有这个区别就可以了。
我们还会发现,公有实例成员和私有实例成员名字是可以相同的,这样不会有冲突吗?
当然不会。原因在于它们的存取方式不同,公有实例成员在类中存取时,必须要用 this. 前缀来引用。而私有实例成员在类中存取时,不使用也不能够使用 this. 前缀来存取。而在类外存取时,只有公有成员是可以通过类的实例对象存取的,私有成员无法存取。
2.3 公有静态成员
公有静态成员的定义很简单,例如:
- class3 = function () {
- // private fields
- var m_first = 1 ;
- var m_second = 2 ;
- // private methods
- function method1 () {
- alert ( m_first ) ;
- }
- var method2 = function () {
- alert ( m_second ) ;
- }
- // constructor
- {
- method1 () ;
- method2 () ;
- }
- }
- // public static field
- class3 . field1 = 1 ;
- // public static method
- class3 . method1 = function () {
- alert ( class3 . field1 ) ;
- }
- class3 . method1 () ;
这个例子的 class3 跟 class1 很像。不同的是 class3 的外面,我们又给 class3 定义了一个静态字段和静态方法。
定义的方式就是给 className .memberName 直接赋值。
这里定义的静态字段和静态方法都是可以被直接通过类名引用来存取的,而不需要创建对象。因此它们是公有静态成员。
不过有点要记住,一定不要将公有静态成员定义在它所在的类的内部,否则你会得到非你所期望的结果。我们可以看下面这个例子:
- class4 = function () {
- // private fields
- var m_first = 1 ;
- var m_second = 2 ;
- var s_second = 2 ;
- // private methods
- function method1 () {
- alert ( m_first ) ;
- }
- var method2 = function () {
- alert ( m_second ) ;
- }
- class4 . method1 = function () {
- s_second ++;
- }
- class4 . method2 = function () {
- alert ( s_second ) ;
- }
- }
- var o1 = new class4 () ;
- class4 . method2 () ; // 2
- class4 . method1 () ;
- class4 . method2 () ; // 3
- var o2 = new class4 () ;
- class4 . method2 () ; // 2
- class4 . method1 () ;
- class4 . method2 () ; // 3
这个例子中,我们期望 s_second 能够扮演一个私有静态成员的角色,但是输出结果却不是我们所期望的。我们会发现 s_second 实际上是 class4 的一个私有实例成员,而不是私有静态成员。而 class4 的 method1 和 method2 所存取的私有成员总是类的最后一个实例对象中的这个私有实例成员。
问题出在哪儿呢?
问题出在每次通过 new class4() 创建一个对象实例时,class4 中的所有语句都会重新执行,因此,s_second 被重置,并成为新对象中的一个私有实例成员。而 class4.method1 和 class4.method2 也被重新定义了,而这个定义也将它们的变量作用域切换到了最后一个对象上来。这与把通过 prototype 方式创建的公有实例方法定义在类的内部而产生的错误是一样的。
所以,一定不要将公有静态成员定义在它所在的类的内部!也不要把通过 prototype 方式创建的公有实例方法定义在类的内部!
那如何定义一个私有静态成员呢?
2.4 私有静态成员
前面在基本概念里我们已经清楚了,只有用 function 创建函数,才能创建一个新的作用域,而要创建私有成员(不论是静态成员,还是实例成员),都需要通过创建新的作用域才能够起到数据隐藏的目的。下面所采用的方法就是基于这一点来实现的。
实现私有静态成员是通过创建一个匿名函数函数来创建一个新的作用域来实现的。
通常我们使用匿名函数时都是将它赋值给一个变量,然后通过这个变量引用该匿名函数。这种情况下,该匿名函数可以被反复调用或者作为类去创建对象。而 这里,我们创建的匿名函数不赋值给任何变量,在它创建后立即执行,或者立即实例化为一个对象,并且该对象也不赋值给任何变量,这种情况下,该函数本身或者 它实例化后的对象都不能够被再次存取,因此它唯一的作用就是创建了一个新的作用域,并隔离了它内部的所有局部变量和函数。因此,这些局部变量和函数就成了 我们所需要的私有静态成员。而这个立即执行的匿名函数或者立即实例化的匿名函数我们称它为静态封装环境。
下面我们先来看通过直接调用匿名函数方式来创建带有私有静态成员的类的例子:
- class5 = ( function () {
- // private static fields
- var s_first = 1 ;
- var s_second = 2 ;
- // private static methods
- function s_method1 () {
- s_first ++;
- }
- var s_second = 2 ;
- function constructor () {
- // private fields
- var m_first = 1 ;
- var m_second = 2 ;
- // private methods
- function method1 () {
- alert ( m_first ) ;
- }
- var method2 = function () {
- alert ( m_second ) ;
- }
- // public fields
- this . first = " first " ;
- this . second = [ ' s ' , ' e ' , ' c ' , ' o ' , ' n ' , ' d ' ] ;
- // public methods
- this . method1 = function () {
- s_second --;
- }
- this . method2 = function () {
- alert ( this . second ) ;
- }
- // constructor
- {
- s_method1 () ;
- this . method1 () ;
- }
- }
- // public static methods
- constructor . method1 = function () {
- s_first ++;
- alert ( s_first ) ;
- }
- constructor . method2 = function () {
- alert ( s_second ) ;
- }
- return constructor ;
- })() ;
- var o1 = new class5 () ;
- class5 . method1 () ;
- class5 . method2 () ;
- o1 . method2 () ;
- var o2 = new class5 () ;
- class5 . method1 () ;
- class5 . method2 () ;
- o2 . method2 () ;
这个例子中,通过
- ( function () {
- ...
- function contructor () {
- ...
- }
- return constructor ;
- })() ;
来创建了一个静态封装环境,实际的类是在这个环境中定义的,并且在最后通过 return 语句将最后的类返回给我们的全局变量 class5,然后我们就可以通过 class5 来引用这个带有静态私有成员的类了。
为了区分私有静态成员和私有实例成员,我们在私有静态成员前面用了 s_ 前缀,在私有实例成员前面加了 m_ 前缀,这样避免了重名,因此在对象中总是可以存取私有静态成员的。
但是这种命名方式不是必须的,只是推荐的,私有静态成员可以跟私有实例成员同名,在重名的情况下,在类构造器和在类中定义的实例方法中存取的都是私有实例成员,在静态方法(不论是公有静态方法还是私有静态方法)中存取的都是私有静态成员。
在类外并且在静态封装环境中通过 prototype 方式定义的公有实例方法存取的是私有静态成员。
在静态封装环境外定义的公有静态方法和通过 prototype 方式定义的公有实例方法无法直接存取私有静态成员。
另外一种方式通过直接实例化匿名函数方式来创建带有私有静态成员的类的例子跟上面的例子很相似:
- new function () {
- // private static fields
- var s_first = 1 ;
- var s_second = 2 ;
- // private static methods
- function s_method1 () {
- s_first ++;
- }
- var s_second = 2 ;
- class6 = function () {
- // private fields
- var m_first = 1 ;
- var m_second = 2 ;
- // private methods
- function method1 () {
- alert ( m_first ) ;
- }
- var method2 = function () {
- alert ( m_second ) ;
- }
- // public fields
- this . first = " first " ;
- this . second = [ ' s ' , ' e ' , ' c ' , ' o ' , ' n ' , ' d ' ] ;
- // public methods
- this . method1 = function () {
- s_second --;
- }
- this . method2 = function () {
- alert ( this . second ) ;
- }
- // constructor
- {
- s_method1 () ;
- this . method1 () ;
- }
- }
- // public static methods
- class6 . method1 = function () {
- s_first ++;
- alert ( s_first ) ;
- }
- class6 . method2 = function () {
- alert ( s_second ) ;
- }
- } ;
- var o1 = new class6 () ;
- class6 . method1 () ;
- class6 . method2 () ;
- o1 . method2 () ;
- var o2 = new class6 () ;
- class6 . method1 () ;
- class6 . method2 () ;
- o2 . method2 () ;
这个例子的结果跟通过第一种方式创建的例子是相同的。只不过它的静态封装环境是这样的:
- new function () {
- ...
- } ;
在这里,该函数没有返回值,并且对于 class5 的定义是直接在静态封装环境内部通过给一个没有用 var 定义的变量赋值的方式实现的。
当然,也完全可以在
- ( function () {
- ...
- })() ;
这种方式中,不给该函数定义返回值,而直接在静态封装环境内部通过给一个没有用 var 定义的变量赋值的方式来实现带有私有静态成员的类的定义。
这两种方式在这里是等价的。
2.5 静态类
所谓的静态类,是一种不能够被实例化,并且只包含有静态成员的类。
在 JavaScript 中我们通过直接实例化一个匿名函数的对象,就可以实现静态类了。例如:
- class7 = new function () {
- // private static fields
- var s_first = 1 ;
- var s_second = 2 ;
- // private static method
- function method1 () {
- alert ( s_first ) ;
- }
- // public static method
- this . method1 = function () {
- method1 () ;
- alert ( s_second ) ;
- }
- }
- class7 . method1 () ;
大家会发现,class7 其实就是个对象,只不过这个对象所属的是匿名类,该类在创建完 class7 这个对象后,就不能再被使用了。而 class7 不是一个 function,所以不能够作为一个类被实例化,因此,这里它就相当于一个静态类了。
前面我们讨论了 如何在 JavaScript 语言中实现对私有实例成员、公有实例成员、私有静态成员、公有静态成员和静态类的封装 。这次我们来讨论一下面向对象程序设计中的另外两个要素:继承与多态。1 又是几个基本概念
为什么要说又呢?
在讨论继承时,我们已经列出了一些基本概念了,那些概念是跟封装密切相关的概念,今天我们要讨论的基本概念,主要是跟继承与多态相关的,但是它们跟封装也有一些联系。
1.1 定义和赋值
变量定义是指用
这种形式来声明变量。
函数定义是指用
这种形式来声明函数。
是两个过程。第一个过程是定义变量 a,第二个过程是给变量 a 赋值。
同样
也是两个过程,第一个过程是定义变量 a 和一个匿名函数,第二个过程是把匿名函数赋值给变量 a。
变量定义和函数定义是在整个脚本执行之前完成的,而变量赋值是在执行阶段完成的。
变量定义的作用仅仅是给所声明的变量指明它的作用域,变量定义并不给变量初始值,任何没有定义的而直接使用的变量,或者定义但没有赋值的变量,他们的值都是 undefined。
函数定义除了声明函数所在的作用域外,同时还定义函数体结构。这个过程是递归的,也就是说,对函数体的定义包括了对函数体内的变量定义和函数定义。
通过下面这个例子我们可以更明确的理解这一点:
alert ( b ) ;
alert ( c ) ;
var a = " a " ;
function a () {}
function b () {}
var b = " b " ;
var c = " c " ;
var c = function () {}
alert ( a ) ;
alert ( b ) ;
alert ( c ) ;
猜猜这个程序执行的结果是什么?然后执行一下看看是不是跟你想的一样,如果跟你想的一样的话,那说明你已经理解上面所说的了。
这段程序的结果很有意思,虽然第一个 alert(a) 在最前面,但是你会发现它输出的值竟然是 function a() {},这说明,函数定义确实在整个程序执行之前就已经完成了。
再来看 b,函数 b 定义在变量 b 之前,但是第一个 alert(b) 输出的仍然是 function b() {},这说明,变量定义确实不对变量做什么,仅仅是声明它的作用域而已,它不会覆盖函数定义。
最后看 c,第一个 alert(c) 输出的是 undefined,这说明 var c = function() {} 不是对函数 c 定义,仅仅是定义一个变量 c 和一个匿名函数。
再来看第二个 alert(a),你会发现输出的竟然是 a,这说明赋值语句确实是在执行过程中完成的,因此,它覆盖了函数 a 的定义。
第二个 alert(b) 当然也一样,输出的是 b,这说明不管赋值语句写在函数定义之前还是函数定义之后,对一个跟函数同名的变量赋值总会覆盖函数定义。
第二个 alert(c) 输出的是 function() {},这说明,赋值语句是顺序执行的,后面的赋值覆盖了前面的赋值,不管赋的值是函数还是其它对象。
理解了上面所说的内容,我想你应该知道什么时候该用 function x(..) {…},什么时候该用 var x = function (…) {…} 了吧?
最后还要提醒一点,eval 中的如果出现变量定义和函数定义,则它们是在执行阶段完成的。 所以,不到万不得已,不要用 eval!另外,即使要用 eval,也不要在里面用局部变量和局部方法!
1.2 this 和执行上下文
在前面讨论封装时,我们已经接触过 this 了。在对封装的讨论中,我们看到的 this 都是表示 this 所在的类的实例化对象本身。真的是这样吗?
先看一下下面的例子吧:
- var x = " I'm a global variable! " ;
- function method () {
- alert ( x ) ;
- alert ( this . x ) ;
- }
- function class1 () {
- // private field
- var x = " I'm a private variable! " ;
- // private method
- function method1 () {
- alert ( x ) ;
- alert ( this . x ) ;
- }
- var method2 = method ;
- // public field
- this . x = " I'm a object variable! " ;
- // public method
- this . method1 = function () {
- alert ( x ) ;
- alert ( this . x ) ;
- }
- this . method2 = method ;
- // constructor
- {
- this . method1 () ; // I'm a private variable!
- // I'm a object variable!
- this . method2 () ; // I'm a global variable!
- // I'm a object variable!
- method1 () ; // I'm a private variable!
- // I'm a global variable!
- method2 () ; // I'm a global variable!
- // I'm a global variable!
- method1 . call ( this ) ; // I'm a private variable!
- // I'm a object variable!
- method2 . call ( this ) ; // I'm a global variable!
- // I'm a object variable!
- }
- }
- var o = new class1 () ;
- method () ; // I'm a global variable!
- // I'm a global variable!
- o . method1 () ; // I'm a private variable!
- // I'm a object variable!
- o . method2 () ; // I'm a global variable!
- // I'm a object variable!
为什么是这样的结果呢?
那就先来看看什么是执行上下文吧。那什么是执行上下文呢?
如果当前正在执行的是一个方法,则执行上下文就是该方法所附属的对象,如果当前正在执行的是一个创建对象(就是通过 new 来创建)的过程,则创建的对象就是执行上下文。
如果一个方法在执行时没有明确的附属于一个对象,则它的执行上下文是全局对象(顶级对象),但它不一定附属于全局对象。全局对象由当前环境来决定。在浏览器环境下,全局对象就是 window 对象。
定义在所有函数之外的全局变量和全局函数附属于全局对象,定义在函数内的局部变量和局部函数不附属于任何对象。
那执行上下文跟变量作用域有没有关系呢?
执行上下文与变量作用域是不同的。
一个函数赋值给另一个变量时,这个函数的内部所使用的变量的作用域不会改变,但它的执行上下文会变为这个变量所附属的对象(如果这个变量有附属对象的话)。
Function 原型上的 call 和 apply 方法可以改变执行上下文,但是同样不会改变变量作用域。
要理解上面这些话,其实只需要记住一点:
变量作用域是在定义时就确定的,它永远不会变;而执行上下文是在执行时才确定的,它随时可以变。
这样我们就不难理解上面那个例子了。this.method1() 这条语句(注意,这里说的还没有进入这个函数体)执行时,正在创建对象,那当前的执行上下文就是这个正在创建的对象,所以 this 指向的也是当前正在创建的对象,在 this.method1() 这个方法执行时(这里是指进入函数体),这个正在执行的方法所附属的对象也是这个正在创建的对象,所以,它里面 this.x 的 this 也是同一个对象,所以你看的输出就是 I’m a object variable! 了。
而在执行 method1() 这个函数时(是指进入函数体后),method1() 没有明确的附属于一个对象,虽然它是定义在 class1 中的,但是他并没有不是附属于 class1 的,也不是附属于 class1 实例化后的对象的,只是它的作用域被限制在了 class1 当中。因此,它的附属对象实际上是全局对象,因此,当在它当中执行到 alert(this.x) 时,this.x 就成了我们在全局环境下定义的那个值为 “I’m a global variable!” 的 x 了。
method2() 虽然是在 class1 中定义的,但是 method() 是在 class1 之外定义的,method 被赋值给 method2 时,并没有改变 method 的作用域,所以,在 method2 执行时,仍然是在 method 被定义的作用域内执行的,因此,你看到的就是两个 I’m a global variable! 输出了。同样,this.method2() 调用时,alert(x) 输出 I’m a global variable! 也是这个原因。
因为 call 会改变执行上下文,所以通过 method1.call(this) 和 method2.call(this) 时,this.x 都变成了 I’m a object variable!。但是它不能改变作用域,所以 x 仍然跟不使用 call 方法调用时的结果是一样的。
而我们后面执行 o.method1() 时,alert(x) 没有用 this 指出 x 的执行上下文,则 x 表示当前执行的函数所在的作用域中最近定义的变量,因此,这时输出的就是 I’m a private variable!。最后输出 I’m a object variable! 我想不用我说大家也知道为什么了吧?:D
2 继承和多态
2.1 从封装开始
前面我们说了,封装的目的是实现数据隐藏。
但是更深一层来说,在 javascript 中进行封装还有以下几个好处:
1、隐身实现细节,当私有部分的实现完全重写时,并不需要改变调用者的行为。这也是其它面向对象语言要实现封装的主要目的。
2、javascript 中,局部变量和局部函数访问速度更快,因此把私有字段以局部变量来封装,把私有方法以局部方法来封装可以提高脚本的执行效率。
3、对于 javascript 压缩混淆器(据我所知,目前最好的 javascript 分析、压缩、混淆器就是 JSA ) 来说,局部变量和局部函数名都是可以被替换的,而全局变量和全局函数名是不可以被替换的(实际上,对于 javascript 脚本解析器工作时也是这样的)。因此,不论对于开源还是非开源的 javascript 程序,当私有字段和私有方法使用封装技术后,编写代码时就可以给它们定义足够长的表意名称,增加代码的可读性,而发布时,它们可以被替换为一些很短的名称 (一般是单字符名称),这样就可以得到充分的压缩和混淆。及减少了带宽占用,又可以真正实现细节的隐藏。
所以,封装对于 javascript 来说,是非常有用的!
那么在 javascript 实现继承是为了什么呢?
2.2 为什么要继承
在其它面向对象程序设计语言中,继承除了可以减少重复代码的编写外,最大的用处就是为了实现多态。尤其是在强类型语言中,尤为如此:
1、在强类型语言中,一个变量不能够被赋予不同类型的两个值,除非这两种类型与这个变量的类型是相容的,而这个相容的关系就是由继承来实现的。
2、在强类型语言中,对一个已有的类型无法直接进行方法的扩充和改写,要扩充一个类型,唯一的方法就是继承它,在它的子类中进行扩充和改写。
因此,对于强类型的面向对象语言,多态的实现是依赖于继承的实现的。
而对于 javascript 语言来说,继承对于实现多态则显得不那么重要:
1、在 javascript 语言中,一个变量可以被赋予任何类型的值,且可以用同样的方式调用任何类型的对象上的同名方法。
2、在 javascript 语言中,可以对已有的类型通过原型直接进行方法的扩充和改写。
所以,在 javascript 中,继承的主要作用就是为了减少重复代码的编写。
接下来我们要谈的两种实现继承的方法可能大家已经都很熟悉了,一种是原型继承法,一种是调用继承法,这两种方法都不会产生副作用。我们主要讨论的是这两种方法的本质和需要注意的地方。
2.3 原型继承法
在 javascript 中,每一个类(函数)都有一个原型,该原型上的成员在该类实例化时,会传给该类的实例化对象。实例化的对象上没有原型,但是它可以作为另一个类(函数)的 原型,当以该对象为原型的类实例化时,该对象上的成员就会传给以它为原型的类的实例化对象上。这就是原型继承的本质。
原型继承也是 javascript 中许多原生对象所使用的继承方法。
- function parentClass () {
- // private field
- var x = " I'm a parentClass field! " ;
- // private method
- function method1 () {
- alert ( x ) ;
- alert ( " I'm a parentClass method! " ) ;
- }
- // public field
- this . x = " I'm a parentClass object field! " ;
- // public method
- this . method1 = function () {
- alert ( x ) ;
- alert ( this . x ) ;
- method1 () ;
- }
- }
- parentClass . prototype . method = function () {
- alert ( " I'm a parentClass prototype method! " ) ;
- }
- parentClass . staticMethod = function () {
- alert ( " I'm a parentClass static method! " ) ;
- }
- function subClass () {
- // private field
- var x = " I'm a subClass field! " ;
- // private method
- function method2 () {
- alert ( x ) ;
- alert ( " I'm a subClass method! " ) ;
- }
- // public field
- this . x = " I'm a subClass object field! " ;
- // public method
- this . method2 = function () {
- alert ( x ) ;
- alert ( this . x ) ;
- method2 () ;
- }
- this . method3 = function () {
- method1 () ;
- }
- }
- // inherit
- subClass . prototype = new parentClass () ;
- subClass . prototype . constructor = subClass ;
- // test
- var o = new subClass () ;
- alert ( o instanceof parentClass ) ; // true
- alert ( o instanceof subClass ) ; // true
- alert ( o . constructor ) ; // function subClass() {...}
- o . method1 () ; // I'm a parentClass field!
- // I'm a subClass object field!
- // I'm a parentClass field!
- // I'm a parentClass method!
- o . method2 () ; // I'm a subClass field!
- // I'm a subClass object field!
- // I'm a subClass field!
- // I'm a subClass method!
- o . method () ; // I'm a parentClass prototype method!
- o . method3 () ; // Error!!!
- subClass . staticMethod () ; // Error!!!
上面这个例子很好的反映出了如何利用原型继承法来实现继承。
利用原型继承的关键有两步操作:
首先创建一个父类的实例化对象,然后将该对象赋给子类的 prototype 属性。
这样,父类中的所有公有实例成员都会被子类继承。并且用 instanceof 运算符判断时,子类的实例化对象既属于子类,也属于父类。
然后将子类本身赋值给它的 prototype 的 constructor 属性。(注意:这里赋值的时候是没有 () 的!)
这一步是为了保证在查看子类的实例化对象的 constructor 属性时,看到的是子类的定义,而不是其父类的定义。
接下来,通过对 o.method1() 调用的结果我们会看到,子类继承来的公有实例方法中,如果调用了私有实例字段或者私有实例方法,则所调用的这些私有实例成员是属于父类的。
同样,通过对 o.method2() 调用的结果我们看到,子类中定义的实例方法,如果调用了私有实例字段或者私有实例方法,则所调用的这些私有实例成员是属于子类的。
通过对 o.method() 调用的结果我们看到,定义在父类原型上的方法,会被子类继承。
通过对 o.method3() 调用的结果我们看到,子类中定义的实例方法是不能访问父类中定义的私有实例成员的。
最后,通过对 subClass.staticMethod() 调用的结果我们看到,静态成员是不会被继承的。
2.4 调用继承法
调用继承的本质是,在子类的构造器中,让父类的构造器方法在子类的执行上下文上执行,父类构造器方法上所有通过 this 方式操作的内容实际上都都是操作的子类的实例化对象上的内容。因此,这种做法仅仅为了减少重复代码的编写。
- function parentClass () {
- // private field
- var x = " I'm a parentClass field! " ;
- // private method
- function method1 () {
- alert ( x ) ;
- alert ( " I'm a parentClass method! " ) ;
- }
- // public field
- this . x = " I'm a parentClass object field! " ;
- // public method
- this . method1 = function () {
- alert ( x ) ;
- alert ( this . x ) ;
- method1 () ;
- }
- }
- parentClass . prototype . method = function () {
- alert ( " I'm a parentClass prototype method! " ) ;
- }
- parentClass . staticMethod = function () {
- alert ( " I'm a parentClass static method! " ) ;
- }
- function subClass () {
- // inherit
- parentClass . call ( this ) ;
- // private field
- var x = " I'm a subClass field! " ;
- // private method
- function method2 () {
- alert ( x ) ;
- alert ( " I'm a subClass method! " ) ;
- }
- // public field
- this . x = " I'm a subClass object field! " ;
- // public method
- this . method2 = function () {
- alert ( x ) ;
- alert ( this . x ) ;
- method2 () ;
- }
- this . method3 = function () {
- method1 () ;
- }
- }
- // test
- var o = new subClass () ;
- alert ( o instanceof parentClass ) ; // false
- alert ( o instanceof subClass ) ; // true
- alert ( o . constructor ) ; // function subClass() {...}
- o . method1 () ; // I'm a parentClass field!
- // I'm a subClass object field!
- // I'm a parentClass field!
- // I'm a parentClass method!
- o . method2 () ; // I'm a subClass field!
- // I'm a subClass object field!
- // I'm a subClass field!
- // I'm a subClass method!
- o . method () ; // Error!!!
- o . method3 () ; // Error!!!
- subClass . staticMethod () ; // Error!!!
上面这个例子很好的反映出了如何利用调用继承法来实现继承。
利用调用继承的关键只有一步操作:
就是在子类定义时,通过父类的 call 方法,将子类的 this 指针传入。使父类方法在子类上下文中执行。
这样,父类中的所有在父类内部通过 this 方式定义的公有实例成员都会被子类继承。
用 instanceof 运算符判断时,子类的实例化对象只属于子类,不属于父类。
查看子类的实例化对象的 constructor 属性时,看到的是子类的定义,不是其父类的定义。
接下来,通过对 o.method1() 和 o.method2() 调用的结果跟原型继承法的调用结果是相同的,所说明的问题也是一样的,这里不再重复。
通过对 o.method() 调用的结果我们看到,定义在父类原型上的方法,不会被子类继承。
通过对 o.method3() 调用的结果我们看到,子类中定义的实例方法同样不能访问父类中定义的私有实例成员的。
最后,通过对 subClass.staticMethod() 调用的结果我们看到,静态成员同样不会被继承的。
最后,还有一点,在这个例子中没有体现出来,就是通过调用继承法,可以实现多继承。也就是说,一个子类可以从多个父类中继承通过 this 方式定义在父类内部的所有公有实例成员。
作为一种弱类型语言,javascript 提供了丰富的多态性,javascript 的多态性是其它强类型面向对象语言所不能比的。
多态
重载和覆盖
先来说明一下重载和覆盖的区别。重载的英文是 overload,覆盖的英文是 override。发现网上大多数人把 override 当成了重载,这个是不对的。重载和覆盖是有区别的。
重载的意思是,同一个名字的函数(注意这里包括函数)或方法可以有多个实现,他们依靠参数的类型和(或)参数的个数来区分识别。
而覆盖的意思是,子类中可以定义与父类中同名,并且参数类型和个数也相同的方法,这些方法的定义后,在子类的实例化对象中,父类中继承的这些同名方法将被隐藏。
重载
javascript 中函数的参数是没有类型的,并且参数个数也是任意的,例如,尽管你可以定义一个:
- function add ( a , b ) {
- return a + b ;
- }
这样的函数,但是你仍然可以再调用它是带入任意多个参数,当然,参数类型也是任意的。至于是否出错,那是这个函数中所执行的内容来决定的,javascript 并不根据你指定的参数个数和参数类型来判断你调用的是哪个函数。
因此,要定义重载方法,就不能像强类型语言中那样做了。但是你仍然可以实现重载。就是通过函数的 arguments 属性。例如:
- function add () {
- var sum = 0 ;
- for ( var i = 0 ; i < arguments . length ; i ++ ) {
- sum += arguments [ i ] ;
- }
- return sum ;
- }
这样你就实现了任意多个参数加法函数的重载了。
当然,你还可以在函数中通过 instanceof 或者 constructor 来判断每个参数的类型,来决定后面执行什么操作,实现更为复杂的函数或方法重载。总之,javascript 的重载,是在函数中由用户自己通过操作 arguments 这个属性来实现的。
覆盖
实现覆盖也很容易,例如:
- function parentClass () {
- this . method = function () {
- alert ( " parentClass method " ) ;
- }
- }
- function subClass () {
- this . method = function () {
- alert ( " subClass method " ) ;
- }
- }
- subClass . prototype = new parentClass () ;
- subClass . prototype . constructor = subClass ;
- var o = new subClass () ;
- o . method () ;
这样,子类中定义的 method 就覆盖了从父类中继承来的 method 方法了。
你可能会说,这样子覆盖是不错,但 java 中,覆盖的方法里面可以调用被覆盖的方法(父类的方法),在这里怎么实现呢?也很容易,而且比 java 中还要灵活,java 中限制,你只能在覆盖被覆盖方法的方法中才能使用 super 来调用次被覆盖的方法。我们不但可以实现这点,而且还可以让子类中所有的方法中都可以调用父类中被覆盖的方法。看下面的例子:
- function parentClass () {
- this . method = function () {
- alert ( " parentClass method " ) ;
- }
- }
- function subClass () {
- var method = this . method ;
- this . method = function () {
- method . call ( this ) ;
- alert ( " subClass method " ) ;
- }
- }
- subClass . prototype = new parentClass () ;
- subClass . prototype . constructor = subClass ;
- var o = new subClass () ;
- o . method () ;
你会发现,原来这么简单,只要在定义覆盖方法前,定义一个私有变量,然后把父类中定义的将要被覆盖的方法赋给它,然后我们就可以在后面继续调用它了,而且这个是这个方法是私有的,对于子类的对象是不可见的。这样跟其它高级语言实现的覆盖就一致了。
最后需要注意,我们在覆盖方法中调用这个方法时,需要用 call 方法来改变执行上下文为 this(虽然在这个例子中没有必要),如果直接调用这个方法,执行上下文就会变成全局对象了。