JavaScript寻踪OOP之路

时间:2023-06-17 13:09:32

  上一集中,重点介绍了谁动了你的代码。这里先总结一下:咱们的代码从敲下来到运行出结果,经历了两个阶段:分析期与运行期。在分析期,JavaScript分析器悄悄动了我们的代码;在运行期,JavaScript又按照自己的一套机制进行变量寻找。我们的代码是如何被动了手脚的,相信看官你已经明白。但是前面所聊均是面向过程的,如果说只是简单的面向过程言语,那JavaScript能够有基本的数据类型,基本的执行单元那也差不多了。但是故事并没有在此结束。接下来剧情的发展,那才是造成今天鞋同们困惑的地方,那们还是从故事开始。大伙不要嫌楼主啰嗦(楼主确实是个啰嗦之人),讲这故事是为了让大伙了解当年布大师设计JavaScript的背景,融入布大师的设计思维,你就知道JavaScript为什么会有哪些奇怪的设计。好,故事开始了。

  前几集的故事中,咱们提到了布大师只想设计一个简单、满足浏览器进行数据检验的脚本言语。当时的web应用毫无颜值,犹如白纸黑字,顶多再加点图片。所以,你也别期待当时的布大师会想到如UI交互、动画效果等等的设计需求。为此,从一开始布大师设计的JavaScript就是一个过程式的简单的言语,但是布大师也不是个迂腐落后之人。c的升级版c++、让编程界有点疯狂的Java,布大师也不能视而不见,多少受点影响。于是乎,布大师想:我这JavaScript能否也玩点OOP思想呢?布大师这么一想,一堆问题就来了,本来就没打算搞个正式的OOP脚本,也没设计有class、extend,更没有override啥的。但是今天拍脑袋一想要玩OOP,那总得在现有的设计基础上去实现OOP三大思想(封装、继承、多态)吧。那咱们就看看布大师是如何给JavaScript赋予OOP的。

封装

  概念,楼主就不说了。但是你看看JavaScript定义的那些数据类型,压根就没class的概念。没有类何来实例,没有实例谈何封装?布大师翻来覆去研究已经定义的数据类型,再对比了c++、java。他发现c++、java每次创建对象都离不开调用构造函数。布大师灵感一来“对!绕过class直接调用构造函数创建对象,刚好function可以作为构造函数”。于是乎,你见到了今天JavaScript是这样创建实现对象的:

	/***
*定义构造函数
****/
function clazz(params){ }
/**通过构造函数创建实例**/
var ins=new clazz(1);

  好了,创建对象的事情是解决了。但也不是说有了new就万事大吉。码农们都知道,有类得有类的数据成员吧。布大师也知道这个问题,那他是怎么赋予数据成员呢?这家伙又去挖java的idea了,他发现java实例都有this。this这个神器都可以直接访问数据成员。于是乎,他又把this扯进了function里面,用于封装实例的数据成员。这样看上去封装的事情解决得差不多了。

     /***
*定义构造函数
****/
function clazz(p1,p2){
this.p1=p1;
this.p2=p2;
this.print=function(){
console.log(this.p1+this.p2);
}
} /**通过构造函数创建实例**/
var ins1=new clazz(1,1);
/***调用实例成员 修改ins1.p1 看看是否影响了ins2.p1****/
ins1.p1=9;
ins1.print(); //结果1+9=10
var ins2=new clazz(3,1);//结果3+1=4
ins2.print();
console.log("结论修改ins1.p1不影响ins2.p1;证明this下的定义是各自储存副本的");

  

  至此,JavaScript即可创建实例,有具有实例成员,看上去没啥问题,满足了OOP的“封装”思想,但是布大师就是大师,他总觉得上面的代码还有点问题。this是属于实例的,这意味着,挂在this下面的成员不是实例共享的(ins1、ins2分别储存了各自的副本)。如上面p1、p2作为数据成员,各自实例分别拥有那是十分合理的。但是print这个函数也搞分别拥有似乎不合理啊。因为函数是基于栈运行的,而栈又是私有的,函数的执行其实就是在私有栈里面跑代码,这里将实例函数定义到this下面有点浪费内存了。布大师又脑洞大开:那我得在function上面再搞点东西来存放实例共享的成员,解决无法共享成员浪费内存的问题。于是乎,他在每个function构造函数里面搞了prototype用于存放实例共享成员,每个实例实际上都拥有prototype定义的成员。

    /***
*定义构造函数
****/
function clazz(p1,p2){
this.p1=p1;
this.p2=p2;
this.print=function(){
console.log(this.p1+this.p2);
}
}
/***定义实例共享成员***/
clazz.prototype.p3=function(){
console.log("come on man!");
};
/**通过构造函数创建实例**/
var ins1=new clazz(1,1);
var ins2=new clazz(3,1); console.log(ins1.p3());//结果:come on man!
console.log(ins2.p3());//结果:come on man! /**clazz.prototype.p3 看看是否影响了ins1 ins2**/
clazz.prototype.p3=function(){
console.log("come on girl!");
};
console.log(ins1.p3());//结果:come on girl!
console.log(ins2.p3());//结果:come on girl!
console.log("定义到prototype的成员是各个实例共享的!");

  布大师终于围绕着funciton搞掂了OOP的封装思想,对funciton瞬间大爱啊!那接下来的继承,看来布大师还是会围绕function来动脑筋了。看官你听说过“function是JavaScript一等公民”这个说法不?布大师在给JavaScript扯OOP的时候全靠funciton,你说它能不是一等公民嘛?那咱们看看布大师是如何又对function做手脚来实现继承的。

