JavaScript之面向对象学习一

时间:2022-04-24
本文章向大家介绍JavaScript之面向对象学习一,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1、通过Object构造函数和对象字面量来创建对象缺点:使用同一个接口创建很多的对象,会产生大量的重复代码。比如我需要创建人的对象,并且需要三类人,医生、工程师、老师,他们可以抽象出很多属性,比如姓名、年龄、工作等,只是各自的值不一样,如果这里我用Object或者对象字面量来创建的话,那我必须每个人都写一次这三个属性,造成了代码重复。

2、思考下方法一的问题,我们发现我们可以通过一个工厂来创建对象,这样做的好处是,我们能把一些共同的属性封装到工厂里面,而我们创建对象时,只需要把对象的参数,传递给工厂函数,在由工厂来返回对象。代码如下:

function createPeron(name,age,job){
        var object=new Object();
        object.name=name;
        object.age=age;
        object.job=job;
        object.sayName=function(){
            alert(this.name);
        }
        object.sayForm=function(){
            alert(typeof this);
        }
        return object;
    }
var person=createPeron("张三",22,"coder");
    person.sayName();
    person.sayForm();

输出:"张三","object";  

结论:虽然工厂模式帮助我们解决了创建多个相似对象的问题,极大的减少了创建对象所用的代码,但是却有解决对象识别的问题(即怎样知道一个对象的类型),从第二个输出"object"来看,通过工厂创建的对象,其对象类型永远是Object类型。我们永远无法知道它的具体类型!

3、思考方法二的问题, 发现因为众所周知,ECMAScript中的构造函数可用来创建特定类型的对象,像Object、Array、Date、Math等等的都是通过构造函数来创建的对应的对象。所以我们可以创建自定义的构造函数,来达到我们创建对象的目的,下面来简单的分析下构造函数:

    //构造函数模式创建对象
    function Person(name,age,job){
        this.name=name;
        this.age=age;
        this.job=job;
        this.sayName=function(){
            alert(this.name);
        }
        this.sayForm=function(){
            alert(Person);
            alert(this.constructor);
        }
    }
    var person=new Person("张三",22,"coder");
    var person1=new Person("李四",23,"coder");
    person.sayForm();
    alert(person instanceof Object);
    alert(person instanceof Person);

比较工厂模式创建的对象和构造函数模式创建的对象的差异:

(1)构造函数模式没有显示的创建对象

(2)直接将属性和方法赋给了this对象

(3)没有return 语句

注意:所有的构造函数函数名必须是大写(如果构造函数使用来创建对象的),这个做法借鉴与其他的oo语言(面向对象语言),并且这也是为了区别于ECMAScript中的其他函数,表名这个函数使用来创建对象的,因为构造函数本身也是一个函数。

要创建Person的实例,必须使用new操作符。下面来分析下构造函数模式创建对象的过程:

(1)创建一个新对象

(2)将构造函数的作用域赋给新对象(所以this就指向了这个新对象)

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

(4)返回新对象

在构造函数模式创建对象的代码实例中,person和person1分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person,某种意义上,constructor属性就代表了对象的类型,即构造函数的函数名。

输出:Person所在的构造函数方法体,Person所在的构造函数方法体,"true","true"

person.sayForm()说明了对象实例的constructor属性永远是指向构造函数的(在这里只Person构造函数),所以我们可以通过constructor属性来判断实例的对象的类型,这解决了方法二工厂模式的不足之处.

最后两个输出true说明person实例即是Person类型又是Object类型。

总结:创建自定义的构造函数意味着将来可以将它的实例表示为一种特定的类型;这正是构造函数模式胜过工厂模式的地方,person和person1之所以同时是Object的实例,是因为所有对象均继承自Object;

4、将构造函数当作函数

      构造函数与其他函数的区别就在与调用他们的方式不同。不过构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它和普通函数也没什么区别。例如上面代码中的Person()函数可以通过下面任何一种方式来调用

//当作构造函数使用
    var person=new Person("张三",22,"coder");
    person.sayName();

    //作为普通函数调用
    Person("李四",22,"coder");  //这里面的属性值,都将赋值给window对象
    window.sayName();

    //在另一个对象的作用域中调用(相当于在其他的类中调用Person类的属性和方法)
    var o=new Object();
    Person.call(o,"kobe",39,"Backetball Player"); //通过call()(或者apply())在摸个特殊对象(这里指Object对象o)的作用域中调用Person对象的属性和方法
    o.sayName();

这里说下两种特殊情况:

(1)当将构造函数作为普通函数调用,函数的属性和方法都被添加给window对象。因为当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中就指window对象),因此再调用完函数之后,可以通过window对象来调用sayName()方法。

(2)当我需要在某个特殊的对象的作用域中调用Person()对象,需要使用call()(或者apply()方法)。

