ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数”。严格来讲,这就相当于说对象是一组没有特定顺序的值。

1 理解对象

创建对象:

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

1.1 属性类型

ECMA-262第5版在定义只有内部才用的特性时,描述了属性的各种特性。ECMA-262定义这些特性是为了实现JavaScript引擎用的,因此在JavaScript中不能直接访问它们。
ECMAScript中有两种属性:数据属性访问器属性

1.1.1 数据属性

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

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性的属性值为true
  • [[Enumerable]]:表示能否通过for-in循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性的默认值为true
  • [[Writable]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined

要修改属性默认的特性,必须使用ECMAScript 5Object.defineProperty()方法:

var person = {};
Object.defineProperty(person, "name", {
    writable : false,
    value : "Nicholas"
});

console.log(person.name);    //"Nicholas"
person.name = "Greg";
console.log(person.name);    //"Nicholas"

在严格模式下,上面的赋值操作将会导致抛出错误

var person = {};
Object.defineProperty(person, "name", {
    configurable : false,
    value : "Nicholas"
});

console.log(person.name);    //"Nicholas"
delete person.name;
console.log(person.name);    //"Nicholas"

一旦把属性定义为不可配置的,就不能再把它变回可配置了:

var person = {};
Object.defineProperty(person, "name", {
    configurable : false,
    value : "Nicholas"
});

Object.defineProperty(person, "name", {
    configurable : true,    //抛出错误
    value : "Nicholas"
});

也就是说,可以多次调用Object.defineProperty()方法修改同一个属性,但在把configurable特性设置为false之后就会有限制了

注意!利用Object.defineProperty()方法创建一个新的属性时,如果不指定,configurableenumerablewritable特性的默认值都是false

1.1.2 访问器属性

访问器有以下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(){
        if(newValue > 2004){
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
});

book.year = 2005;
console.log(book.edition);    //2

不一定非要同时指定getter和setter。只指定getter意味着属性是不能写,尝试写入属性会被忽略。

1.2 定义多个属性

  • Object.defineProperties():利用这个方法可以通过描述符一次定义多个属性
var book = {};

Object.defineProperties(book, {
    _year : {
        writable : true,
        value : 2004
    },

    edition : {
        writable : true,
        value : 1
    },

    year : {
        get : function(){
            return this._year;
        },

        set : function(){
            if(newValue > 2004){
                this._year = newValue;
                this.edition += newValue - 2004;
            }
        }
    }
});

1.3 读取属性的特性

  • Object.getOwnPropertyDescriptor():可以取得给定属性的描述符
//接上段代码
    var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
    console.log(descriptor.value);                //2004
    console.log(descriptor.configurable);    //false
    console.log(typeof descriptor.get);        //"undefined"

    var descriptor = Object.getOwnPropertyDescriptor(book, "year");
    console.log(descriptor.value);                //undefined
    console.log(descriptor.enumerable);        //false
    console.log(typeof descriptor.get);        //"function"

对于访问器属性yearget是一个指向getter函数的指针

2 创建对象

Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显缺点:使用同一个接口创建很多对象,会产生大量的重复代码。为解决这个问题,人们开始使用工厂模式的一种变体。

2.1 工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到在ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节:

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

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

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

2.2 构造函数模式

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

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

构造函数模式与工厂模式的不同之处:

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

按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。

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

  • 创建一个对象;
  • 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
  • 执行构造函数中的代码(为这个新对象添加属性);
  • 返回新对象。
console.log(person1.constructor == truPersone);   //true
console.log(person2.constructor == Person);   //true

console.log(person1 instanceof Object);     //true
console.log(person1 instanceof Person);     //true
console.log(person2 instanceof Object);     //true
console.log(person2 instanceof Person);     //true

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方

这种方式定义的构造函数是定义在Global对象(在浏览器中是window对象)中的

2.2.1 将构造函数当作函数

//当作构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();   //"Nicholas"

//作为普通函数调用
Person("Greg", 27, "Doctor");   //严格模式下会抛出错误!
window.sayName();   //"Greg"

//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName();    //"Kristen"

2.2.2 构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例

不要忘了——ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。从逻辑角度讲,此时的构造函数也可以这样定义:

function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = new Function("console.log(this.name);");    //此声明函数在逻辑上是等价的    
}

console.log(person1.sayName == person2.sayName);    //false

创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上。于是可以将函数定义转移到构造函数外部来解决这个问题:

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

function sayName() {
  console.log(this.name);
};

这样做解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。更重要的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,原型模式可以解决这些问题

2.3 原型模式

function Person() {
}

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

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

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

