关于JavaScript OOP实现的论证

很久都没涉及JavaScript这门语言了,有些生疏,今天将对其OOP(面向对象编程)方式进行复习论证,加深印象。 面向对象编程模式的出现,使得大型项目开发效率提高,耦合度降低,有利于代码维护,特别是在.net/JAVA平台,一开始便坠入了面向对象的深渊....

JavaScript本身并没有正式的类,其实现方式完全是通过JavaScript的特性模拟出来的,至于JavaScript OOP基础教程,下文将不完全涉及,请读者先参考w3school关于JAVASCRIPT OOP教程:http://www.w3school.com.cn/js/projsobject_oriented.asp
在此基础上,下面将会对本人觉得有疑点的地方逐一进行论证。

1.new 和this的关系

function ClassA() {  
    this.num = 100;
    return this;
}
var b = ClassA();  
alert(b.toString()); //window  
alert(b.num); //100  
var b = new ClassA(); //object  
alert(b.toString()); //100  
alert(b.num);  
//////////////////////////
function ClassA() {  
    //return this;//使用new时可省略
}
var b = new ClassA();  
alert(b.toString());//object  

第一次返回window,证明关键字this总是指向调用该方法的对象.. 第二次返回object,证明new在运行构造函数时,创建了一个对象,并且这个对象只能被this访问,而当使用new时,默认构造函数返回this,而不需要显示的return。

2.prototype 和 proto
先看几段代码

1.///////////////////  
<script type="text/javascript">  
function ClassA(){  
    ClassA.prototype.clo="";
}
var b=new ClassA();  
var c=new ClassA();  
b.clo="aa";  
c.clo="bb";  
alert(b.clo);//aa  
alert(c.clo);//bb  
</script>


2.///////////////  
function ClassA(cs){  
    ClassA.prototype.clo=cs;
}
var b=new ClassA("aa");  
var c=new ClassA("bb");  
alert(b.clo);//bb  
alert(c.clo);//bb


3.//////////////////////  
<script type="text/javascript">  
function ClassA(cs){  
    ClassA.prototype.clo=cs;
}
var b=new ClassA("aa");  
var c=new ClassA("bb");  
b.clo="aa";  
c.clo="bb";  
alert(b.clo);//aa  
alert(c.clo);//bb  
</script>

4.////////////////////  
<script type="text/javascript">  
function ClassA(){  
    ClassA.prototype.clo="";
}
var b=new ClassA();  
var c=new ClassA();  
b.clo="aa";  
c.clo="bb";  
ClassA.prototype.clo="ss";  
alert(b.clo);//aa  
alert(c.clo);//bb  
alert(b.__proto__.clo);//ss  
</script>  

以上四段代码证明,在新对象创建的时候,当一个对象被创建时,构造函数将会把它的属性prototype赋给新对象的内部属性proto。这个proto被这个对象用来查找它的属性。其查找顺序是,先在this范围内查找,如果没有,就到proto中查找,而proto指向构造函数的prototype..... 第一段代码往往被误认为b.clo c.clo 其实就是ClassA.prototype.clo,其实这是错误的,JavaScript是种晚绑定的机制。。。

其后续的b.clo和c.clo其实是为(new this)(即用new创建的对象或者b,c指向的对象)范围内创建的新属性clo,所以在后面输出的时候,b,c分别先在this范围内查找到了clo,然后输出.... 第二段代码证明proto指向构造函数的prototype,所有由该构造函数创建的实例都共享同样样的方法和属性,那就是prototype原型对象..

第二段和第三段代码证明了第一段代码的证明 第四段代码alert(b.proto.clo);//ss证明了第二段代码的论证...

所以当一个对象被创建时,构造函数将会把它的属性prototype赋给新对象的内部属性proto。这个proto被这个对象用来查找它的属性。其查找顺序是,先在this范围内查找,如果没有,就到proto中查找,而proto指向构造函数的prototype,而其后出现的和prototype中相同的属性,其本是JAVASCRIPT中的晚绑定机制,创建了this范围内的新属性,其查询先后顺序,感觉覆盖了prototype中的属性....

所以根据1,2可以得出混合的构造函数/原型方式,即用构造函数定义对象的所有非函数属性,用原型方式定义对象的函数属性(方法)。结果是,所有函数都只创建一次,而每个对象都具有自己的对象属性实例。

function ClassA(name, sex) {  
    this.name = name;
    this.sex = sex;
    ClassA.prototype.getName = function () {
        return this.name;
    };
}

3.原型链

<script type="text/javascript">  
function ClassA(){


};
alert(ClassA.prototype);  
</script>  

