JavaScript面向对象

来源:互联网 发布:mac 照片应用 密码 编辑:程序博客网 时间:2024/06/15 06:20

      ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。正因为这样(以及其他将要讨论的原因),我们可以把ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。每个对象都是基于一个引用类型创建的,这个引用类型可以是第5 章讨论的原生类型,也可以是开发人员定义的类型。

一、创建对象

      比较简单的创建对象的方式创建一个Object 的实例,然后再为它添加属性和方法

var person = new Object();person.name = "Nicholas";person.age = 29;person.job = "Software Engineer";person.sayName = function(){    alert(this.name);};
或者使用字面量的方式:

var person = {    name: "Nicholas",    age: 29,    job: "Software Engineer",    sayName: function(){      alert(this.name);  }};
使用上面的方法虽然可以创建对象,但缺点也很明显,假如再次创建一个具有相同属性以及方法的对象,还得把代码复制修改一遍,会产生大量的重复代码。下面来看一些比较复杂(高级)的创建对象的方式

工厂模式

     这种模式抽象了创建对象的具体过程,可以通过每次调用相同的函数创建对象,有点类似java中的类,减少了代码量,但是不能判断类型。

function createperson(name,age){    var o = new Object();    o.name = name;    o.age = age;    o.sayName = function(){        alert(this.name);    };    return o;}    var p1 = createperson('mncu',120);    var p2 = createperson('linghuchong',120);    p1.sayName();   //'mncu'    p2.sayName();   //'linghuchong'    console.log(typeof p1); //object    p1 instanceof createperson  // false ,无法判断类型
构造函数模式

     使用构造函数模式创建对象更像诸如Java一类的面向对象的语言了,你可以将构造函数理解为一个类(JavaScript中并没有类这个概念,只是这样理解),通过new关键字创建对象是不是很熟悉。使用new关键字创建对象会经历一下四个步骤:

1  创建一个新对象

2  将构造函数的作用域赋值给这个新对象

3  执行构造函数的代码

4  返回新对象

使用构造函数创建的对象都有一个constructor属性,该属性可以标识对象类型,但一般我们还是常用instanceof来判断对象类型, 因为constructor属性仅返回构造函数类型。构造函数实际上和普通函数并没有区别,因为其不存在特殊的语法,任何函数,只要通过new调用,那么他就可以作为构造函数,反之他就是普通函数(它们的区别只在与调用方式的不同)。

