看透JavaScript:原理、方法与实践
上QQ阅读APP看书,第一时间看更新

4.4 创建对象

JS中的function除了前面介绍的两种用法之外,还有一种非常重要的用法,那就是创建object实例对象。关于object类型对象的具体内容,本书将在下一章详细讲解,本节主要介绍如何使用function来创建object对象以及创建时的一些细节问题。

4.4.1 创建方式

使用function对象创建object类型对象的方法非常简单,直接在function对象前面使用new关键字就可以了,例如下面的例子。

    function F(){
        this.v = 1;
    }
    var obj = new F();  //创建F类型对象obj
    console.log(obj.v); //1

这个例子中,首先定义了一个function类型的对象F,然后使用F创建了object类型的对象obj,最后在控制台打印出obj对象的v属性的值。

使用function(例如F)创建object类型的对象(例如obj),只需要在function对象(F)前加new关键字就可以了。也就是说,对于一个function类型的对象,如果调用时前面没有new关键字,那么调用方法处理业务,如果前面有new关键字,那么用来创建对象。当然,创建对象时函数体也会被执行,对于具体创建的步骤下一节将详细介绍。

其实,经常使用的Array、Date等对象也都是function类型,可以使用new关键字来创建相应的object类型的对象实例。

为了区分主要用于处理业务的function和主要用于创建对象的function,一般会将主要用于创建对象的function的首字母大写而将主要用于处理业务的function的首字母小写。但这只是人为区分,实际使用时并没有什么影响。

4.4.2 创建过程

使用function创建object类型对象的过程可以简单地分为以下两步(可以这么理解,实际创建过程要复杂一些)。

1)创建function对应类型的空object类型对象。

2)将function的函数体作为新创建的object类型对象的方法来执行(主要目的是初始化object对象)。

例如下面的例子。

    function Car(color, displacement){
        this.color = color;
        this.displacement = displacement;
    }
    var car = new Car("black", "2.4T");
    console.log(car.color+", "+car.displacement);    //black, 2.4T

这个例子中,首先创建了function类型的Car,然后使用它新建object类型的car实例对象。在新建car对象时首先会新建Car类型的空对象car,然后再将Car函数作为新建对象的方法来调用,从而初始化新建的car实例对象,相当于下面的过程。

    function Car(){}
    var car = new Car();
    car.init = function (color, displacement){
        this.color = color;
        this.displacement = displacement;
    };
    car.init("black", "2.4T");
    console.log(car.color+", "+car.displacement);    //black, 2.4T

上述示例将原来Car对象中的函数体的内容放到新建的car的init方法中,在使用Car创建完car实例对象后,再调用init方法初始化,这种方式和前面例子中将初始化内容放到Car的函数体内的效果是完全相同的。

需要特别注意的是,创建过程的第二步,也就是说,在使用function对象新建object对象时依然会执行function的函数体。通过下面的例子可以更加直观地看到这一点。

    var name="和珅";
    function Sikuquanshu(){
      name="纪晓岚";
    }
    console.log(name);               //和珅
    var skqs = new Sikuquanshu();
    console.log(name);               //纪晓岚

这个例子中存在一个全局变量name,原值为“和珅”,在函数Sikuquanshu内部将其改为“纪晓岚”,在上述代码中并没有直接执行此函数,但在使用它创建skqs对象时其函数体得到了执行,这从创建skqs对象前后打印出的内容就能看出来,全局变量name被修改了。在使用function对象创建实例对象时一定要注意这一点。

理解了对象创建的过程就可以理解为什么在构造函数(例如Car)中使用this可以将属性添加到新创建的对象上。因为这时的函数体就相当于新创建的对象的一个方法,方法中的this指的就是新创建的对象自身,给this赋值就是给新创建的对象赋值,因此在function对象中使用this就可以给新创建出来的对象添加属性,就像本书第一个例子中的Car方法的“this.color = color; ”语句,这条语句会给新创建的car对象添加color属性并将color参数的值赋给它。对于这一点,在后面讲到object的属性时还会做进一步介绍。

如果觉得这里不容易理解也没关系,因为还没有学习object类型对象和this关键字的具体含义,可以先继续往下看,等学习完相关内容之后再返回来理解就容易了。

多知道点

对象在内存中是怎么保存的

