06.JavaScript面向对象的程序设计

面向对象的程序设计

理解对象

创建自定义对象的最简单方式就是创建一个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);
    }
};
属性类型

ECMAScript中有两种属性:数据属性和访问器属性。

  • 数据属性

    数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。

    • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true

    • [[Enumerable]]:表示能否通过for-in循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true

    • [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true

    • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined

    • 要修改属性默认的特性,必须使用ECMAScript 5的**Object.defineProperty()方法**。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是:configurableenumerablewritablevalue。设置其中的一或多个值,可以修改对应的特性值。

      var person = {};
      Object.defineProperty(person, "name", {
          writable: false,
          value: "Nicholas" 
      });
      
      alert(person.name);    //"Nicholas"
      person.name = "Greg";
      alert(person.name);    //"Nicholas"
      
  • 访问器属性

    访问器属性不包含数据值;它们包含一对儿getter和setter函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性。

    • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为true
    • [[Enumerable]]:表示能否通过for-in循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为true
    • [[Get]]:在读取属性时调用的函数。默认值为undefined
    • [[Set]]:在写入属性时调用的函数。默认值为undefined

    访问器属性不能直接定义,必须使用Object.defineProperty()来定义

    var book = {
        _year: 2004, 
        edition: 1
    };
    
    Object.defineProperty(book, "year", {
        get: function(){
            return this._year;
        },
        set: function(newValue){
    
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    });
    
    book.year = 2005;
    alert(book.edition);  //2
    
定义多个属性
  • Object.defineProperties()方法

    • 通过描述符一次定义多个属性。

    • 接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应

    • var book = {};
      
      Object.defineProperties(book, {
          _year: {
              value: 2004
          },
      
          edition: {
              value: 1
          },
      
          year: {
              get: function(){
                  return this._year;
              },
      
              set: function(newValue){
                  if (newValue > 2004) {
                      this._year = newValue;
                      this.edition += newValue - 2004;
                  }
              }
          }
      });
      
读取属性的特性
  • Object.getOwnPropertyDescriptor()方法

    • 取得给定属性的描述符

    • 接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurableenumerablegetset;如果是数据属性,这个对象的属性有configurableenumerablewritablevalue

    • var book = {};
      
      Object.defineProperties(book, {
          _year: { 
              value: 2004
          },
      
          edition: {
              value: 1
          },
      
          year: {
              get: function(){
                  return this._year;
              },
      
              set: function(newValue){
                  if (newValue > 2004) {
                      this._year = newValue;
                      this.edition += newValue - 2004;
                  }
              }
          }
      });
      
      var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
      alert(descriptor.value);         //2004
      alert(descriptor.confi gurable); //false
      alert(typeof descriptor.get);    //"undefined"
      
      var descriptor = Object.getOwnPropertyDescriptor(book, "year");
      alert(descriptor.value);        //undefined
      alert(descriptor.enumerable);   //false
      alert(typeof descriptor.get);   //"function"
      

创建对象

工厂模式

开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };    
    return o;
}

var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
构造函数模式

ECMAScript中的构造函数可用来创建特定类型的对象,此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
        alert(this.name);
    };    
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

在这个例子中,Person()函数取代了createPerson()函数。我们注意到,Person()中的代码除了与createPerson()中相同的部分外,还存在以下不同之处:

  • 没有显式地创建对象;
  • 直接将属性和方法赋给了this对象;
  • 没有return语句。

此外,还应该注意到函数名Person使用的是大写字母P。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。

要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

在前面例子的最后,person1person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person,如下所示。

alert(person1.constructor == Person);  //true
alert(person2.constructor == Person);  //true

对象的constructor属性最初是用来标识对象类型的。但是,提到检测对象类型,还是instanceof操作符要更可靠一些。我们在这个例子中创建的所有对象既是Object的实例,同时也是Person的实例,这一点通过instanceof操作符可以得到验证。

alert(person1 instanceof Object);  //true
alert(person1 instanceof Person);  //true
alert(person2 instanceof Object);  //true
alert(person2 instanceof Person);  //true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。在这个例子中,person1person2之所以同时是Object的实例,是因为所有对象均继承自Object

  1. 将构造函数当做函数
  • 构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样

  • // 当作构造函数使用
    var person = new Person("Nicholas", 29, "Software Engineer");
    person.sayName(); //"Nicholas"
    
    // 作为普通函数调用
    Person("Greg", 27, "Doctor"); // 添加到window
    window.sayName(); //"Greg"
    
    // 在另一个对象的作用域中调用
    var o = new Object();
    Person.call(o, "Kristen", 25, "Nurse");
    o.sayName(); //"Kristen"
    
  1. 构造函数的问题
  • 使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

  • 因此,大可像下面这样,通过把函数定义转移到构造函数外部来解决这个问题

  • function Person(name, age, job){
        this.name = name;
        this.age = age;
        this.job = job;
        this.sayName = sayName;
    }
    
    function sayName(){
        alert(this.name);
    }
    
    var person1 = new Person("Nicholas", 29, "Software Engineer");
    var person2 = new Person("Greg", 27, "Doctor");
    
  • 在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。