5、构造函数模式创建对象的缺点

构造函数模式虽然好用,但也并非没有缺点,它的主要缺点就是每个方法都要在每个实例中重新创建一遍,在上面的代码中,person和person1都有一个名为sayName()的方法,但那两个方法不是同一个对象,因为在ECMAScript中,函数(Function)就是一个对象,因此每定义一个函数,就是实例化了一个对象,从逻辑角度讲,此时的构造函数可以这样定义,代码如下:

    function Person(name,age,job){
        this.name=name;
        this.age=age;
        this.job=job;
        this.sayName=new Function("alert(this.age)");  
    }
    var person=new Person("张三",22,"coder");

      从上面代码的角度来看构造函数,更容易明白每个Person实例都包含一个不同的Function实例(以显示name属性)的本质。但是他们做的确是同一件事,说明白点以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制任然是相同的。因此,不同实例上的同名函数是不相等的,以下代码可以证明:

    function Person(name,age,job){
        this.name=name;
        this.age=age;
        this.job=job;
        this.sayName=new Function("alert(this.age)");
    }
    var person=new Person("张三",22,"coder");
    var person1=new Person("李四",22,"coder");
    alert(person.sayName==person1.sayName);

输出:false;  说明上述结论正确,两个Function完成的是同一功能,但是却非同一实例。

所以,为了让每个对象拥有相同的作用域链和标识符解析,说明白点,这里创建两个相同功能的Funcrion实例没有必要,我们可以这样,将对象的函数定义到对象外面,通过this对象,是每个对象在实例化方法前去掉用对应的外部函数,而这个函数在实例化对象调用前已被初始化,所以所有的实例化对象调用的都是同一函数!再点以下代码可以证明:

    function Person(name,age,job){
        this.name=name;
        this.age=age;
        this.job=job;
        /*this.sayName=new Function("alert(this.age)");*/
        this.sayName=sayName;
    }
    function sayName(){
        alert(this.name);
    }
    var person=new Person("张三",22,"coder");
    var person1=new Person("李四",22,"coder");
    person.sayName();
    person1.sayName();
    alert(person.sayName);
    alert(person.sayName==person1.sayName);

输出:张三、李四、sayName()方法所在的方法体、true;

最后的true说明,两个实例共用一个方法体!

在上面这个例子中,我们把sayName()函数的定义转到了构造函数外部。而在构造函数内部,我们将sayName属性设置成等于全局的sayName函数,这样一来,由于sayName包含的是一个指向函数的指针,因此两个实例就共用了共享了在全局作用域中定义的同一个sayName()函数。这样做确实让解决了构造函数的两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象所调用,这让全局作用域有点名不其实,而最主要的问题是:如果对象需要定义很多方法,那么就需要定义很多的全局函数,于是我们这个自定义的应用类型就毫无封装型可言!

6、原型模式

原型模式创建对象的代码如下:

   function Person(){
    }
    Person.prototype.name="张三";
    Person.prototype.age=22;
    Person.prototype.job="coder";
    Person.prototype.sayName=function(){
        alert(this.name);
    }
    var person1=new Person();
    var person2=new Person();
    alert(person1.sayName==person2.sayName);

输出:true;说明原型模式很好的解决了构造函数模式创建对象实例时,创建Function时导致的不同作用域链和标识符解析的问题,两个或多个Function相同功能,却并非同一实例。

    无论什么时候,只要创建了一个新函数(这里是Person函数),就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象(原型对象里面所有的方法和属性都被其对应的对象的实例所共享)。在默认情况下,所有的原型对象都会自动获得一个constructor属性,这个属性指向prototype属性所在函数(这里是Person函数)的指针!

   创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于原型对象其他方法,则都是从Object对象继承而来。

   当使用构造函数创建了该对象的实例后,该实例的内部将包含一个指针(内部属性),ECMA-262 第5版中定义这个指针为[[prototype]],虽然在脚本中没有标准的方式访问该属性,但是在Firefox、Safari、Chrome中每个对象都支持一个属性_proto_;在其他实现中,这个属性对脚本是完全不可见的。不过我们需要知道的是这个连接(属性[[prototype]]或者_proto_)存在于实例与构造函数的原型对象之间,而不是存在于实例于构造函数之间;根据上面关于原型模式的描述,可以得到如如下的流程图:

     上图展示了Person构造函数、Person原型属性(Person prototype)对象、以及Person现有的两个实例之间的关系,从上面的图中我们可以看出,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person函数。Person的两个实例Person1和Person2都包含一个内部属性[[prototype]],这个属性仅指向了Person.prototype,也就是说,他们于构造函数没有直接的关系,另外结合上图的上面的代码我们发现,虽然两个Person的实例都不包含属性和方法,但我们却可以调用person1.sayName()。这是通过查找对象的属性来实现的。

     虽然在所有的实现中都无法访问到[[Prototype]],但是我们可以通过isPrototypeOf()方法来判断实例和原型对象之间是否存在某种关系,即判断实例是否指向对象的原型属性对象(对象.prototype),代码如下:

    function Person(){
    }
    Person.prototype.name="张三";
    Person.prototype.age=22;
    Person.prototype.job="coder";
    Person.prototype.sayName=function(){
        alert(this.name);
    }
    var person1=new Person();
    var person2=new Person();
    alert(Person.prototype.isPrototypeOf(person1)); //判断person1的[[prototype]]属性是否指向Person.prototype原型属性对象
    alert(Person.prototype.isPrototypeOf(person2));