前面主要介绍了对象属性、函数参数以及变量在内存中的保存方法,那么对象自身在内存中怎么保存呢?一般来说主要分为两大类方法。一类方法是直接将名称(或者名称的哈希值)和值(可能是实际内容也可能是地址)全部保存进去,这时可以使用类似JSON的格式。这种方式使用起来会比较灵活,但是在执行效率上会存在些问题,因为查找属性的过程会比较费时间。另一类方法是使用类似C语言中结构体的方式来保存,即只保存值而不保存变量,变量通过偏移量来查找。例如,{width:10, length:15}这个对象可以直接用8个字节来表示,width的偏移量是0, length的偏移量是4。这样在使用时不需要查找,直接按偏移量调用,效率就比第一类方法高,C++中的类就采用这种方法。但是,JS中的对象有些特殊,因为它的对象的属性是不确定的,而且可以随时修改(例如添加新的属性),这对于编程来说会很方便,但是对于按照第二类方法来保存对象数据来说就有点麻烦了。具体使用哪种方法来保存对象数据是由具体引擎的设计者来定的,不同的引擎可能会采用不同的处理方法。早期的JS引擎以第一类方法为主,而新的引擎为了提高效率也有采用第二类方法来保存的。

对于第二类方法来说,用同一个function创建的实例对象具有相同的结构,这时就可以将其看作同一类型(类似于C语言中的同一个结构体)来处理,但所创建的对象自身的属性也是可以修改的,在修改之后就成了新的类型。

在使用function创建对象时需要注意一种特殊情况,当function的函数体返回一个对象类型时,使用new关键字创建的对象就是返回的对象而不是function所对应的对象,例如下面的例子。

    function F(){}
    function Car(color, displacement){
        this.color = color;
        this.displacement = displacement;
        return new F();
    }
    var car = new Car("black", "2.4T");
    console.log(car.color+", "+car.displacement);   //undefined, undefined
    console.log(car instanceof Car);                //false
    console.log(car instanceof F);                  //true

这个例子中存在两个function对象:F和Car。在Car的函数体中返回了新建的F类型实例对象,这时使用Car新建出来的car对象就成了F类型的实例对象,而不是Car类型的实例对象。

4.4.3 prototype属性与继承

1.继承方法

继承是Java、C++等基于类的语言中的一个术语。它的含义是子类的对象可以调用父类的属性和方法。基于对象的ES语言根本没有类的概念,当然也就不存在基于类的那种继承方式,但是,它可以通过prototype属性来达到类似于继承的效果。

prototype是ES中function类型对象的一个特殊属性。每个function类型的对象都有prototype属性,prototype属性的值是object类型的对象。在FireBug中可以看到Object(Object本身是function类型)的prototype属性类型如图4-8所示。

图4-8 FireBug中的prototype属性类型

function对象中prototype属性对象的作用是这样的:在function对象创建出的object类型对象实例中可以直接调用function对象的prototype属性对象中的属性(包括方法属性),例如下面的例子。

    function Car(color, displacement){
        this.color = color;
        this.displacement = displacement;
    }
    Car.prototype.logMessage = function(){
        console.log(this.color+", "+this.displacement);
    }
    var car = new Car("black", "2.4T");
    car.logMessage();                //black, 2.4T

这个例子中,给Car的prototype属性对象添加了logMessage方法,这样使用Car创建的car对象就可以直接调用logMessage方法。虽然这里可以使用car调用logMessage方法,但是car对象本身并不会添加这个方法,只是可以调用而已。

function创建的实例对象在调用属性时会首先在自己的属性中查找,如果找不到就会去function的prototype属性对象中查找。但是,创建的对象只是可以调用prototype中的属性。但是并不会实际拥有那些属性,也不可以对它们进行修改(修改操作会在实例对象中添加一个同名属性)。当创建的实例对象定义了同名的属性后就会覆盖prototype中的属性,但是原来prototype中的属性并不会发生变化,而且当创建出来的对象删除了添加的属性后,原来prototype中的属性还可以继续调用,请看下面的例子。

    function Car(color, displacement){
        this.color = color;
        this.displacement = displacement;
    }
    Car.prototype.logMessage = function(){
        console.log(this.color+", "+this.displacement);
    }
    var car = new Car("black", "2.4T");
    car.logMessage();                //black,2.4T
    car.logMessage = function() {
        console.log(this.color);
    }
    car.logMessage();                //black
    delete car.logMessage;
    car.logMessage();                //black,2.4T

在这个例子中,使用Car直接创建的car对象并没有logMessage方法,所以第一次调用logMessage方法时会调用Car的prototype属性对象中的logMessage方法,然后给car定义了logMessage方法,这时再调用logMessage方法就会调用car自己的logMessage方法了,最后又删除了car的logMessage方法,此时调用logMessage方法就会再次调用Car的prototype属性对象中的logMessage方法,而且Car的prototype属性对象中的logMessage方法的内容也没有发生变化。代码执行后的输出结果如下。

    black,2.4T
    black
    black,2.4T