原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中

function Person(){
}

Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
    alert(this.name);
};

var person1 = new Person();
person1.sayName();   //"Nicholas"

var person2 = new Person();
person2.sayName();   //"Nicholas"

alert(person1.sayName == person2.sayName);  //true
  1. 理解原型对象
  • 无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。

  • 创建了自定义的构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象

  • 以上面代码为例

  • ECMAScript 5增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值

    alert(Object.getPrototypeOf(person1) == Person.prototype); //true
    alert(Object.getPrototypeOf(person1).name); //"Nicholas"
    
  • 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。

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

  • 当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用delete操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性

  • 使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例中时,才会返回true

    function Person(){
    }
    
    Person.prototype.name = "Nicholas";
    Person.prototype.age = 29;
    Person.prototype.job = "Software Engineer";
    Person.prototype.sayName = function(){
        alert(this.name);
    };
    
    var person1 = new Person();
    var person2 = new Person();
    
    alert(person1.hasOwnProperty("name"));  //false
    
    person1.name = "Greg";
    alert(person1.name);     //"Greg"——来自实例
    alert(person1.hasOwnProperty("name"));  //true
    
    alert(person2.name);     //"Nicholas"——来自原型
    alert(person2.hasOwnProperty("name"));  //false
    
    delete person1.name;
    alert(person1.name);     //"Nicholas"——来自原型
    alert(person1.hasOwnProperty("name"));  //false
    

  1. 原型与in操作符

有两种方式使用in操作符:单独使用和在for-in循环中使用。

  • 单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中

    function Person(){
    }
    
    Person.prototype.name = "Nicholas";
    Person.prototype.age = 29;
    Person.prototype.job = "Software Engineer";
    Person.prototype.sayName = function(){
        alert(this.name);
    };
    
    var person1 = new Person();
    var person2 = new Person();
    
    alert(person1.hasOwnProperty("name"));  //false
    alert("name" in person1);  //true
    
    person1.name = "Greg";
    alert(person1.name);   //"Greg" ——来自实例
    alert(person1.hasOwnProperty("name"));  //true
    alert("name" in person1);  //true
    
    alert(person2.name);   //"Nicholas" ——来自原型
    alert(person2.hasOwnProperty("name"));  //false
    alert("name" in person2);  //true
    
    delete person1.name;
    alert(person1.name);   //"Nicholas" ——来自原型
    alert(person1.hasOwnProperty("name"));  //false
    alert("name" in person1);  //true
    
    • 由于in操作符只要通过对象能够访问到属性就返回truehasOwnProperty()只在属性存在于实例中时才返回true,因此只要in操作符返回truehasOwnProperty()返回false,就可以确定属性是原型中的属性
  • 在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记的属性)的实例属性也会在for-in循环中返回

    • 要取得对象上所有可枚举的实例属性,可以使用ECMAScript 5的Object.keys()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组

      function Person(){
      }
      
      Person.prototype.name = "Nicholas";
      Person.prototype.age = 29;
      Person.prototype.job = "Software Engineer";
      Person.prototype.sayName = function(){
          alert(this.name); 
      };
      
      var keys = Object.keys(Person.prototype);
      alert(keys);       //"name,age,job,sayName"
      
      var p1 = new Person();
      p1.name = "Rob";
      p1.age = 31;
      var p1keys = Object.keys(p1);
      alert(p1keys);    //"name,age"
      
  • 如果你想要得到所有实例属性,无论它是否可枚举,都可以使用Object.getOwnPropertyNames()方法。

    var keys = Object.getOwnPropertyNames(Person.prototype);
    alert(keys);    //"constructor,name,age,job,sayName"
    
  1. 更简单的原型语法

前面例子中每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象

function Person(){
}

Person.prototype = {
    name : "Nicholas",
    age : 29,
    job: "Software Engineer",
    sayName : function () {
        alert(this.name);
    }
};
  1. 原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此

var friend = new Person();

Person.prototype.sayHi = function(){
    alert("hi");
};

friend.sayHi();   //"hi"(没有问题!)

但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数

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

  1. 原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(ObjectArrayString,等等)都在其构造函数的原型上定义了方法

  1. 原型对象的问题

原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。原型模式的最大问题是由其共享的本性所导致的。

然而,对于包含引用类型值的属性来说,问题就比较突出了

function Person(){
}