输出:true,true;说明person1实例和person2实例都指向Person.prototype原型属性对象,都可以调用里面的共享方法和属性。

在知道person1和person2是Person的两个实例后,我们就可以使用ECMAScript5中新增加方法-Object.getPrototypeOf()方法(在所有支持的实现中,这个方法返回[[Prototype]]的值)也就是说我们可以通过对象的实例来获取对象原型属性对象的值,代码如下:

    function Person(){
    }
    Person.prototype.name="张三";
    Person.prototype.age=22;
    Person.prototype.job="coder";
    Person.prototype.sayName=function(){
        alert(this.name);
    }
    var person1=new Person();
    var person2=new Person();
    alert(Object.getPrototypeOf(person1)==Person.prototype);
    alert(Object.getPrototypeOf(person1).age);

输出:true,22;

第一行alert说明了person1的[[prototype]]和Person.prototype是一样的都代表Person的属性对象;

第二行alert取得了原型对象中age的值22,使用Object.getPrototypeOf()可以方便的取得一个对象的原型属性,而这在利用原型实现继承是非常重要的!

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型属性对象中的值。如果我们在实例中添加一个属性,而该属性与实例原型中的一个属性同名,那就会在实力中创建该属性,该属性将会屏蔽原型中的那个属性。代码如下:

    function Person(){
    }
    Person.prototype.name="张三";
    Person.prototype.age=22;
    Person.prototype.job="coder";
    Person.prototype.sayName=function(){
        alert(this.name);
    }

    var person1=new Person();
    var person2=new Person();
    person1.name="李四";
    alert(person1.name);
    alert(person2.name);

输出:李四,张三;

观察上面代码的输出我们发现,person1对应原型对象中的属性被一个新值给屏蔽了,但无论访问person1.name还是person2.name都能够正常的返回值,既分别是"李四"(来自对象实例)和"张三"(来自原型对象),为什么会这样呢?

因为每当代码读取某个对象的属性时,都会执行一次搜索,目标是具有给定名字的属性。首先从对象本身实例开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值,如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回这个属性的值。

再看上面的代码,我们为对象实例添加一个属性(这个属性和原型对象中的属性名一样),这样只会阻止我们访问原型中的那个属性,不会修改那个属性!综上所述,任何对对象实例的操作只会改变当前实例的属性值,不会对对象的原型属性对象造成任何改变!即使使用delete操作符,也只能删除实例属性。代码如下:

    function Person(){
    }
    Person.prototype.name="张三";
    Person.prototype.age=22;
    Person.prototype.job="coder";
    Person.prototype.sayName=function(){
        alert(this.name);
    }

    var person1=new Person();
    var person2=new Person();
    person1.name="李四";
    alert(person1.name);
    delete person1.name;
    alert(person1.name);

输出:李四,张三。

观察上面的代码,发现,使用delete操作符删除了person1.name,之前他保存的"李四"屏蔽了同名的原型属性name的属性值"张三"。把它删除以后,就恢复了对原型中name属性的连接。因此,接下来在调用person1.name时,输出的就是原型属性对象中的name属性值"张三"了;

当我们判断一个属性是存在于实例中还是原型中可以使用hasOwnProperty()方法(这个方法继承于Object)只在给定属性存在于对象实例中才会返回true;

   function Person(){
    }
    Person.prototype.name="张三";
    Person.prototype.age=22;
    Person.prototype.job="coder";
    Person.prototype.sayName=function(){
        alert(this.name);
    }

    var person1=new Person();
    var person2=new Person();
    alert(person1.hasOwnProperty("name")); //这段代码可以这样理解:person1实例是否有他自己的属性name,
    // 输出:false,因为他没有自己的属性,他只有原型对象的name属性,这是被所有实例所共享的并不是person1自己的
    person1.name="李四";//定义了一个自己的name属性相当于实例属性
    alert(person1.hasOwnProperty("name")); //因为上面定义了一个person1自己的实例属性   所以输出:true

    delete person1.name;  //删除了  person1的实例属性name;
    alert(person1.hasOwnProperty("name")); //因为上面删除了person1的实例属性name,所以输出false