function Person(name,age){   // 按照惯例,构造函数的首字母要大写    this.name = name;    this.age = age;    this.sayName = function(){        alert(this.name);    };}    var p1 = new Person('mncu',120);  // 必须使用new关键字创建对象    var p2 = new Person('linghuchong',120);    p1.sayName();  //'mncu'    alert(p1 instanceof Person); // true,可判断类型
构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍(代码无法复用),会造成内存的浪费。这些问题我们可以通过原型模式来解决。

原型模式

     原型模式解决了构造函数模式中同功能的方法的代码无法重用的问题。我们创建的每个函数都有一个名为prototype的属性,这个属性是一个指针,指向一个对象,这个对象被称为原型对象。原型对象有一个名叫constructor的属性,这个属性是一个指针,指向构造函数。默认情况下,所有函数的原型都是Object的实例。放在原型对象中的属性和方法是所有实例共享的,看下面这段代码:

function Person(){    Person.prototype.name = 'noOne';    Person.prototype.age = 'noAge';    Person.prototype.sayName = function(){        return this.name;    };}    var p1 = new Person();    p1.name;  // 'noOne'    var p2 = new Person()    p2.name; //'noOne'

函数、对象与原型的关系如下图所示,函数Person中有一个prototype属性指向原型对象,而原型对象中又有一个constructor属性指向Person函数,而对象person1和person2中并没有属性和方法,它们和函数Person也没有直接的关系,但他们内部都有一个[[prototype]](__proto__)指向原型对象,从原型对象中获取属性和方法。而如果实例和原型中有相同的属性或者方法时,会调用实例(对象)中的方法(即先在对象(实例)中查找,若未找到再去原型中查找)。

in和hasOwnProperty():使用in关键字可以判断一个方法或属性是否能够通过对象访问到(属性既可以存在于对象实例中也可以存在于原型中),而hasOwnProperty()只有当属性来自与实例时才会返回true。

重写原型对象及其可能造成的问题,看下面一段代码:

function Person(){}var friend = new Person();Person.prototype = {    constructor: Person,    name : "Nicholas",    age : 29,    job : "Software Engineer",    sayName : function () {        alert(this.name);    }};friend.sayName(); //error
报错的原因是friend指向的原型对象中并没有sayName方法。这是因为重写原型对象切断了构造函数与旧的原型对象之间的联系,使得构造函数指向了新创建的原型对象,而实例指向的是旧的原型对象,如下图所示:


原型对象存在的问题:原型对象的问题显而易见,因为内容都是共享的,所以在一个实例中修改值(特别是引用类型值)时会影响其他实例中的值。

构造函数和原型模式组合使用(常用)

 构造函数模式的属性没毛病。缺点是:无法共享方法 

 原型模式的方法没毛病。缺点是:当原形对象的属性的值为引用类型时,对其进行修改会反映到所有实例中 

     所以我们可以两者组合使用,构造函数模式用于定义属性,原型模式用于定义方法,这样就可以集两种模式之所长了。如下:

function Person(name,age){        this.name = name;        this.age = age;    }    Person.prototype={        constructor:Person,        sayName:function(){            alert(this.name);        }    };    var p1 = new Person('mncu',120);    p1.name;  // 'mncu'
动态原型模式

      这种方式本质上和上面那种方式一样,只是将所有的信息都封装在了构造函数中,更符合面向对象的思想。

function Person(name, age, job){  //属性放在构造函数中    this.name = name;    this.age = age;    this.job = job;    //方法放在原型中    if (typeof this.sayName != "function"){        Person.prototype.sayName = function(){            alert(this.name);        };    }}var friend = new Person("Nicholas", 29, "Software Engineer");friend.sayName()
二、继承

     继承是面向对象中的一个重要特性,继承包括:接口继承和实现继承,而JavaScript只支持实现继承。

原型链

      原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。假如我们让原型对象等于另一个类型的实例,此时的原型对象SubType将包含一个指向另一个原型B的指针,而这就形成了SubType继承SuperType的情况了,如下图所示。


   

function SuperType(){    this.property = true;}SuperType.prototype.getSuperValue = function(){    return this.property;};function SubType(){    this.subproperty = false;}//继承了SuperTypeSubType.prototype = new SuperType();SubType.prototype.getSubValue = function (){    return this.subproperty;};var instance = new SubType();alert(instance.getSuperValue()); //true
而如果上例中的SuperType又指向其他实例(比如GrandType)的话,就形成了一条原型链,分别是SubType继承自SuperType,而SuperType又继承自GrandType。所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object 的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。还需要注意的是在使用原型链继承时,不能使用对象字面量的方式创建原型方法,这样会使原型链断掉。

使用原型链继承的问题:1、是像使用原型模式创建对象一样,使用原型链继承也会使得引用类型的值不易被修改(在其中一个实例中修改会影响到其他的实例)。2、在创建子类型的实例时,不能向超类型的构造函数中传递参数。

借用构造函数

      在构造函数内部通过call或apply调用超类型的构造函数,如下所示:

    function SuperObject(){        this.colors = ['red','blue'];        this.sayBye= function(){            console.log('Bye')        }    }    function SubObject(){        SuperObject.call(this);  // 在子类中调用父类的构造方法,实际上子类和父类已经没有上下级关系了    }    var instance1 = new SubObject();    instance1.colors.push('yellow');    var instance2 = new SubObject();    console.log(instance2.colors); //['red','blue']    console.log(instance2 instanceof SuperObject); // false    console.log(instance1.sayBye === instance2.sayBye)  // false
使用这种方式可以传递参数,但是就像使用构造函数创建对象一样,使用借用构造函数实现继承无法实现方法的服用。

组合继承(常用)

     组合继承指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。即使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function SuperType(name){    this.name = name;    this.colors = ["red", "blue", "green"];}SuperType.prototype.sayName = function(){    alert(this.name);};function SubType(name, age){    //继承属性    SuperType.call(this, name);    this.age = age;}//继承方法SubType.prototype = new SuperType();SubType.prototype.constructor = SubType;SubType.prototype.sayAge = function(){    alert(this.age);};var instance1 = new SubType("Nicholas", 29);instance1.colors.push("black");alert(instance1.colors); //"red,blue,green,black"instance1.sayName(); //"Nicholas";instance1.sayAge(); //29var instance2 = new SubType("Greg", 27);alert(instance2.colors); //"red,blue,green"instance2.sayName(); //"Greg";instance2.sayAge(); //27
寄生组合式继承

      组合继承是JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function object(o){//原型继承    function F(){};    F.prototype = o;    return new F();//返回一个对象}function inheritPrototype(subType, superType){    var prototype = object(superType.prototype); //创建对象,该对象的原型对象即为superType.prototype    prototype.constructor = subType; //原型对象constructor属性指向子构造函数    subType.prototype = prototype; //指定对象}function SuperType(name){    this.name = name;    this.colors = ["red", "blue", "green"];}SuperType.prototype.sayName = function(){    alert(this.name);};function SubType(name, age){    SuperType.call(this, name);//继承属性    this.age = age;}inheritPrototype(SubType, SuperType);//继承原型对象中的方法SubType.prototype.sayAge = function(){    alert(this.age);};
上述代码中通过新创建一个构造函数,然后将SuperType的原型对象赋给新创建的构造函数的原型对象(只继承了原型而没有继承属性),而新创建的原型对象中的constructor属性指向SubType。这样就避免了两次调用构造函数使得属性在原型和实例中都出现了。