JavaScript是一门极其灵活的语言,烂七八糟的设计是它最大的优点。不同于其他严格类型的语言例如java,学习曲线比较友好。JavaScript个人感觉上手基本不用费劲,要想上高度那就是一个悲催而且毁三观的故事。特别是有面向对象语言基础的人来说,JavaScript真像一个噩梦。JavaScript更加的零碎,封装的不是很好。你必须理清脉络深入理解了,才能写出来高大上的优雅的代码。在下尽量的用简练易懂的语言,简单的阐述一下我对JavaScript面向对象的一点粗浅的理解。
1,要想面向对象先得创建对象
a,原始模式
var object = new Object(); object.name="huazi"; object.age="22"; object.sayHi = function(){ console.log("hi"+this.name); } object.sayHi(); //hi huazi
首先通过一个名称为object的对象,为其添加name和age属性以及一个sayHi的方法。this.name将会被解析成object.name。这种方式的缺点显而易见:使用同一个接口(Object)创建对象,产生大量的冗余代码。所以一般不会这么使用。
b,工厂模式
function createObject(name,age){ var obj = new Object(); obj.name=name; obj.age=age; obj.sayHi=function(){ console.log("hi "+this.name); } return obj; } var obj = createObject("huazi",22); console.log(obj.sayHi(); //hi huazi
此种方式创建对象叫做工厂模式,你给我属性,我给你个包含这些属性和方法的对象.这种模式创建的对象也有很大的问题:得到的对象属于什么类型的是不确定的.instanceof发现只能匹配到Object,不能匹配到createObject。
c,构造器模式
function createObject(name,age){ this.name=name; this.age=age; this.sayHi=function(){ console.log("hi "+this.name); } } var obj = new createObject("huazi",22); obj.sayHi(); //hi huazi obj instanceof createObject;//true
构造器模式创建对象的过程分为四步:1,创建一个Object类型对象.2,将执行环境交给这个对象(this指向).3,执行构造很熟.4,返回对象.此种方式解决了类型不确定的问题.但是缺点是,在每次创建对象的过程中,都会重新创建类如sayHi的对象.而这明显是不必要的.修改如下:
function createObject(name,age){ this.name=name; this.age=age; this.sayHi=sayHi; } function sayHi(){ console.log("hi "+this.name); } var obj = new createObject("huazi",22); obj.sayHi(); //hi huazi obj instanceof createObject;//true
很简单,只需要把函数放到全局作用域即可。可是问题又来了,全局作用域中的函数只能被某一个对象调用。这在逻辑上实在有点牵强。更严重的情况是,往往我们需要定义很多方法来实现一个对象。所以就会出现大量的全局函数,并且全局函数不能在其他对象上使用。
d,原型模式
function createObject(){}; createObject.prototype.name = "huazi"; createObject.prototype.age=22; createObject.prototype.sayHi=function(){ console.log("hi "+this.name); } var obj1 = new createObject(); var obj2 = new createObject(); obj1.sayHi(); //hi huazi obj1.sayHi===obj2.sayHi; //true
好了,现在好像解决了刚才的问题,把属性和方法都加到了原型中。这样就不会出现全局属性和重复函数对象了。这种模式的缺点也显而易见:构造不能传参,也就是说所有对象将长的一模一样。还有就是内存共享的问题。属性要是引用类型比如Array那么就热闹了。牵一发动全身。
e,组合模式(构造+原型)
function createObject(name,age){ this.name=name; this.age=age; this.array = [1,2,3]; } createObject.prototype.sayHi = function(){ console.log("hi "+this.name); } var obj1 = new createObject("huazi",22); var obj2 = new createObject("huazi",22); obj1.sayHi(); //hi huazi obj1.array===obj2.array;//false
除了此种写法之外,在给原型加方法的时候还可以使用字面量的方式添加。但是需要注意的是使用字面量添加等于重写了prototype了,所以需要显示的申明,constructor的指向。
createObject.prototype={ constructor:createObject, sayHi:function(){ console.log("hi "+this.name); } }
还有需要注意的一点是,在使用字面量之前不能创建对象。前面说过此种方式等于是重写了prototype。所以之前创建的对象实例,不会更新新的属性和方法。为啥?自行脑补一下。
f,动态原型模式
function createObject(name,age){ this.name=name; this.age=age; if(typeof this.sayHi != "function"){ //不存在的情况下添加方法 createObject.prototype.sayHi = function(){ console.log("hi "+this.name); } } } var obj1 = new createObject("huazi",22); var obj2 = new createObject("saint",22); obj1.sayHi(); //hi huazi obj2.sayHi(); //hi saint
这种方式就算比较完美的了。但是还还需要注意,在构造内部不能使用字面量的方式去添加原型属性。回溯到构造创建对象过程,我们知道第一步就已经创建好对象了。所以使用字面量也会发生意外的事情。
g,寄生构造模式和稳妥模式
寄生构造模式的思路是:创建一个函数,这个函数的职责就是封装一个对象所需要的所有属性和方法,然后返回对象。从样子上来看,此种方式几乎与工厂模式无异。只是早创建对象的时候方式有些变化。就是使用new关键字来创建。
function createObject(name,age){ var obj = new Object(); obj.name=name; obj.age=age; obj.sayHi=function(){ console.log("hi "+this.name); } return obj; } var o = new createObject("huazi",22); o.sayHi();
再回忆一下构造模式创建对象的最后一步,返回对象。寄生模式其实是覆盖了返回对象的那一步。同时此种模式也没有摆脱类型不能确定的问题。那么此种模式在什么时候可以用到呢?
function createObject(){ var array = new Array(); //Array构造中创建对象是使用push的 array.push.apply(array,arguments); //添加新的方法 array.toPipeString=function(){ return this.join("|"); } return array; } var o = new createObject("huazi",22); o.toPipeString(); //huazi|22
为Array添加了一个方法,同时没有修改Array的构造和原型。实际上就还对Array的二次包装,添加新的方法。这种方式比较靠谱。安全性也比较好,够封闭。
还有一种非常安全的创建对象模式叫做稳妥模式。与寄生模式非常的类似
function createObject(name,age){ var obj = new Object(); obj.sayHi=function(){ console.log("hi "+name); } return obj; } var o = new createObject("huazi",22); o.sayHi(); //hi huazi
可以发现,name值只能在sayHi中访问,在构造中包装的数据是绝对安全可靠的。这就有点private的意思了。sayHi是暴露给外部的接口。和寄生模式一样采用稳妥模式创建的对象,类型是无法确定的。
2,原型
无论什么时候只要是创建了一个新函数,随之就会根据一组特定的规则为该函数创建一个prototype属性。在默认情况下prototype属性会自动获得一个constructor(构造函数)属性,这个函数非常的特殊。包含一个指向prototype属相所在函数的指针。
function createObject(name,age){} var o = new createObject("huazi",22); console.log(createObject.prototype.constructor);//function createObject(name,age) {} console.log(createObject.prototype)//createObject(name,age) {} //判断对象o是否是原型对象的派生。 console.log(createObject.prototype.isPrototypeOf(o));//true
实际上是这样的。每个对象都会有一个_proto_属性,这个属性指向的是函数原型createObject.prototype。而crateObject.prototype中存在一个constructor属性,此属性指向了createObject构造函数。等于指来指去,指出了一个回路。isPrototypeOf函数的参数是函数对象实例,作用是判断该实例的归属,是否是该原型对象的派生。使用hasOwnProperty()可以检测一个方法是存在于原型中还是存在于实例中,当然前提是确定可以访问这个方法或者这个方法存在才能确定。使用delete操作符可以删除掉实例中的方法或者属性。在原型中的属性方法默认是不能被delete的。还有一个坑就是delete不存在的属性或者方法也会返回true所以在使用delete的时候需要小心一些。
在给prototype添加属性之前创建了一个对象,那么这个对象是否可以引用新添加的原型上的属性呢?答案是可以的。在访问属性的时候首先会查看实例对象是否存在此属性,不然就去原型找。而_proto_就是指向原型对象的。没错是指向,所以不管何时更新原型属性都是ok的。
3,继承
ECMAScript中描述了原型链的概念,并说明原型链将作为实现继承的主要方法。原型链顾名思义,就是讲原型链接起来实现继承。子类的prototype指向父类的实例对象就是很简单的一种实现。
function superClass(){ this.name="huazi"; } superClass.prototype.getName=function(){ console.log(this.name); } function childClass(){ this.name="bob"; } childClass.prototype = new superClass(); childClass.prototype.getName = function(){ console.log(this.name+"v"); } var instance = new childClass(); //重写了父类方法和属性 instance.getName(); //bobv console.log(instance instanceof Object); //true console.log(instance instanceof childClass);//true console.log(instance instanceof superClass);//true console.log(Object.prototype.isPrototypeOf(instance));//true console.log(superClass.prototype.isPrototypeOf(instance));//true console.log(childClass.prototype.isPrototypeOf(instance));//true
和其他的情况一样,一旦使用到prototype,就不建议使用字面量的方式为原型添加属性了。脑补即可。
同时此种不加区分的继承方式,任然保留内存共享的问题(引用类型属性的问题)。同原型模式创建对象的问题是一样的。当然解决这个问题的办法和解决创建对象时的方法也是一样的,那就是混合使用构造和原型来实现继承。
function superClass(name){ this.name=name; this.number=[1,2,4]; } superClass.prototype.getName=function(){ console.log(this.name); } function childClass(name,age){ //继承属性 superClass.call(this,name); this.age=age; } childClass.prototype = new superClass(); childClass.prototype.getName = function(){ console.log(this.name+"v"); } var instance = new childClass("huazi",22); //重写了父类方法和属性 instance.getName(); //huaziv instance.number.push("1"); instance.number===new childClass().number; //false instance.number//[1,2,4,'1']
此种方法的原则就是属性靠构造,方法靠原型。可以简单地这么理解一下。此种模式也有个问题就是效率没有达到极致,因为多次调用了父类构造。还有一个让人不舒服的地方就是原型和实例上都包括有name和age属性。这是不能接受的。
最后放一个大招,叫做寄生组合模式。其实也就是避免了上面这个方式的缺点,即绕过父类构造来继承父类原型。
/** * 1,找到父类原型对象 * 2,修改构造的指向 * 3,父类原型 */ function inheritPrototype(superClass,childClass){ var _prototype = new Object(superClass.prototype); _prototype.constructor = childClass; childClass.prototype=_prototype; } function superClass(name){ this.name=name; this.number=[1,2,4]; } superClass.prototype.getName=function(){ console.log(this.name); } function childClass(name,age){ //继承属性 superClass.call(this,name); this.age=age; } //不同点就再这 inheritPrototype(superClass,childClass); childClass.prototype.getName = function(){ console.log(this.name+"v"); } var instance = new childClass("huazi",22); //重写了父类方法和属性 instance.getName(); //huaziv instance.number.push("1"); instance.number===new childClass().number; //false instance.number//[1,2,4,'1']
恶心死我了,终于写完了!