Person.prototype = {
    constructor: Person,
    name : "Nicholas",
    age : 29,
    job : "Software Engineer",
    friends : ["Shelby", "Court"],
    sayName : function () {
        alert(this.name);
    }
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Van");

alert(person1.friends);    //"Shelby,Court,Van"
alert(person2.friends);    //"Shelby,Court,Van"
alert(person1.friends === person2.friends);  //true
组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "Court"];
}

Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");
alert(person1.friends);    //"Shelby,Count,Van"
alert(person2.friends);    //"Shelby,Count"
alert(person1.friends === person2.friends);    //false
alert(person1.sayName === person2.sayName);    //true
动态原型模式

它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型

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();

这里只在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。其中,if语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆if语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用instanceof操作符确定它的类型。

寄生构造函数模式

种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数

function SpecialArray(){

    //创建数组
    var values = new Array();

    //添加值
    values.push.apply(values, arguments);

    //添加方法
    values.toPipedString = function(){
        return this.join("|");
    };

    //返回数组
    return values;
}

var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"

在这个例子中,我们创建了一个名叫SpecialArray的构造函数。在这个函数内部,首先创建了一个数组,然后push()方法(用构造函数接收到的所有参数)初始化了数组的值。随后,又给数组实例添加了一个toPipedString()方法,该方法返回以竖线分割的数组值。最后,将数组以函数值的形式返回。接着,我们调用了SpecialArray构造函数,向其中传入了用于初始化数组的值,此后又调用了toPipedString()方法。

关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式

稳妥构造函数模式

稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象

function Person(name, age, job){

    //创建要返回的对象
    var o = new Object();

    //可以在这里定义私有变量和函数

    //添加方法
    o.sayName = function(){
        alert(name);
    };    

    //返回对象
    return o;
}

变量person中保存的是一个稳妥对象,而除了调用sayName()方法外,没有别的方式可以访问其数据成员

继承

原型链

基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法

function SuperType(){
    this.property = true;
}
SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

//继承了SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue());      //true

  1. 别忘记默认的原型

所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。大家要记住,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()valueOf()等默认方法的根本原因

展示了该例子中完整的原型链

  1. 确定原型和实例的关系
  • 第一种方式是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true

    alert(instance instanceof Object);           //true
    alert(instance instanceof SuperType);        //true
    alert(instance instanceof SubType);          //true
    

    由于原型链的关系,我们可以说instanceObjectSuperTypeSubType中任何一个类型的实例。因此,测试这三个构造函数的结果都返回了true

  • 第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true

    alert(Object.prototype.isPrototypeOf(instance));           //true
    alert(SuperType.prototype.isPrototypeOf(instance));        //true
    alert(SubType.prototype.isPrototypeOf(instance));          //true
    
  1. 谨慎地定义方法

子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后

在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链

  1. 原型链的问题
  • 其中,最主要的问题来自包含引用类型值的原型,在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了

  • 原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚刚讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链

借用构造函数

有时候也叫做伪造对象或经典继承,这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数,因此通过使用apply()call()方法也可以在(将来)新创建的对象上执行构造函数

function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){  
    //继承了SuperType
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors);    //"red,blue,green"
  1. 传递参数

相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数

function SuperType(name){
    this.name = name;
}

function SubType(){  
    //继承了SuperType,同时还传递了参数
    SuperType.call(this, "Nicholas");

    //实例属性
    this.age = 29;
}

var instance = new SubType();
alert(instance.name);    //"Nicholas";
alert(instance.age);     //29
  1. 借用构造函数的问题

如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。

组合继承

组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性

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.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();           //29

var instance2 = new SubType("Greg", 27);
alert(instance2.colors);      //"red,blue,green"
instance2.sayName();          //"Greg";
instance2.sayAge();           //27
原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"

ECMAScript 5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()object()方法的行为相同

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
  • Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性

  • var person = {
        name: "Nicholas",
        friends: ["Shelby", "Court", "Van"]
    };
    
    var anotherPerson = Object.create(person, {
        name: {
            value: "Greg"
        }
    });
    
    alert(anotherPerson.name); //"Greg"
    
寄生式继承

寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象

function createAnother(original){
    var clone = object(original);    //通过调用函数创建一个新对象
    clone.sayHi = function(){        //以某种方式来增强这个对象
        alert("hi");
    };
    return clone;                    //返回这个对象
}

var person = {
    name: "Nicholas",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
寄生组合式继承

组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部

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

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);       //创建对象
    prototype.constructor = subType;                   //增强对象
    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构造函数,并且因此避免了在SubType.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceofisPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式

小结

ECMAScript支持面向对象(OO)编程,但不使用类或者接口。对象可以在代码执行过程中创建和增强,因此具有动态性而非严格定义的实体。在没有类的情况下,可以采用下列模式创建对象。

  • 工厂模式,使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。
  • 构造函数模式,可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。
  • 原型模式,使用构造函数的prototype属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。

JavaScript主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法,这一点与基于类的继承很相似。原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样就可以做到每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型。使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。

此外,还存在下列可供选择的继承模式。

  • 原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。
  • 寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。
  • 寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值