A-A+

javascript中面向对象知识详细

2016年01月14日 前端设计 暂无评论 阅读 7 views 次

本文章介绍了关于面向对象基础知识总结,有需要了解的同学可参考一下。

面向对象,JS把所有的对象放到Object类型中,这样,JS就有6种用户可使用的数据类型。除了Undefined,JS为所有的类型提供了字面值(literal)语法,现在来看,JS的Object字面值表示设计的相当成功,现在甚至成为了一种数据交换的格式,这就是大家所熟悉的JSON。A Sample:

var aTShirt={color:"yellow",size:"big"} 作为动态语言,JS允许使用者对一个已经创建的对象添加或者删除属性。对一个不存在的属性赋值即向其添加属性,delete关键字被用于删除属性。这个delete比较容易跟C++的delete运算符混淆,后者是用来释放不再使用的对象的。

本来有了这些语法,已经可以做基本的面向对象编程了,但是仅仅如此,JS代码复用性比其它语言弱太多。比如,你甚至无法为一组对象做一个统一的操作,必须通过循环遍历来实现,于是JS引入了原型(prototype),具体的实现方式是为每个对象规定一个私有属性[[prototype]],当读取一个对象的属性时,如果对象本身没有这个属性,会尝试访问[[prototype]]的相应属性。具体实现中,[[prototype]]所指向的对象仍然可以有[[prototype]],实际的访问就是一个链式的操作,直到找到这个属性或者[[prototype]]为空为止,所以常常听到[[prototype]]链的说法。为了防止[[prototype]]出现循环,JS引擎会在任何对象的[[prototype]]属性被修改时检查。

按照标准,这个[[prototype]]语言使用者是无法访问的,不过FireFox的JS引擎把[[prototype]]暴露出来,作为公有属性"__proto__",这样,我们就可以通过操作原型对象来控制一组对象的行为。我们可以借用FF提供的便利来了解一下[[prototype]]的工作原理,代码如下:

  1. var proto={a:1};  
  2. var m={__proto__:proto};  
  3. var n={__proto__:proto};  
  4. alert([m.a,n.a]);  
  5. proto.a=2;  
  6. alert([m.a,n.a]);  

JS规定了一个内建对象作为所有对象的最终[[prototype]],也就是说即使用{}创建的对象,也会有[[prototype]]指向这个内建对象。

通过这个机制,我们完全可以得到跟基于类的语言相当程度的对象复用能力——但是当然我们还需要函数。在JS中,函数仅仅是一种特殊的对象,JS设计了()运算符和function关键字让JS的函数看起来更像是传统的语言。只要实现了私有方法[[call]]的对象都被认为是函数对象(这个[[call]]跟大家比较熟悉的Function.prototype.call完全是两回事),类似[[prototype]],[[call]]也是语言使用者完全无法访问的,这一次FF也没有为我们提供公有属性来替代。

本来到这里为止,JS的面向对象已经很完整了,但是JS为了让自己的语法看起来更像是Java之类的语言,又引入了new关键字,在上面大部分语言中new都是针对类来做的,而JS没有类,甚至没有声明域,所以这个new还是要在对象上做文章,new会调用私有方法[[contruct]],任何实现了[[construct]]的对象都可以被new接受。然而如何才能让一个对象可以被new呢?JS并没有额外提供构造这种对象方法,所以所有通过function关键字构造的函数对象被设计成实现了[[construct]]方法。这也就是JS的new很奇怪地针对函数的原因。值得一提的是,并非只有函数可以被new,JS的宿主环境可能提供一些其它对象,典型的例子是IE中的ActiveXObject。
所有函数的[[construct]]方法都是类似的:创建一个新的对象,将它的[[prototype]]设为函数对象的共有属性prototype,以新对象做为this指针的值,执行函数对象.

这样对同一函数的new运算实际上创建了相似的对象:拥有共同的原型[[prototype]],被同一函数处理过。这样的new运算就很类似Class了,同时由于JS的动态性,所有的"类"在运行时任你宰割,想要模拟继承之类的行为也就很容易了,由于是弱类型且是动态函数,不存在需要多态的问题,JS完全可以做到基于类的面向对象.

一、封装

对象定义:ECMA-262把对象定义为:“无序属性的集合,其中属性可以包括基本值、对象或者函数”。

