自我学习——javascript——面向对象程序设计
1.构造函数只是个普通函数,如果不通过new 使用,就是一个普通的函数
直接看示例代码:
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = function() { alert(this.name); } } // 当做构造函数使用 var person = new Person("xiaosi", 29, "Software Engineer"); person.sayName(); // 当做普通函数调用 Person("xiaosi2", 27, "Doctor"); window.sayName(); // 在另一个作用域中调用 var o = new Object(); Person.call(o, "xiaosi3", 22, "第三个职业"); o.sayName();
执行之后浏览器会依次显示出3个名字,所以可以看做构造函数只是普通函数作用域不一样而已,实际上构造函数做的事情也只是先将作用域赋给新的对象(所以this指向的就是这个对象)然后执行函数,如果不使用 new 关键字,构造函数就是一个首字母大写的普通函数
2.实现类继承的一种方式
直接上例子吧,需要说明的都在注释里面
function PeopleClass(){ this.type="人"; } PeopleClass.prototype.getType=function(){ alert("这是一个人"); } function StudentClass(name,sex){ PeopleClass.apply(this,arguments); //声明对象的时候使用apple指定作用域,这样就实现了属性的继承,因为新建的对象也会执行这个“伪构造函数”,会实现属性的初始化 var grop; for(grop in PeopleClass.prototype){ var proto=this.constructor.prototype; //proto指的就是StudentClass的原型链 console.log(proto); if(!proto[grop]){ //判断自己类的名字有没有和父类的重名,如果有的话不执行任何操作,就是用自己的方法覆盖了父类的方法 proto[grop]=PeopleClass.prototype[grop]; //新建自己的原型链上的方法,并且和父类同名,实现方法的继承 console.log(proto[grop]); // console.log(PeopleClass.prototype[proto]); } proto[grop]["super"]=PeopleClass.prototype; //将继承了的方法名内实现一个super属性,指向父类的原型链 console.log(proto[grop].super); } this.name=name; //注意,这里最后才实例化对象的属性是为了当有重名的属性的时候覆盖掉父类的属性 this.sex=sex; } var stu=new StudentClass("小明","男"); alert(stu.name+":"+stu.sex+":"+stu.type); stu.getType();
这里面实现了基本继承需要注意的事情,这样可以基本上实现从一个子类从父类继承属性和方法,并且在有同名方法和属性的时候覆盖(但是这种方式有个严重问题,不能调用父类的同名函数,做复杂项目的时候实用性不佳)
3.prototype是存在于构造函数之中的,而不是在实例对象中
直接上例子
Person.prototype.name="xiaosi"; Person.prototype.age=20; Person.prototype.job="实习"; Person.prototype.sayName=function(){ alert(this.name); } var person1=new Person(); var person2=new Person(); person1.name="xiaosi1"; // person1.prototype.name="xiaosi2"; //在这里就会报错了,因为新实例化的对象是没有prototype属性的,要函数对象才会有, alert(person1.name); //xiaosi1p person1.__proto__.name="xiaosi2" //通过这种模式可以在现代浏览器里面访问到原型链 alert(person2.name); //xiaosi2
在这里,如果从实例对象中访问prototype的话会报错,因为prototype是构造函数的一个属性(注意,构造函数是一个Function对象的实例),而实例对象的话是在创建的时候包含一个指针
[[prototype]],这个指针指向构造函数的原型对象,在现代浏览器内可以通过 __proto__访问,要明确的一点是,这个连接时存在于实例与构造函数的原型对象之间的,不是存在于实例和构造函数之间的。
4.delete 操作符可以用来重建连接
直接上例子:
function Person(){} Person.prototype.name="xiaosi"; Person.prototype.age=20; Person.prototype.job="实习"; Person.prototype.sayName=function(){ alert(this.name); } var person1=new Person(); var person2=new Person(); alert("No1:"+ person1.name ); //No1:xiasoi person1.name="xiaosi2 "; alert("No2:"+person1.name); //No2:xiaosi2 person1.name=null; alert("No3:"+person1.name); //No3:null delete person1.name; alert("No4:"+person1.name); //No4:xiaosi
最后一种方式就是delete实例对象的属性用来重建prototype连接的作用的,因为一旦本身的属性和原型中的属性重名的时候就会直接阻止对原型中的访问(因为访问顺序就是先访问实例对象中的属性),即使值为null,访问到的值还是null。(ps:delete操作符是用来删除对象属性和数组元素的,删除其他东西的时候严格模式下会报错的)
5.constructor属性值指向改变
为了方便和看起来更具有包装感觉,
我们经常:
function Person(){}; Person.prototype={ name:"xiaosi", age:20, job:"实习", sayName:function(){ alert(this.name); } }; var xiaosi1=new Person(); console.log(xiaosi1.constructor); //function Object() { [native code] }
这样子会导致constructor不会指向构造函数,而是指向object,因为我们重写了prototype属性值,而constructor是在构造函数创建的时候自动随prototype一起创建的,重写了prototype就导致constructor也改变了。
原始写法:
function Person(){}; Person.prototype.name="xiaosi"; Person.prototype.age=20; Person.prototype.job="实习"; Person.prototype.sayName=function(){ alert(this.name); } var xiaosi1=new Person(); console.log(xiaosi1.constructor) ; //function Person(){}
这样是自动创建prototype的
解决办法(重写constructor属性):
function Person(){}; Person.prototype={ constructor:Person, name:"xiaosi", age:20, job:"实习", sayName:function(){ alert(this.name); } }; var xiaosi1=new Person(); console.log(xiaosi1.constructor); //function Person(){}
这样子我们手动指定constructor制定方式就可以了
导致的问题(prototype指向的是一个新的对象,不是创建时的指针):
function Person(){}; var xiaosi1=new Person(); Person.prototype={ constructor:Person, name:"xiaosi", age:20, job:"实习", sayName:function(){ alert(this.name); } }; console.log(xiaosi1.constructor); //function Person(){} xiaosi1.sayName(); //报错:Object #<Person> has no method 'sayName'
最后一行代码会报错,而报错的原因就是因为实例化了xiaosi1对象的时候[[prototype]]指向的是Person.prototype(初始值),但是我们后面又重写了prototype(创建了一个新的对象),[[prototype]]指针指向的位置就是这个新的对象 Person.prototype(新值),所以我们后面调用
xiaosi1.sayName();的时候会报错,因为原来的那个指针指向的对象里没有这个sayName()方法
其实每个对象都会有自己的constructor属性,表面上看起来是指向构造函数(实际上是指向prototype.constructor,但是因为prototype.constructor也是指向构造函数)prototype.constructor更改直接导致constructor属性值被更改。在多级原型链的继承下,不手动重写constructor会导致所用constructor指向顶级的构造函数,上例子:
function A(name) { this.name = name; }; function B(age) { this.age = age; } function C(sex) { this.sex = sex; } B.prototype = new C('男'); A.prototype = new B(20); var a = new A('xiaosi'); var b = new B(21); var c = new C('女'); console.log(A.prototype.constructor); //function C(sex){this.sex=sex;} console.log(a.constructor); //function C(sex){this.sex=sex;} console.log(B.prototype.constructor); //function C(sex){this.sex=sex;} console.log(b.constructor); //function C(sex){this.sex=sex;} console.log(C.prototype.constructor); //function C(sex){this.sex=sex;} console.log(c.constructor); //function C(sex){this.sex=sex;} A.prototype.constructor = '2'; console.log(a.constructor); //2 说明更改A.prototype.constructor可以直接影响到a.constructor a.constructor = '1'; console.log(A.prototype.constructor); //2 说明更改a.constructor对A.prototype.constructor没什么影响
我们看到,所有的constructor都是指向C的构造函数,而原因就是对象a和对象b的prototype指向的是一个新的对象而这个对象的constructor属性指向的又是本身构造函数的prototype.constructor属性,恩,如果可以看懂的话就是这样了
a.constructor==(A.prototype).constructor-->(new B()).constructor==(B.prototype).constructor-->(new C()).constructor==C.prototype.constructor-->C构造函数
重点就在于如果我们没有重写constructor属性,那么A.prototype是等于一个B的对象实例(A.prototype==new B()),而这个B的实例有本身就有一个constructor属性,这个属性又会等于(B.prototype).constructor,然后我们就很愉快的一直延伸到顶部,因为顶部的constructor是指向自己的(这是初始化这个构造函数就赋值好的,所以记得如果改了prototype改了要重写constructor,不然一条链都废了)
6.原型链模式实现javascript的面向对象继承(一般不会单独使用)
如何实现面向对象的继承呢?之前我们提到过prototype这个对象,利用这个对象,我们有构造函数,原型,实例,如果有一个对象的原型指向另一个对象的实例的话会怎么样呢?
初始设计模式:
function SuperType() { //“父类”的构造函数, this.property = true; } SuperType.prototype.getSuperValue = function() { //在父类的原型里设定一个方法 return this.property; } function SubType() { //“子类”的构造函数 this.subproperty = false; } SubType.prototype = new SuperType(); //关键:将父类的一个实例给子类的原型,这样子的话子类原原型内就有了父类的属性和方法,还有一个指向父类的[[prototype]](注意是指向原型,不是指向构造函数) SubType.prototype.getSubValue = function() { //给子类的原型上添加方法 return this.subproperty; } var instance = new SubType(); var instance1 = new SuperType(); alert(instance.getSuperValue()); //true 实现了从父类继承方法的目的
这样子就初步实现了继承父类的属性和方法的目的了,顺带一提,构造函数默认情况下 Fun.prototype.__proto__ 指向的都是 Object{},相当于说
Fun.prototype=new Object(); 这就是为什么对象都会有toString,valueOf等方法的根本原因
会引起的问题:prototype覆盖的问题
function SuperType() { //“父类”的构造函数, this.property = true; } SuperType.prototype.getSuperValue = function() { //在父类的原型里设定一个方法 return this.property; } function SubType() { //“子类”的构造函数 this.subproperty = false; } SubType.prototype = new SuperType(); //关键:将父类的一个实例给子类的原型,这样子的话子类原原型内就有了父类的属性和方法,还有一个指向父类的[[prototype]](注意是指向原型,不是指向构造函数) SubType.prototype.getSubValue=function(){ return this.subproperty; } SubType.prototype.getSuperValue=function(){ return false; } SubType.prototype.getSubValue = function() { //给子类的原型上添加方法 return this.subproperty; } var instance = new SubType(); var instance1 = new SuperType(); alert(instance.getSuperValue()); //false 子类的同名方法被重写了 alert(instance1.getSuperValue()); //true 父类的方法并没有被重写
父类的方法不会重写,这点需要注意(原因是因为这个方法重写实际上是子类prototype上有何父类同名的方法,阻止对父类方法的访问)
另一个问题就是:如果你覆盖的语句写在原型链赋值之前的话,这段赋值语句是没有效果的
function SuperType() { //“父类”的构造函数, this.property = true; } SuperType.prototype.getSuperValue = function() { //在父类的原型里设定一个方法 return this.property; } function SubType() { //“子类”的构造函数 this.subproperty = false; } SubType.prototype.getSubValue=function(){ return this.subproperty; } SubType.prototype.getSuperValue=function(){ return false; } SubType.prototype = new SuperType(); //关键:将父类的一个实例给子类的原型,这样子的话子类原原型内就有了父类的属性和方法,还有一个指向父类的[[prototype]](注意是指向原型,不是指向构造函数) SubType.prototype.getSubValue = function() { //给子类的原型上添加方法 return this.subproperty; } var instance = new SubType(); var instance1 = new SuperType(); alert(instance.getSuperValue()); //true 子类重写方法失去效果 alert(instance1.getSuperValue()); //true 父类的方法并没有被重写
因为后面原型链赋值的时候直接把prototype重写了一遍,前面的赋值自然就失效了,同样的原理,我们也不能使用对象字面量的方式重写父类方法,一样会把之前的prototype给覆盖调,然后会导致没有被重写的方法是不存在的,调用的话会报错
更大的问题:会导致父类的属性也是共享的(因为从一个对象属性变成一个prototype的属性,会导致共享),而且不能在不影响其他类的情况下给父类传递参数
7.更改继承关系(最好别这么做)
java和javascript有很大的差异,比如说javascript可以更改继承关系(因为是原型链模式,而不是类模式)
function class1() { this.pro=1 }; function class2() { this.pro2= 2 }; class2.prototype = new class1(); function class3() { this.pro=3 }; class3.prototype = new class2(); function class4() { this.pro=4 }; var obj = new class3(); class2.prototype = new class4(); //更改class2的原型链,但是对obj不会造成什么影响,因为原型链上的是实例对象 alert(obj instanceof class3); //true; alert(obj instanceof class2); //false; alert(obj instanceof class1); //true
我们看到,更改class2的原型链会导致 obj 原型链上没有class2 但是却保留了class1,而且实际上obj的属性和值都不会改变,因为继承的是对象(很重要),不是类继承,在继承的时候用的原型链,prototype是等于一个对象的实例,而不是这个对象的原型!!
8.模拟类式面向对象语言继承(解决不能调用父类同名方法的问题)
恩,好吧,我是写不出那么优秀的代码,直接上jquery之父的代码:Simple JavaScript Inheritance
1 /* Simple JavaScript Inheritance 2 * By John Resig http://ejohn.org/ 3 * MIT Licensed. 4 */ 5 // Inspired by base2 and Prototype 6 (function(){ 7 var initializing = false, fnTest = /xyz/.test(function(){xyz;}) ? /\b_super\b/ : /.*/; 8 9 // The base Class implementation (does nothing) 10 this.Class = function(){}; 11 12 // Create a new Class that inherits from this class 13 Class.extend = function(prop) { 14 var _super = this.prototype; 15 16 // Instantiate a base class (but only create the instance, 17 // don't run the init constructor) 18 initializing = true; 19 var prototype = new this(); 20 initializing = false; 21 22 // Copy the properties over onto the new prototype 23 for (var name in prop) { 24 // Check if we're overwriting an existing function 25 prototype[name] = typeof prop[name] == "function" && 26 typeof _super[name] == "function" && fnTest.test(prop[name]) ? 27 (function(name, fn){ 28 return function() { 29 var tmp = this._super; 30 31 // Add a new ._super() method that is the same method 32 // but on the super-class 33 this._super = _super[name]; 34 35 // The method only need to be bound temporarily, so we 36 // remove it when we're done executing 37 var ret = fn.apply(this, arguments); 38 this._super = tmp; 39 40 return ret; 41 }; 42 })(name, prop[name]) : 43 prop[name]; 44 } 45 46 // The dummy class constructor 47 function Class() { 48 // All construction is actually done in the init method 49 if ( !initializing && this.init ) 50 this.init.apply(this, arguments); 51 } 52 53 // Populate our constructed prototype object 54 Class.prototype = prototype; 55 56 // Enforce the constructor to be what we expect 57 Class.prototype.constructor = Class; 58 59 // And make this class extendable 60 Class.extend = arguments.callee; 61 62 return Class; 63 }; 64 })();
这段代码很奇妙,我们先来看看这个方法是怎么使用的:
1 var ClassParent=Class.extend({ //包装ClassParent构造函数为模拟类 2 init:function(name){ 3 this.name=name; 4 }, 5 sayName:function(){ 6 alert(this.name); 7 } 8 }); 9 10 var ClassSon= ClassParent.extend({ //在这里实现继承关系,并且包装ClassSon构造函数为模拟类 11 init:function(name,age){ 12 this._super(name); //在这里调用父类的同名构造函数方法,传递的参数为字方法的 name 13 this.age=age; 14 }, 15 sayName:function(){ 16 this._super(); //在这里调用父类的同名方法 17 } 18 }); 19 20 var objSon=new ClassSon("xiaosi",20); 21 22 objSon.sayName();
恩,看到这里,可能有点糊涂——为什么extend方法好似不是用来继承的,因为如果是继承的话,那么parent明显是父类,不需要再从其他地方继承什么。
所以这么方法实际的用途可以这么理解:实现把构造函数改装成和类继承相似的模式,实现能够调用父类同名函数这个功能(原型链模式不能实现调用父类同名方法,同名方法会被禁止访问)
所以现在我们就能理解,为什么extend看起来不像是java里面的继承,因为extend这个方法首要的目标是改装构造函数,使其脱离原型链继承模式的限制,然后才是父类通过调用这extend方法返回一个被“继承”过的子类,实际上依旧是构造函数的改变。其实现能够继承的核心就在于——构造函数被改完之后,调用父类同名方法时,其实是调用一个被更改过的函数,并不是直接调用父类的函数(实际上并没有实现直接对父类的函数调用,因为原型链上是不允许访问同名函数的,而是创建了一个和父类一样的函数返回调用,但是给用户的感官确是直接调用)
ps:如果想知道详细的解释,请看三生石上大大的博客:
http://www.cnblogs.com/sanshi/archive/2009/07/14/1523523.html