第六章 面向对象的程序设计

289 阅读19分钟

1、理解对象

数据属性

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

  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为 true

  • [[Writable]]:表示能否修改属性的值。对于直接在对象上定义的属性,这个特性的默认值为 true

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

Object.defineProperty()

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

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

把 configurable 设置为 false,表示不能从对象中删除属性。如果对这个属性调用 delete,则在非严格模式下什么也不会发生,而在严格模式下会导致错误。而且,一旦把属性定义为不可配置的, 就不能再把它变回可配置了。此时,再调用 Object.defineProperty()方法修改除 writable 之外的其他属性,都会导致错误

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

在调用 Object.defineProperty()方法时,如果不指定,configurable、enumerable 和 writable 特性的默认值都是 false。

访问器属性

访问器属性不包含数据值,但包含一对 getter 和 setter 函数(非必需)。 在读取访问器属性时,会调用 getter 函数,负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,决定如何处理数据。

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

Object.defineProperty()

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

var book = {
 	_year: 2004,//year前是下划线是一种常用的记号,用于表示只能通过对象方法访问的属性
 	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 

getter 和 setter不一定非要同时被指定 。只指定 getter 意味着属性是不能写,尝试写入属性会被忽略。 在严格模式下,尝试写入只指定了 getter 函数的属性会抛出错误。类似地,只指定 setter 函数的属性也不能读,否则在非严格模式下会返回 undefined,而在严格模式下会抛出错误。

定义多个属性

Object.defineProperties()

利用 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() 方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,该对象的属性有 configurable、enumerable、get 和 set;如果是数据属性,该对象的属性有 configurable、enumerable、writable 和 value。

Object.getOwnPropertyDescriptor() 方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用 Object.getOwnPropertyDescriptor() 方法。

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.configurable); //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" 

2、创建对象

工厂模式

这种模式抽象了创建具体对象的过程,用函数来封装以特定接口创建对象的细节。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

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

构造函数模式

可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

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

检测对象类型

  • 构造函数创建的实例对象有constructor属性,该属性指向构造函数,可用来检测对象类型

    alert(person1.constructor == Person); //true
    alert(person2.constructor == Person); //true 
    
  • instanceof 操作符可以检测对象类型,而且比constructor属性更可靠一些

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

将构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。任何函数只要通过 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" 

构造函数的缺点

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍,并且不同实例上的同名函数是不相等的。可以通过把函数定义转移到构造函数外部来解决这个问题。但是这样会导致新的问题出现:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是这个自定义的引用类型就丝毫没有封装性可言了。

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(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

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

理解原型对象

创建了一个新函数时,会为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。调用构造函数创建一个新实例后,该实例内部将包含一个指针(内部属性,称为[[Prototype]]),指向构造函数的原型对象。虽然在脚本中没有标准的方式访问[[Prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性 _proto_;而在其他实现中,这个属性对脚本则是完全不可见的。要明确的一点就是,这个连接存在于实例与构造函数的原型对象之间。 以前面使用 Person 构造函数和 Person.prototype 创建实例的代码为例

  • isPrototypeOf():确定对象之间是否存在原型连接关系。如果 [[Prototype]] 指向调用 isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回 true
  • Object.getPrototypeOf():返回 [[Prototype]] 的值。可以借助该方法通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值
  • hasOwnProperty():从Object中继承来的,用于检测一个属性是存在于实例中,还是存在于原型中。给定属性存在于对象实例中时,返回 true
alert(Person.prototype.isPrototypeOf(person1)); //true 
alert(Person.prototype.isPrototypeOf(person2)); //true 

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

alert(person1.hasOwnProperty("name")); //false 
person1.name = "Greg";
alert(person1.name); //"Greg"——来自实例
alert(person1.hasOwnProperty("name")); //true 

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性(即不会修改原型中的同名属性)。但是,使用 delete 操作符则可以完全删 除实例属性,从而让我们能够重新访问原型中的属性。

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();
person1.name = "Greg";
alert(person1.name); //"Greg"——来自实例
alert(person2.name); //"Nicholas"——来自原型

delete person1.name;
alert(person1.name); //"Nicholas"——来自原型

原型与in操作符

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

(1)、在单独使用时,无论该属性存在于实例中还是原型中,in 操作符会在通过对象能够访问给定属性时返回 true。同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中

function hasPrototypeProperty(object, name){
 	return !object.hasOwnProperty(name) && (name in object);
} 

(2)、在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。当实例与原型存在同名属性,且原型中的同名属性的 [[Enumerable]] 为false时,实例属性也会在 for-in 循环中返回

var o = {
 	toString : function(){
 	return "My Object";
 	}
};
for (var prop in o){
 	if (prop == "toString"){
 	alert("Found toString"); //会弹出警告框,即使原型的toString()方法[[Enumerable]]值为false
 	}
} 
  • Object.keys():取得所有可枚举的实例属性,接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组
  • Object.getOwnPropertyNames():取得所有实例属性,且无论是否可枚举
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" 

var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); //"constructor,name,age,job,sayName" 

更简单的原型语法

用一个包含所有属性和方法的对象字面量重写整个原型对象,可以减少不必要的输入。这样本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数,但是 instanceof 操作符还能返回正确的结果。

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

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

如果 constructor 的值真的很重要,可以在定义对象时设置适当的值( [[Enumerable]] 特性被设置为 true,默认 下,原生的 constructor 属性是不可枚举的),也可以使用 Object.defineProperty() 方法设置constructor属性的值。

function Person(){
}
//定义对象时直接给constructor设置适当的值
Person.prototype = {
 	constructor : Person,
 	name : "Nicholas",
	age : 29,
 	job: "Software Engineer",
 	sayName : function () {
 		alert(this.name);
 	}
}; 

//使用Object.defineProperty()方法设置constructor属性的值
Person.prototype = {
 	name : "Nicholas",
 	age : 29,
 	job : "Software Engineer",
 	sayName : function () {
 		alert(this.name);
 	}
}; 
//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
 	enumerable: false,
 	value: Person
});