创建对象:每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型(Object, Array, Date, RegExp, Function, Boolean, Number, String),也可以是自定义类型。

1、构造函数模式,代码如下:

  1. function Person(name, age) {  
  2.     this.name = name;  
  3.     this.age = age;  
  4.     this.sayName = function() {  
  5.         alert(this.name);  
  6.     }  
  7. }  

通过以上构造函数使用new操作符可以创建对象实例,代码如下:

  1. var zhangsan = new Person('zhangsan', 20);  
  2. var lisi = new Person('lisi', 20);  
  3. zhangsan.sayName();//zhangsan  
  4. lisi.sayName();    //lisi通过new创建对象经历4个步骤  

1、创建一个新对象;[var o = new Object();]

2、将构造函数的作用域赋给新对象(因此this指向了这个新对象);[Person.apply(o)] [Person原来的this指向的是window]

3、执行构造函数中的代码(为这个新对象添加属性);

4、返回新对象。

通过代码还原new的步骤,代码如下:

  1. function createPerson(P) {  
  2.     var o = new Object();  
  3.     var args = Array.prototype.slice.call(arguments, 1);  
  4.     o.__proto__ = P.prototype;  
  5.     P.prototype.constructor = P;  
  6.     P.apply(o, args);  
  7. }  

测试新的创建实例方法,代码如下:

var wangwu = createPerson(Person, 'wangwu', 20);

wangwu.sayName();//wangwu2、原型模式