说明prototype本身是个对象....
下面将引用别人针对prototype继承的代码解释,结合以上两点,将更容易理解....
基本的用法 把ClassA的一个实例赋值给ClassB ClassB就继承了ClassA的所有属性

function ClassA() {  
    this.a = 'a';
}
function ClassB() {  
    this.b = 'b';
}
ClassB.prototype = new ClassA();  
var objB = new ClassB();  
for (var p in objB) document.write(p + "<br>");  

从原型继承理论的角度去考虑 js的原型继承是引用原型 不是复制原型
所以 修改原型会导致所有B的实例的变化

<script>  
function ClassA()  
{
    this.a='a';
}
function ClassB()  
{
    this.b='b';
}
ClassB.prototype=new ClassA();  
var objB=new ClassB();  
alert(objB.a);  
ClassB.prototype.a='changed!!';  
alert(objB.a);  
</script>  

然而 子类对象的写操作只访问子类对象中成员 它们之间不会互相影响 因此 写是写子类 读是读原型(如果子类中没有的话)

<script>  
function ClassA()  
{
    this.a='a';
}
function ClassB()  
{
    this.b='b';
}
ClassB.prototype=new ClassA();  
var objB1=new ClassB();  
var objB2=new ClassB();  
objB1.a='!!!';  
alert(objB1.a);  
alert(objB2.a);  
</script>  

每个子类对象都执有同一个原型的引用 所以子类对象中的原型成员实际是同一个

<script>  
function ClassA()  
{
    this.a=function(){alert();};
}
function ClassB()  
{
    this.b=function(){alert();};
}
ClassB.prototype=new ClassA();  
var objB1=new ClassB();  
var objB2=new ClassB();  
alert(objB1.a==objB2.a);  
alert(objB1.b==objB2.b);  
</script>  

构造子类时 原型的构造函数不会被执行

<script>  
function ClassA()  
{
    alert("a");
    this.a=function(){alert();};
}
function ClassB()  
{
    alert("b");
    this.b=function(){alert();};
}
ClassB.prototype=new ClassA();  
var objB1=new ClassB();  
var objB2=new ClassB();  
</script>  

接下来是致命的,在子类对象中访问原型的成员对象:

Code:js  
 
<script>  
function ClassA()  
{
    this.a=[];
}
function ClassB()  
{
    this.b=function(){alert();};
}
ClassB.prototype=new ClassA();  
var objB1=new ClassB();  
var objB2=new ClassB();  
objB1.a.push(1,2,3);  
alert(objB2.a);  
//所有b的实例中的a成员全都变了!!
</script>  

所以 在prototype继承中 原型类中不能有成员对象! 所有成员必须是值类型数据(string也可以)
用prototype继承有执行效率高,不会浪费内存,为父类动态添置方法后子类中马上可见等的优点。
我就非常喜欢用prototype继承。
prototype继承是通过把子类的原型对象(prototype)设置成父类的一个实例来进行继承的。
只简单的这样设置继承的确如上所说,有不少缺点。总的来说有四个缺点:   缺点一:父类的构造函数不是像JAVA中那样在给子类进行实例化时执行的,而是在设置继承的时候执行的,并且只执行一次。这往往不是我们希望的,特别是父类的构造函数中有一些特殊操作的情况下。

  缺点二:由于父类的构造函数不是在子类进行实例化时执行,在父类的构造函数中设置的成员变量到了子类中就成了所有实例对象公有的公共变量。由于JavaScript中继承只发生在“获取”属性的值时,对于属性的值是String,Number和Boolean这些数据本身不能被修改的类型时没有什么影响。但是Array和Object类型就会有问题。

  缺点三:如果父类的构造函数需要参数,我们就没有办法了。

  缺点四:子类原本的原型对象被替换了,子类本身的constructor属性就没有了。在类的实例取它的constructor属性时,取得的是从父类中继承的constructor属性,从而constructor的值是父类而不是子类。

4.call()和apply() 方法

从以上得知,原型链的父类成员被子类所有对象共享,所以和JAVA/C++的继承还是不同
下面针对W3SCHOOL的继承做解释论证:

对象冒充
构想原始的 ECMAScript 时,根本没打算设计对象冒充(object masquerading)。它是在开发者开始理解函数的工作方式,尤其是如何在函数环境中使用 this 关键字后才发展出来。

其原理如下:构造函数使用 this 关键字给所有属性和方法赋值(即采用类声明的构造函数方式)。因为构造函数只是一个函数,所以可使 ClassA 构造函数成为 ClassB 的方法,然后调用它。ClassB 就会收到 ClassA 的构造函数中定义的属性和方法。例如,用下面的方式定义 ClassA 和 ClassB:

Code:js

function ClassA(sColor) {  
    this.color = sColor;
    this.sayColor = function () {
        alert(this.color);
    };
}

function ClassB(sColor) {  
}

还记得吗?关键字 this 引用的是构造函数当前创建的对象。不过在这个方法中,this 指向的所属的对象。这个原理是把 ClassA 作为常规函数来建立继承机制,而不是作为构造函数。如下使用构造函数 ClassB 可以实现继承机制

function ClassB(sColor) {  
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
}

在这段代码中,为 ClassA 赋予了方法 newMethod(请记住,函数名只是指向它的指针)。然后调用该方法,传递给它的是 ClassB 构造函数的参数 sColor。最后一行代码删除了对 ClassA 的引用,这样以后就不能再调用它。

为什么形成了继承机制,还记得我们的第一条论证吗,this是指调用它的对象,new在运行构造函数时,创建了一个对象,并且这个对象只能被this访问...
var ts=new ClassB("blue");
1.new创建了一个新对象, this.newMethod = ClassA;的this指向了这个新对象
2.this.newMethod(sColor);调用ClassA(sColor)
3.ClassA(sColor)中的 this.color = sColor;中的this指向了调用它的对象this.newMethod(sColor);中的this,即new创建的新对象...
这下懂了吧...
call() 方法
call() 方法是与经典的对象冒充方法最相似的方法。它的第一个参数用作 this 的对象。其他参数都直接传递给函数自身。例如:

function sayColor(sPrefix, sSuffix) {  
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();  
obj.color = "blue";  
sayColor.call(obj, "The color is ", "a very nice color indeed.");  

在这个例子中,函数sayColor() 在对象外定义,即使它不属于任何对象,也可以引用关键字 this。对象 obj 的 color 属性等于 blue。调用 call() 方法时,第一个参数是 obj,说明应该赋予 sayColor() 函数中的 this 关键字值是 obj。第二个和第三个参数是字符串。它们与sayColor() 函数中的参数 sPrefix和 sSuffix 匹配,最后生成的消息 "The color is blue, avery nice color indeed." 将被显示出来。 要与继承机制的对象冒充方法一起使用该方法,只需将前三行的赋值、调用和删除代码替换即可:

function ClassB(sColor, sName) {  
    //this.newMethod = ClassA;    
    //this.newMethod(color);   
    //delete this.newMethod;    
    ClassA.call(this, sColor);
    this.name = sName;
    this.sayName = function () { alert(this.name); };
}

这里,我们需要让 ClassA 中的关键字 this 等于新创建的 ClassB 对象,因此 this 是第一个参数。第二个参数 sColor 对两个类来说都是唯一的参数。
call()的原理就是对象冒充.....
apply() 方法同理...只是参数是数组

5.针对原型链继承和对象冒充继承的混合式继承

<script>  
function ClassA(){  
    ClassA.prototype.clo="aa";
    this.name="bb";
}
function ClassB(){  
    ClassA.call(this);
}
var b=new ClassB();  
var a=new ClassA();  
alert(a.clo);//aa  
alert(b.clo);//undefined  
alert(b.name);//bb  
</script>  

说明使用对象冒充方式无法继承超类prototype中的成员, 为什么呢?
因为prototype指向的对象的引用赋给proto是在使用new时,看下对象冒充

this.newMethod = ClassA;  
this.newMethod();  
delete this.newMethod;  

1.在new ClassB()时,即把ClassA.prototype指向的对象的引用赋给了this.proto
2.然后this.newMethod()时,并没有new ,创建新对象,而ClassA中的this其实是this.newMethod = ClassA;中的this,并没有把ClassA.prototype赋给this.proto
所以最后实例化的对象只继承了子类的prototype...
那么我们可以采用混合方式,用对象冒充继承构造函数的属性,用原型链继承 prototype 对象的方法.

<script>  
function ClassA(){  
    ClassA.prototype.clo="aa";


    this.name="bb";
}
function ClassB(){  
    ClassA.call(this);//对象冒充
}
ClassB.prototype=new ClassA();//原型对象  
var b=new ClassB();  
var a=new ClassA();  
alert(a.clo);//aa  
alert(b.clo);//aa  
alert(b.name);//bb  
</script>  

其原因是虽然用用原型链继承 prototype 对象,继承了ClassA中所有成员,用冒充继承构造函数只继承了ClassA中除了prototype 对象的全部成员,但是冒充继承的成员属于this范围覆盖了继承 prototype 对象的同名成员,所以最后冒充继承构造函数的属性,而原型链继承 prototype 对象的方法....

BreezeDust

继续阅读我的更多文章