继承

  说到继承,那得先有个继承链上的上帝,Java继承链上的上帝是Object。毫无疑问,布大师又将JavaScript的Object封为上帝,JavaScript里面的一切对象实例(引用类型)都是这个上帝的子民。有了上帝那还得有个维系上帝与子民之间关系的纽带啊。布大师又头痛了,我这里没有设计extend啊,怎么建立继承关系呢?他又将目光投到了function身上。话说function都具有创建实例的能力了,那就在它上面再加点料让它具有指向上帝的功能,那既不是解决了问题啊。于是他随意一划,在function上加了个"__proto__"用于指向当前子民的上帝。"__proto__"是加上去了,但是要不要让程序员手动去编写”function.__proto__==上帝“才建立子民与上帝的关系呢?布大师想了想,还是搞个内部自动实现吧,于是乎,你看到了每个对象都默认有个“__proto__”指向上帝Object。“__proto__”有了,同时还有个"prototype",老布看着两玩儿,好像都差不多,也是他又想,既然都看着差不多,那么内部实现就都合并一起吧,所以咱们定义的prototype成员实际上是被合并到了“__proto__”中。“__proto__”是隐形的而prototype则是开放给程序员的。  

  话说,布大师解决了子民如何找到上帝的问题的,但是这还不够。子民本身有自己的成员,上帝也有上帝的成员。我们在调用子民成员的时候,如何兼顾调用上帝成员的能力呢?那这个调用逻辑得做下处理。通过前面的封装故事,你已经知道了数据被封装到了实例对象[this],我们调用成员的时候都是找实例[this]成员,并没有向那个指向上帝的“__proto__”要数据啊。于是他做下调用逻辑的调整:先找当前实例this上是否存在被调用的成员,没有则在指向上帝的"__proto__"里查找。于是乎继承实现了。这里强调一下:在function.prototype上定义的成员实际上也是被调整到了”__proto__“中,而”__proto__“中又有”__proto__“,这样就形成了所谓的原型链。

  看到这里你应该明白了,既然“__proto__”是用于指向父类的,而prototype最终也是和”__proto__“合并一起,如果我们通过修改prototype的指向是不是就实现了对父类的继承呢。正确,这也是布大师所想的,但这么一修改就存在一个问题了,prototype本来是属于某个fuction的,修改后指向了另外的对象,于是乎,布大师又往”__proto__“加了个constructor用于指向归属的function(构造函数),以表明这个”__proto__“的归属,如果我们通过修改prototype指向父类,还得手动将其constructor指向修正回来。于是布大师的继承是这样实现的。

        /***定义一个人类构造函数,作为男人、女人的基类***/
function human(sex,name){
this.sex=sex;
this.name=name;
}
/**人都会说话* ***/
human.prototype.say=function(){
console.log("人都会说话!");
console.log("我的性别是:"+this.sex);
console.log("我的名字是:"+this.name);
}
/**
* 定义一个女人构造函数
* ***/
function lady(hobby){
this.hobby=hobby;
}
/**原型继承**/
lady.prototype=human.prototype;
/**记得修改归属**/
lady.prototype.constructor=lady;
var girl = new lady("化妆");
/**女人拥有了人类能说话的能力***/
girl.say();//结果 人都会说话! 我的性别是:undefined 我的名字是:undefined

  结果有问题啊!女人是可以说话了,但是人类的性别、名字怎么继承啊?布大师又遇到了恼火的问题:原型继承只能继承prototype上定义的成员,无法继承父类对象上的this成员?他又想点子了。还得拿function开刀。他想了想:构造函数里的this成员变量是实例私有的,this.sex、this.name只归属与human实例的,无法归属于lady实例呢?除非有个偷梁换柱的办法,运行下human函数,运行期间让里面的this换成lady的this。这样借助JavaScript的动态属性,让lady的this偷偷加上sex和name。于是乎他在function上加了两个方法:call、applay。专门干偷梁换柱的事情。上面的代码进一步改良:

	/***定义一个人类构造函数,作为男人、女人的基类***/