原型对象概念:无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。而通过这个构造函数,可以继续为原型对象添加其他属性和方法。创建了自定义的构造函数后,其原型对象默认只会取得 constructor 属性;至于其他方法,则都从 Object 继承而来。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版管这个指针叫 [[Prototype]] 。脚本中没有标准的方式访问 [[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个属性__proto__;而在其他实现中,这个属性对脚本是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于示例和构造函数的原型对象之间,而不是存在于实例和构造函数之间。

这段话基本概述了构造函数、原型、示例之间的关系,下图表示更清晰,代码如下:

  1. function Person(name, age) {  
  2.     this.name = name;  
  3.     this.age = age;  
  4. }  
  5. Person.prototype.country = 'chinese';  
  6. Person.prototype.sayCountry = function() {  
  7.     alert(this.country);  
  8. }  
  9.    
  10. var zhangsan = new Person('zhangsan', 20);  
  11. var lisi = new Person('lisi', 20);  
  12.    
  13. zhangsan.sayCountry();    //chinese  
  14. lisi.sayCountry();        //chinese  

alert(zhangsan.sayCountry == lisi.sayCountry); //true注意地方:构造函数的原型对象,主要用途是让多个对象实例共享它所包含的属性和方法。但这也是容易发生问题的地方,如果原型对象中包含引用类型,那么应引用类型存的是指针,所以会造成值共享。如下:

  1. Person.prototype.friends = ['wangwu']; //Person添加一个数组类型  
  2. zhangsan.friends.push('zhaoliu');  //张三修改会对李四造成影响  
  3. alert(zhangsan.friends);  //wangwu,zhaoliu  
  4. alert(lisi.friends); //wangwu,zhaoliu李四也多了个 3、组合使用构造函数模式和原型模式  

这种模式是使用最广泛、认同度最高的一种创建自定义类型的方式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。这样,每个实例都有自己的一份实例属性的副本,同时有共享着对方法的引用,最大限度的节省了内存。

原型模式改造后的如下:

  1. function Person(name, age) {  
  2.     this.name = name;  
  3.     this.age = age;  
  4.     this.friends = ['wangwu'];  
  5. }  
  6.    
  7. Person.prototype.country = 'chinese';  
  8. Person.prototype.sayCountry = function() {  
  9.     alert(this.country);  
  10. }  
  11.    
  12. var zhangsan = new Person('zhangsan', 20);  
  13. var lisi = new Person('lisi', 20);  
  14.    
  15. zhangsan.friends.push('zhaoliu');  
  16. alert(zhangsan.friends); //wangwu,zhaoliu  
  17. alert(lisi.friends); //wangwu  

二、继承

继承基本概念

ECMAScript主要依靠原型链来实现继承(也可以通过拷贝属性继承)。

原型链基本思想是,利用原型让一个引用类型继承另外一个引用类型的属性和方法。构造函数、原型、示例的关系是:每个构造函数都有一个原型对象,原型对象都包含了一个指向构造函数的指针,而实例都包含了一个指向原型的内部指针。所以,通过过让原型对象等于另外一个类型的实例,此时原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含这一个指向另一个构造函数的指针。假如另外一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例和原型的链条。这就是原型链的基本概念。

读起来比较绕,不容易理解。直接通过实例说明验证。

1、原型链继承,代码如下:

  1. function Parent() {  
  2.     this.pname = 'parent';  
  3. }  
  4. Parent.prototype.getParentName = function() {  
  5.     return this.pname;  
  6. }  
  7.    
  8. function Child() {  
  9.     this.cname = 'child';  
  10. }  
  11. //子构造函数原型设置为父构造函数的实例,形成原型链,让Child拥有getParentName方法   
  12. Child.prototype = new Parent();   
  13. Child.prototype.getChildName = function() {  
  14.     return this.cname;  
  15. }  
  16.    
  17. var c = new Child();  
  18. alert(c.getParentName()); //parent  

原型链的问题,如果父类中包括了引用类型,通过Child.prototype = new Parent()会把父类中的引用类型带到子类的原型中,而引用类型值的原型属性会被所有实例共享。问题就回到了[一、2]节了。

2、组合继承-最常用继承方式

组合继承(combination inheritance),是将原型链和借用构造函数(apply, call)的技术组合到一块。思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样既可以在原型上定义方法实现了函数的复用,又能保证每个实例都有它自己的属性,代码如下:

  1. function Parent(name) {  
  2.     this.name = name;  
  3.     this.colors = ['red', 'yellow'];  
  4. }  
  5. Parent.prototype.sayName = function() {  
  6.     alert(this.name);  
  7. }  
  8.    
  9. function Child(name, age) {  
  10.     Parent.call(this, name); //第二次调用Parent()   
  11.     this.age = age;  
  12. }  
  13.    
  14. Child.prototype = new Parent(); //第一次调用Parent(),父类的属性会  
  15. Child.prototype.sayAge = function() {  
  16.     alert(this.age);  
  17. }  
  18.    
  19. var c1 = new Child('zhangsan', 20);  
  20. var c2 = new Child('lisi', 21);  
  21. c1.colors.push('blue');   
  22. alert(c1.colors);    //red,yellow,blue  
  23. c1.sayName();    //zhangsan  
  24. c1.sayAge();    //20  
  25.    
  26. alert(c2.colors); //red,yellow  
  27. c2.sayName(); //lisi  
  28. c2.sayAge(); //21  

组合继承的问题是,每次都会调用两次超类型构造函数:第一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。这样就会造成属性的重写 ,子类型构造函数中包含了父类的属性,而且子类的原型对象中也包含了父类的属性。

3、寄生组合继承-最完美继承方式

所谓寄生组合继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。 其背后的基本思路是:不必为了指定子类的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本,代码如下:

  1. function extend(child, parent) {  
  2.     var F = function(){}; //定义一个空的构造函数  
  3.     F.prototype = parent.prototype; //设置为父类的原型  
  4.     child.prototype = new F(); //子类的原型设置为F的实例,形成原型链  
  5.     child.prototype.constructor = child; //重新指定子类构造函数指针  
  6. }  
  7. function Parent(name) {  
  8.     this.name = name;  
  9.     this.colors = ['red', 'yellow'];  
  10. }  
  11. Parent.prototype.sayName = function() {  
  12.     alert(this.name);  
  13. }  
  14. function Child(name, age) {  
  15.     Parent.call(this, name);  
  16.     this.age = age;  
  17. }  
  18. extend(Child, Parent); //实现继承  
  19. Child.prototype.sayAge = function() {  
  20.     alert(this.age);  
  21. }  
  22. var c1 = new Child('zhangsan', 20);  
  23. var c2 = new Child('lisi', 21);  
  24. c1.colors.push('blue');   
  25. alert(c1.colors); //red,yellow,blue  
  26. c1.sayName(); //zhangsan  
  27. c1.sayAge(); //20  
  28. alert(c2.colors); //red,yellow  
  29. c2.sayName(); //lisi  
  30. c2.sayAge(); //21  
标签:

给我留言