console.log(person1.sayName == person2.sayName);    //true

2.3.1 理解原型对象

各个对象之间的关系

console.log(Person.prototype.isPrototypeOf(person1));   //true
console.log(Person.prototype.isPrototypeOf(person2));   //true

ECMAScript 5增加了一个新方法,叫Object.getPrototypeOf(),这个方法返回[[Prototype]]的值

console.log(Object.getPrototypeOf(person1) == Person.prototype);    //true
console.log(Object.getPrototypeOf(person1).name);   //"Nicholas"

不能通过对象实例重写原型中的值。如果在实例中添加一个属性,而该属性与实例原型中的一个属性同名,那么该属性将会屏蔽原型中的那个属性:

function Person() {
}

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

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

console.log(person1.hasOwnProperty("name"));    //false
console.log("name" in person1);    //true

person1.name = "Greg";
console.log(person1.name);    //"Greg"——来自实例
console.log(person1.hasOwnProperty("name"));    //true
console.log("name" in person1);    //true

console.log(person2.name);    //"Nicholas"——来自原型
console.log(person2.hasOwnProperty("name"));    //false

delete person1.name;
console.log(person1.name);    //"Nicholas"——来自原型
  • in操作符只要通过对象能够访问到属性就返回true
  • 使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例中时,才会返回true
function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object);
}
  //返回true则表明该属性存在于原型中
  //返回false则表明该属性存在于实例中

在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的属性,其中包括实例和原型中的属性。

  • ECMAScript 5Object.keys()方法可以取得对象上所有可枚举的实例属性
var keys = Object.keys(Person.prototype);
alert(keys);    //"name, age, job, sayName"
//keys中保存一个数组,数组中是字符串"name, age, job, sayName"。
  • Object.getownPropertyNames()可以获得所有实例属性,无论它是否可枚举
var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys);    //"constructor, name, age, job, sayName"

2.3.3 更简单的原型语法

function Person() {
}

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

上面代码将Person.prototype设置为等于一个以对象字面量形式创建的新对象。结果相同,但constructor属性不再指向Person了。因此上面使用的语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数

var friend = new Person();
console.log(friend instanceof Object);    //true
console.log(friend instanceof Person);    //true
console.log(friend.constructor == Object);    //false
console.log(friend.constructor == Person);    //true

如果constructor的值真的很重要,可以像下面这样特意将它设置回恰当的值:

function Person() {
}

Person.prototype = {
  constructor : Person
  //……
};

这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true,默认情况下,原声的constructor属性是不可枚举的

因此如果你使用兼容ECMAScript 5JavaScript引擎,可以试一试Object.defineProperty()

Object.defineProperty(Person.prototype, "constructor", {
  enumerable : false,
  value : Person
});

2.3.4 原型的动态性

在原型中查找值的过程是一次搜索

var friend = new Person();
Person.prototype.sayHi = function(){
    console.log("hi");
}

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

重写原型对象:

function Person(){
}

var friend = new Person();
Person.prototype = {
  constructor : Person,
  name : "Nicholas",
  age : 29,
  job : "Software Engineer",
  sayName : function () {
    console.log(this.name);
  }
};
friend.sayName();   //error

重写原型对象

重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型

2.3.5 原生对象的原型

所有原生引用类型(Object、Array、String等)都在其构造函数的原型上定义了方法

console.log(typeof Array.prototype.sort);    //"function"
console.log(typeof String.prototype.substring);    //"function"

给原生对象的原型添加方法:

String.prototype.startsWith = function(text){
    return this.indexOf(text) == 0;
}

var msg = "Hello world!";
console.log(msg.startsWith("Hello"));    //true

2.3.6 原型对象的问题

对于包含引用类型值得属性来说,可能出现以下问题:

function Person() {
}

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

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

person1.friends.push("Van");

console.log(person1.friends);
console.log(person2.friends);
console.log(person1.friends === person2.friends);   //true

2.4 组合使用构造函数模式和原型模式

实例属性都在构造函数中定义,所有实例共享的属性和方法都在原型中定义:

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

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

这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式

2.5 动态原型模式

有其他OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正式致力于解决这个问题的一个方案

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

  //方法
  if(typeof this.sayName != "function"){
    Person.prototype.sayName = function () {
      console.log(this.name);
    }
  }
}

if语句检查的可以是初始化之后应该存在的任何属性和方法——不必用一大堆if语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式的对象,还可以使用instanceof操作符确定它的类型

2.6 寄生构造函数模式

通常,在前述的几种模式都不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数:

function Person(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function () {
    console.log(this.name);
  };
  return o;
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();   //"Nicholas"

构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式:

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");
console.log(colors.toPipedString());    //"red|blue|green"

注意:返回的对象与构造函数或者与构造函数的原型属性直接没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖instanceof操作符来确定对象类型。

由于存在上述问题,建议在可以使用其他模式的情况下,不要使用这种模式。

2.7 稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最合适在一些安全的环境中(这些环境中会禁止使用thisnew),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。

function Person(name, age, job) {
  //创建要返回的对象
  var o = new Object();
  //可以在这里定义私有变量和函数

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

  return o;
}

var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName();   //"Nicholas"

变量friend中保存的是一个稳妥对象,而除了调用sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。

稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境下使用——例如,ADsafeCaja提供的环境

3 继承

许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的

3.1 原型链

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();
console.log(instance.getSuperValue());    //true

通过原型链继承

3.1.1 别忘记默认的原型

所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。
默认原型Object

3.1.2 确定原型和实例的关系

  • instanceof:只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true
console.log(instance instanceof Object);      //true
console.log(instance instanceof SuperType);   //true
console.log(instance instanceof SubType);     //true
  • isPrototypeOf:只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此该方法也会返回true
console.log(Object.prototype.isPrototypeOf(instance));      //true
console.log(SuperType.prototype.isPrototypeOf(instance));   //true
console.log(SubType.prototype.isPrototypeOf(instance));     //true

3.1.3 谨慎地定义方法

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

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;
};

//重写超类型中的方法
SubType.prototype.getSuperValue = function(){
    return false;
}

var instance = new SubType();
console.log(instance.getSuperValue());    //false

getSuperValue()是原型链中已经存在的一个方法,重写这个方法将会屏蔽原来的那个方法。当通过SubType的实例调用getSuperValue()时,调用的就是这个重新定义的方法;但通过SuperType的实例调用getSuperValue()时,还会继续调用原来的那个方法

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

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

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

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

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

//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
    //……
}

var instance = new SubType();
console.log(instance.getSuperValue());    //error

3.1.4 原型链的问题

包含引用类型值的原型会被所有实例共享。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就变成了现在的原型属性了

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

function SubType(){
}

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

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

var instance2 = new SubType();
console.log(instance2.colors);    //"red,blue,green,black"

SubType的所有实例都会共享这一个colors属性。

原型链的第二个问题:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

3.2 借用构造函数

在子类型构造函数的内部调用超类型构造函数

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

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

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

var instance2 = new SubType();
console.log(instance2.colors);    //"red,blue,green"

3.2.1 传递参数

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

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

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

var instance = new SubType();
console.log(instance.name);   //"Nicholas";
console.log(instance.age);    //29

为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性

3.3 组合继承

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceofisPrototypeOf()也能狗用于识别基于组合继承创建的对象。

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

SuperType.prototype.sayName = function (){
  console.log(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 (){
  console.log(this.age);
}

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors);    //["red", "blue", "green", "black"]
instance1.sayName();    //Nicholas
instance1.sayAge();     //29

var instance2 = new SubType("Greg", 27);
console.log(instance2.colors);   //["red", "blue", "green"]
instance2.sayName();    //Greg
instance2.sayAge();     //27

3.4 原型式继承

原型式继承并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型

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

console.log(person.friends);    //["Shelby", "Court", "Van", "Rob", "Barbie"]

object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()队传入其中的对象执行了一次浅复制

ECMAScript 5通过新增Object.create()方法规范化了原型式继承:

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

console.log(person.friends);    //["Shelby", "Court", "Van", "Rob", "Barbie"]

在传入一个参数的情况下,Object.create()object()方法的行为相同

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

var anotherPerson = Object.create(person, {
    name : {
        value : "Greg"
    }
});

console.log(anotherPerson.name);    //"Greg"

Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性

在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型试继承时完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。

3.5 寄生式继承

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

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

可以像下面这样来使用createAnother()函数:

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

var anotherPerson = createAnother(person);
anotherPerson.sayHi();    //"hi"

新对象不仅具有person的所有属性和方法,而且还有自己的sayHi()方法

在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的object()函数不是必须的;任何能够返回新对象的函数都适用于此模式

3.6 寄生组合式继承

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法
寄生组合式继承的基本模式如下:

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 (){
  console.log(this.name);
};

function SubType(name, age){
  SuperType.call(this, name);
  this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function (){
  console.log(this.age);
};

这个例子的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了再SubType.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceofisPrototypeOf()

开发人员普遍认为寄生组合式继承时引用类型最理想的继承方式。


Cor
63 声望9 粉丝