我们可以通过FireBug将整个过程看得更加清楚,在增加断点后可以看到图4-9所示的结构。

图4-9 在增加断点后可以看到的结构

从图4-9中可以看出,Car的prototype属性并没有发生变化,而car对象自己的属性中先被添加,然后又被删除了logMessage属性方法。

2.多层继承

function的prototype属性是object类型的属性对象,其本身可能也使用function创建的对象,通过这种方法就可以实现多层继承,例如下面的例子。

    function log(msg){
        console.log(msg);
    }


    function Person(){}
    Person.prototype.logPerson = function () {
        log("person");
    }


    function Teacher(){
        this.logTeacher = function () {
            log("teacher");
        }
    }
    Teacher.prototype = new Person();
    Teacher.prototype.logPrototype = function () {
        log("prototype");
    }


    var teacher = new Teacher();
    teacher.logTeacher();
    teacher.logPrototype();
    teacher.logPerson();

这个例子中,因为Teacher的prototype属性是Person创建的实例对象,而使用Teacher创建出来的teacher对象可以调用Teacher的prototype属性对象的属性,所以teacher对象可以调用Person创建的实例对象的属性。又因为Person创建的实例对象可以调用Person的prototype属性对象中的属性,所以teacher对象也可以调用Person的prototype属性对象中的属性方法logPerson。另外,因为此程序给Teacher的prototype属性对象添加了logPrototype方法,所以teacher也可以调用logPrototype方法。最后的输出结果如下。

    teacher
    prototype
    person

这种调用方法相当于基于类语言中的多层继承,它的结构如图4-10所示。

图4-10 基于类语言中的多层继承

Teacher创建出来的teacher对象在调用属性时会首先在自己的属性中查找,如果找不到就会到Teacher的prototype属性对象的属性中查找,如果还找不到就会到Person的prototype属性对象的属性中查找,而Teacher的prototype又由两部分组成,一部分是用Person创建的person对象,另一部分是直接定义的logPrototype方法。

3.使用prototype时的注意事项

在function的prototype属性对象中默认存在一个名为constructor的属性。这个属性默认指向function方法自身,例如上节例子中Person的prototype属性对象的constructor属性就指向了Person。但是,Teacher的prototype由于被赋予了新的值,因此它的constructor属性就不存在(使用Person创建的person对象自身并没有constructor属性)。这时,如果调用teacher.constructor就会返回Person函数(因为最后会沿着prototype找到person的prototype属性对象的constructor属性)。为了可以使用constructor属性得到正确的构造函数,可以手动给Teacher的prototype属性对象的constructor属性赋值为Teacher,代码如下所示。

    Teacher.prototype =  new Person();
    Teacher.prototype.logPrototype = function () {
        console.log("prototype");
    }
    Teacher.prototype.constructor = Teacher;

使用prototype时应注意以下三点。

一是,prototype是属于function类型对象的属性,prototype自身的属性可以被function创建的object类型的实例对象使用,但是object类型的实例对象自身并没有prototype属性。

二是,如果要给function对象的prototype属性赋予新的值并且又要添加新的属性,则需要先赋予新值,然后再添加新的属性,否则在赋值时,会将原先添加的属性覆盖掉,例如下面的代码。

    function log(msg){
        console.log(msg);
    }


    function Person(){}
    function Teacher(){}
    Teacher.prototype.logPrototype = function () {
        log("prototype");
    }
    Teacher.prototype = new Person();


    var teacher = new Teacher();
    teacher.logPrototype();

在上述代码中,在执行最后一行代码teacher.logPrototype()的时候会报错,这是因为给Teacher的prototype属性对象添加了logPrototype属性方法后,又将prototype赋值为new Person(),而新的prototype中并没有logPrototype方法,所以调用就会出错,也就是说logPrototype被新的对象覆盖。

function创建的对象在调用属性时是实时按prototype链依次查找的,而不是将prototype中的属性关联到创建的对象本身,因此创建完对象后,再修改function的prototype也会影响到创建的对象的调用,例如下面的例子。

    function Teacher(){}
    var teacher = new Teacher();
    Teacher.prototype.log = function (msg) {
        console.log(msg);
    }
    teacher.log("hello");   //hello

这里的log方法是在teacher对象已经创建完成后添加的,但是在teacher对象中仍然可以使用,也就是说prototype中的属性是动态查询的。

另外,使用prototype除了可以实现继承之外,还可以节约内存,因为无论使用function创建多少对象,它们所指向的prototype对象在内存中都只有一份。但是,使用prototype中的属性比直接使用对象中定义的属性在执行效率上理论来说会低一些。