原型的动态性

为原型添加属性和方法,能在所有对象实例中反映出来,重写整个原型对象就不能在实例对象中反映出来。调用构造函数时会为实例添加一个指向最初原型的 [[Prototype]] 指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系(实例中的指针仅指向原型,而不指向构造函数)

function Person(){
};
var friend = new Person();
//为原型对象添加属性
Person.prototype.sayHi = function(){
 	alert("hi");
};
friend.sayHi(); //"hi"

//重写整个原型对象
Person.prototype = {
 	constructor: Person,
 	name : "Nicholas",
 	age : 29,
 	job : "Software Engineer",
 	sayName : function () {
 		alert(this.name);
 	}
};
friend.sayName(); //error 

原生对象的原型

通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。(但是不推荐,可能会导致命名冲突)

String.prototype.startsWith = function (text) {
 	return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true 

原型对象的缺点

对于包含引用类型值的属性来说,在修改实例的属性时,本质上是在修改原型上的属性,所以会导致修改同步到所有的实例中。

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,//定义constructor
 	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(); 

寄生构造函数模式

创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实 是一模一样的。

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

function Person(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 friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas" 
alert(friend instanceof Person);//false

稳妥构造函数模式

稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序(如 Mashup 程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,因此 instanceof 操作符对这种对象也没有意义,但有两点不同:一是新创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数。

下例中,除了使用 sayName()方法之外,没有其他办法访问 name 的值。

function Person(name, age, job){

 //创建要返回的对象
 	var o = new Object(); 
 //可以在这里定义私有变量和函数
    
 //添加方法
 	o.sayName = function(){
 		alert(name);
 	};

 //返回对象
 	return o;
}

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

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

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

确定原型和实例的关系

  • instanceof 操作符

    alert(instance instanceof Object); //true
    alert(instance instanceof SuperType); //true
    alert(instance instanceof SubType); //true 
    
  • isPrototypeof() 方法

    alert(Object.prototype.isPrototypeOf(instance)); //true
    alert(SuperType.prototype.isPrototypeOf(instance)); //true
    alert(SubType.prototype.isPrototypeOf(instance)); //true 
    

使用原型应该注意的问题

给原型添加方法或修改方法的代码一定要放在替换原型的语句之后。

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();
alert(instance.getSuperValue()); //false
var instance1 = new SuperType();
console.log(instance1.getSuperValue()); //true

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

原型链的缺点

  • 在通过原型来实现继承时,原型实际上会变成另一个类型的实例,所以就会导致原型属性被共享

    function SuperType(){
     	this.colors = ["red", "blue", "green"]; 
    }
    
    function SubType(){
    }
    
    //继承了 SuperType
    SubType.prototype = new SuperType();
    
    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,black" 
    
  • 在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

借用构造函数

在子类型构造函数的内部调用超类型构造函数,通过使用 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" 

传递参数

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

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 

借用构造函数的问题

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

组合继承

组合继承(伪经典继承),将原型链和借用构造函数的技术组合到一块,即使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

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

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(); //29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27 

原型式继承

Object.create() 方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下, 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");
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" 

寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。

function object(o){
 	function F(){}
 	F.prototype = o;
 	return new F();
} 
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 object(o){
 	function F(){}
 	F.prototype = o;
 	return new F();
} 
function inheritPrototype(subType, superType){
 	var prototype = object(superType.prototype); //创建对象
 	prototype.constructor = subType; //增强对象
 	subType.prototype = prototype; //指定对象
}
/* inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。*/
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);
};