function human(sex,name){
this.sex=sex;
this.name=name;
}
/**人都会说话* ***/
human.prototype.say=function(){
console.log("人都会说话!");
console.log("我的性别是:"+this.sex);
console.log("我的名字是:"+this.name);
}
/**
* 定义一个女人构造函数
* ***/
function lady(hobby,sex,name){
//依赖javascript的动态属性功能,通过父类构造函数的call方法实现偷梁换柱
human.call(this,sex,name);
this.hobby=hobby;
}
/**原型继承**/
lady.prototype=human.prototype;
/**记得修改归属**/
lady.prototype.constructor=lady;
var girl = new lady("化妆","woman","pretty-mm");
/**女人拥有了人类能说话的能力***/
girl.say();//结果 人都会说话! 我的性别是:woman 我的名字是:pretty-mm

  看到这里我觉得看官你应该明白了JavaScript的是怎么玩对象创建,怎么玩继承的了。上述内容也只能说抓住要点进行故事般推理,实际详尽的知识点肯定不是这寥寥几段文字能够说清的,要不然《JavaScript高级程序设计》那书怎么能像枕头那般。楼主只是觉得理解了这些要点,再加以进一步学习,玩转JavaScript的oop肯定不在话下。这段是废话了,面向对象的三大特性,咱们只讲了封装、继承、那多态性呢?JavaScript又如何体现?

多态性

  JavaScript实现OOP三大特性,多态性是弱爆的了。布大师实现的封装、继承,虽然确实是非主流,但至少也是实现了。而多态性,布大师就搞得有点潦草了。我估计布大师当时已经有点不耐烦了,所以也没像前面封装、继承那样用心考虑设计,再说了多态性这玩儿层次较高,JavaScript完美实现多态性思想,按当时的需求似乎也有点多余。于是,布大师对多态性的实现,可以说比较简单,那下面就一起聊聊JavaScript的多态性。

  由于多态性是个高层次的思想,在说JavaScript多态性之前,楼主得先表达下自己对多态性的理解。楼主认为:多态性就是某个事物、行为呈现的多种状态,在OOP编程里面可以说到处都是多态性的体现。如一个Class可以根据不同的数据产生不同的instance,同一份Class不同的instance那是不是多种状态了?如函数的重写、重载是不是同一个函数可以产生多种不同的行为结果?这又是多态性的体现了。更甚设计模式里面的”工厂模式“,是不是一个工厂去产生不同的实例?还有面向接口编程,一个接口,N份实现......。可以说在OOP编程里面多态无处不在。楼主有时候甚至想:既然都无处不在了,多态性这玩儿还有必要拿出来谈嘛?谁要是知道这个问题的答案,欢迎给楼主答疑。废话了,咱们还是回归布大师是如何给JavaScript弄点多态性的。

  话说,多态性最经典的表现就是函数的重写、重载。重写是函数实现的覆盖、重载则是函数签名的多种形式。布大师也明白,JavaScript已经被他赋予动态成员(属性)超级能力,对于重写,通过这个超级能力毫不费劲就实现了,如上面human的say函数,如果女人们想说点女人才说的话,那得重写human的say函数,这个轻松实现:

	/***定义一个人类构造函数,作为男人、女人的基类***/
function human(sex,name){
this.sex=sex;
this.name=name;
}
/**人都会说话* ***/
human.prototype.say=function(){
console.log("人都会说话!");
console.log("我的性别是:"+this.sex);
console.log("我的名字是:"+this.name);
}
/**
* 定义一个女人构造函数
* ***/
function lady(hobby,sex,name){
//偷梁换柱
human.call(this,sex,name);
this.hobby=hobby;
}
/**原型继承**/
lady.prototype=human.prototype;
/**记得修改归属**/
lady.prototype.constructor=lady;
/***重写say,让女人们都说女人爱说的话***/
lady.prototype.say=function(){
console.log("我是女人,我当然喜欢化妆啊!");
} var girl = new lady("化妆","woman","pretty-mm");
/**女人拥有了人类能说话的能力***/
girl.say();//结果 我是女人,我当然喜欢化妆啊!

  

  得益于动态成员(属性)的超级能力,JavaScript实现重写,毫不费劲。布大师也可以偷懒一下。但是重载呢?重载呢?重载呢?布大师还是得费脑子了。大师就大师,人家实现重载也是惊艳加省事(当然他这么一搞,咱们写JS的就不省事了)。为了应对一个函数,N个参数的重载实现,布大师又拿function开到,给function加了个arguments用于存放函数的参数,不管你有多少个参数,都可以任意在这个argments里面获取,你也不需要写function(p1,p2,p3.......)这样的代码,你只需要在函数体内根据arguments的长度走不通的逻辑即可。如下

	function f(){
var args=arguments;
if(args.length==0){
console.log("当f函数没有传参的逻辑");
}else if(args.length==1){
console.log("当f函数只有一个传参的逻辑");
}else{
console.log("当f函数参数多个的逻辑");
}
}

  看,布大师是够省事了,但是苦了咱们写代码的了,要走一大段if...else...。那后来布大师还对此做改进啥的不?抱歉,好像没有了,当然也可能楼主孤陋寡闻,没有了解到的可能。各位看官要是有兴趣,可以进一步研究研究。

  写了那么多,看样子对JavaScript寻踪OOP之路也吹得差不多了。以上内容要是有何错误、纰漏,还请客观斧正!