面向对象的程序设计
6.1 理解对象
属性类型
ECMASript中有两种类型的属性:数据属性和访问器属性。
数据属性:Configurable
(表示能够通过delete删除属性而重新定义属性)、Enumerable
(表示能否通过for-in遍历返回属性)、Writable
(表示能否修改属性的值)、Value
(包含这个属性的数据值)
Object.defineProperty()
方法:修改上面四个默认的属性特性。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符对象的属性必须是Configurable
、Enumerable
、Writable
和Value
。
注意:在调用Object.defineProperties()
和Object.defineProperty()
方法时,Configurable
、Enumeraber
和Writable
三个属性默认为false
。而不通过Object.defineProperties()
和
Object.defineProperty()
方法,直接创建对象时,这三个属性默认为true
。
访问器属性:不包含数据值,包含一对setter和getter函数。访问器属性有如下四个特性:configurable
(表示能够通过delete
删除属性而重新定义属性)、Enumerable
(表示能否通过for-in遍历返回属性)、Get(在读取属性时调用的函数)、Set(在写入属性时调用的函数)
访问器属性不能直接定义,必须通过Object.defineProperty()
方法定义。下面看一个示例:
var book = {
_year: 2014,
/*属性前面的下划线是常用的记号,用于表示只能通过对象方法访问的属性*/
edition: 1
};
Object.defineProperty(book, "year", {
get: function () {
return this._year;
},
set: function (v) {
if (v > 2014) {
this._year = v;
this.edition = 100;
}
}
});
book.year = 2015;
alert(book.edition);
这也是访问器属性常见的用法,即设置一个属性的值,另一个属性的值发生变化。
定义多个属性
Object.defineProperties()
方法,和上面的类似。
Object.defineProperties(book, {
_year: {
value: 2014
},
edition: {
value: 1
},
year: {
get: function () {
return this._year;
},
set: function (v) {
if (v > 2014) {
this._year = v;
this.edition = 100;
}
}
}
});
读取对象的属性
使用Object.getOwnPropertyDescriptor()
方法。
var book = {};
Object.defineProperties(book, {
_year: {
value: 2014
},
edition: {
value: 1
},
year: {
get: function () {
return this._year;
},
set: function (v) {
if (v > 2014) {
this._year = v;
this.edition = 100;
}
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(book,"_year");
alert(descriptor.value); //2014
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(一个指向get函数的指针)
6.2 创建对象
工厂模式:工厂模式不必多说,在Java中用的很多。但是在JavaScript中,工厂模式虽然解决了创建多个对象的问题,却没有解决对象识别的问题(即怎样知道一个对象的具体类型)。
构造函数模式:类似于Java中的构造器(有参构造器和无参构造器)。
构造函数的函数名首字母大写。
每个通过构造函数创建的对象都有一个constructor
(构造函数)属性。看一下例子:
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
var person1 = new Person("Ethan", 22, "man");
var person2 = new Person("James", 20, "man");
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
当然,若要识别对象类型,最好还是使用instanceof
操作符。
将构造函数当做函数
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
//当做构造函数使用
var person1 = new Person("Ethan", 22, "man");
alert(person1.name); //"Ethan"
//当做普通函数使用
Person("James", 22, "man"); //在全局作用域中调用,添加到window
alert(window.name); //"James"
//在另一个对象的作用域中使用
var o = new Object();
Person.call(o, "Ma", 22, "man");
alert(o.name); //"Ma"
原型模式
创建的每个函数都有一个prototype
(原型)属性。原型对象默认只有一个属性,就是constructor
属性。
脚本中没有标准的方式访问prototype
属性,但是在Firefox、Safari和Chrome上每个对象都支持一个属性_proto_
,这个属性对脚本是完全不可见的,要明确的一点是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
虽然可以通过对象实例访问保存在原型对象中的值,但却不能通过对象实例重写原型中的值。如果在实例中添加了一个属性,而这个添加的属性的属性名和原型的属性名相同,该属性则会屏蔽原型中的拿个属性。看下面的例子:
function Person() {
Person.prototype.name = "Ethan";
Person.prototype.age = 22;
Person.prototype.sayName = function () {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.name = "James";
person1.sayName(); //James 来自对象实例
person2.sayName(); //Ethan 来自原型
之所以会产生这样的结果,是因为当代码读取某个对象的某个属性时,会首先在对象实例中搜索这个属性(根据属性名),如果搜索到则返回读取结果,如果没有搜索到,则会在该对象的原型中搜索这一属性(根据属性名),如果在原型中搜索到则返回相应的读取结果,如果没有搜索到,则读取失败。
换句话说,重名属性是实例的属性,与原型属性无关,它只是阻止了访问原型属性,而不会修改原型属性。下面是使用了delete
操作符删除掉上面对象实例的name
属性,重新访问,访问到原型中name
的值:
function Person() {
Person.prototype.name = "Ethan";
Person.prototype.age = 22;
Person.prototype.sayName = function () {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
// person1.name = "James";
person1.sayName(); //James 来自对象实例
person2.sayName(); //Ethan 来自原型
delete person1.name;
person1.sayName(); //Ethan 来自原型
delete
要删除的对象属性。
判断一个属性是对象实例的属性还是原型属性:使用hasOwnPrototype()
方法。
对象实例.hasOwnPrototype(“属性名”)——如果是实例属性则返回true
,如果是原型属性则返回false
。看一下示例代码:
function Person() {
Person.prototype.name = "Ethan";
Person.prototype.age = 22;
Person.prototype.sayName = function () {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.name = "James";
alert(person1.hasOwnProperty("name")); //true
alert(person2.hasOwnProperty("name")); //false
关于实例属性和原型属性,可以看书中的图6-2,可以说是非常形象了。
原型与in操作符
除了在for-in中使用in
操作符,in
操作符可以单独使用,in操作符会在通过对象能够访问给定属性时返回true
,无论该属性存在于实例中还是原型中。看一下示例:
function Person() {
Person.prototype.name = "Ethan";
Person.prototype.age = 22;
Person.prototype.sayName = function () {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.name = "James";
alert("name" in person1); //true 来自实例
alert("name" in person2); //true 来自原型
结合in
操作符和hasOwnPrototype()
方法可以判断出属性是在对象实例中还是原型中。示例:
/**
* hasPrototypeProperty()方法
* 返回false则表明name是object的实例属性
* 返回true则表明name是object的原型属性
**/
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
function Person() {
Person.prototype.name = "Ethan";
Person.prototype.age = 22;
Person.prototype.sayName = function () {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.name = "James";
alert(hasPrototypeProperty(person1, "name")); //实例属性 返回false
alert(hasPrototypeProperty(person2, "name")); //原型属性 返回true
Object.keys()
方法:
传入一个对象作为参数,返回该参数的可枚举属性。
function Person() {
Person.prototype.name = "Ethan";
Person.prototype.age = 22;
Person.prototype.sayName = function () {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.name = "James";
alert(Object.keys(Person.prototype)); //原型的可枚举属性 name age sayName
alert(Object.keys(person1)); //对象实例的可枚举属性 name
Object.getOwnPropertyNames()
方法:
传入一个对象作为参数,返回他的所有实例属性,包括不可枚举的属性(例如constructor)。
function Person() {
Person.prototype.name = "Ethan";
Person.prototype.age = 22;
Person.prototype.sayName = function () {
alert(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.name = "James";
alert(Object.getOwnPropertyNames(Person.prototype));
//constructor、name、age、sayName
如上所示,constructor被打印出来,但是它不可枚举。
更简单的原型语法
将Person.prototype设置为一个字面量对象(重写Person.prototype)
function Person() {
}
Person.prototype = {
name : "Ethan",
age : 22,
sayName : function () {
alert(this.name);
}
};
但是这样做也产生了一些问题,即Person原型的constructor不再指向Person,而是指向Object:
function Person() {
}
Person.prototype = {
name : "Ethan",
age : 22,
sayName : function () {
alert(this.name);
}
}
var person1 = new Person();
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person1.constructor == Object); //true
alert(person1.constructor == Person); //false
可以看到,constructor不再指向原型,而是指向了Object。
使用instanceof
确定对象实例的类型时,还是没错的,结果依然正确,这说明instanceof
识别对象不是根据对象实例的constructor
来进行识别的,如果constructor
属性很重要,可以显式的指定constructor
的指向:
function Person() {
}
Person.prototype = {
constructor : Person,
name : "Ethan",
age : 22,
sayName : function () {
alert(this.name);
}
}
var person1 = new Person();
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person1.constructor == Object); //false
alert(person1.constructor == Person); //true
但是这样一来就产生了新的问题,constructor
属性被显式的指定为Person以后,constructor
这个属性变为了可枚举的属性(使用for-in循环可以遍历到这个属性),而原生的constructor
属性的Enumerable
特性是不可枚举的,如果JavaScript引擎兼容ECMAScript5的话,可以试一试Object.defineProperty()
方法:
function Person() {
}
Person.prototype = {
name : "Ethan",
age : 22,
sayName : function () {
alert(this.name);
}
}
/**
* 在兼容ECMAScript5的浏览器上重设constructor属性的Enumerable特性
* */
Object.defineProperty(Person.prototype,"constructor",{
Enumerable : false,
value : Person
});
alert(Object.keys(Person.prototype)); //name age sayName
我的浏览器是Firefox58.0,兼容ECMAScript5,所以显示正常。
原型的动态性
在原型中查找值的过程是一次搜索,在原型上做的任何修改都能立刻从实例上反映出来,即使先创建了对象实例后修改了原型属性。
function Person() {
}
Person.prototype = {
name : "Ethan",
age : 22,
sayName : function () {
alert(this.name);
}
}
var person1 = new Person(); //先创建实例
Person.prototype.sayHi = function () {
alert("Hi!"); //为原型添加了一个sayHi的函数
};
person1.sayHi(); //Hi!
尽管可以随时为原型添加属性和方法,修改也能够立即在实例中反映出来,但如果是重写了整个原型对象,那结果就不一样了。调用构造函数会为对象实例添加一个指向原型的指针,这个指针指向原型而非构造函数,为原型添加或者修改属性方法都是在原本的原型上做的操作,而当我们重写整个原型时,实例中的指针依然指向未修改过的原型而非重写后的原型,重写了原型就相当于切断了构造函数与最初原型之间的联系。
function Person() {
}
var person1 = new Person(); //先创建实例
Person.prototype = { //重写原型对象
name : “Ethan”,
age : 22,
sayName : function () {
alert(this.name);
}
}
person1.sayName(); //error
使用Firefox的开发者工具调试报错:TypeError: person1.sayName is not a function
原书中的图6-3很详细的说明了重写原型之后的实例、新旧原型、构造函数之间的关系。
原生对象的原型(Object、String等)
不建议修改原生对象的原型
原型对象的问题
原型中的属性和方法是可以被所有的对象实例共享的,对于包含基本类型值的属性这些共享并非麻烦,然而,对于引用类型的属性来说,问题就比较突出了:
function Person() {
}
Person.prototype = { //重写原型对象
name : "Ethan",
age : 22,
sayName : function () {
alert(this.name);
},
friends : ["小明","小王","小李"]
}
var p1 = new Person();
var p2 = new Person();
alert(p1.friends); //"小明","小王","小李"
alert(p2.friends); //"小明","小王","小李"
p1.friends.push("小张"); //为p1的friends数组添加一个元素
alert(p1.friends);
alert(p2.friends); //p2的friends也发生了变化,而这种变化不是我们想看到的
很明显,p1的friends添加一个元素之后,p2的friends也添加了同样的元素,这不是我们想看到的结果。因此,对于原型中引用类型的属性,一定要慎重,正如书中所说,尽量不要单独使用原型模式。
组合使用构造函数模式和原型模式
创建自定义类型的最常见的方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义共享的属性(一般是基本数据类型的属性)和方法。这样做的好处不仅避免了单独使用构造函数模式或者原型模式的缺陷,更在最大限度上节省了内存。另外,这种混成模式还可以向构造函数添加参数,可谓是集两种模式之长。下面的代码重写了下面的例子:
function Person(name,age,sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.friends = ["小明","小王"];
}
Person.prototype = {
constructor : Person,
sayName : function () {
alert(this.name);
}
};
var p1 = new Person("Ethan", 22, "male");
var p2
= new Person("Jenry", 20, "female");
alert(p1.friends);
alert(p2.friends);
alert(p1.friends === p2.friends); //false
alert(p1.sayName() === p2.sayName()); //true
动态原型模式
使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例和新原型的联系。示例:
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
if (typeof this.sayName != "function") {
Person.prototype.sayName = function () {
alert(this.name);
}
}
}
var p1 = new Person("Ethan", 22, "male");
var p2
= new Person("Jenry", 20, "female");
p1.sayName(); //Ethan
动态原型模式:即在构造函数中判断是否有应该存在的属性或者方法,如果没有就应该动态的添加。
寄生构造函数模式
这种模式有点类似于工厂模式,但是返回的对象与构造函数没有关系,与构造函数的原型也没有关系,与在构造函数外部创建一个对象没什么不同,也不能使用instanceof
来确定对象实例的类型,一般不使用这种模式。
稳妥构造函数模式
JavaScript中的稳妥对象(durable objects):指没有公共属性,而且其方法也不引用this
的对象。稳妥对象最适合在一些安全的环境中(这些环境会禁用this
和new
),或者在防止数据被其他应用程序(如Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建的对象的实例方法不引用this
;二是不使用new
操作符调用构造函数。示例:
function Person(name,age) {
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function () {
alert(name);
};
return o;
}
var person = Person("Ethan", 22);
person.sayName(); //Ethan
除了使用sayName()
方法,没有办法可以访问到传入构造函数中的数据,即使在构造函数外有代码给这个对象添加方法和数据成员,但也不可能有别的办法访问到传入构造函数中的原始数据。
与寄生构造函数模式类似,使用稳妥构造函数模式创建对象也和构造函数之间没有什么关系,因此instanceof
操作符对这种对象也没有什么意义。
6.3 继承
原型链
许多OO语言都实现了继承:接口继承(方法签名继承)和实现继承(继承实际的方法)。
ECMAScript没有方法签名,因此只有实现继承,主要是依靠原型链来实现的。实现原型链有一种基本的模式,代码如下:
//父类SuperType的构造函数
function SuperType() {
this.property = true;
};
SuperType.prototype.getSuperValue = function () {
return this.property;
};
//子类SubType的构造函数
function SubType() {
this.subProperty = false;
}
//实现继承(实际上就是重写SubType的原型对象)
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subProperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
两个对象分别有一个属性和方法。
继承实现的本质:重写子类的原型对象
原书中图6-4非常重要。
构造函数、原型和实例之间的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
一个完整的原型链应该如原书中图6-5所示。所有引用类型都默认继承了Object,而继承都是通过原型链实现的。
所有函数的默认原型都是Object的实例,因此默认原型都会有一个内部指针,指向Object的原型(Object.prototype
),这也正是所有的自定义类型都会继承toString()
、valueOf()
等默认方法原因。
使用instanceof
操作符确认实例和原型之间的关系。只要实例的构造函数在原型链中出现过,结果就会返回true
。
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
还有一种方法:isPrototypeOf()
方法
alert(SubType.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(Object.prototype.isPrototypeOf(instance)); //true
谨慎的定义方法:子类型有时候会重写超类型的某个方法或添加超类型中不存在的某个方法。无论哪种操作,都要放在继承替换原型之后。示例:
//父类SuperType的构造函数
function SuperType() {
this.property = true;
};
SuperType.prototype.getSuperValue = function () {
return this.property;
};
//子类SubType的构造函数
function SubType() {
this.subProperty = false;
}
//实现继承(实际上就是重写SubType的原型对象)
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
重写的方法屏蔽了原来的方法,调用方法时,搜索到SubType的prototype
即可找到对应名称的方法,而不会继续向上寻找。
实例和实例之间是不同的,为SubType的prototype
重写了继承来的同名方法getSuperValue(),不会对其他SuperType的实例造成任何影响,仅仅是SubType的prototype
中有这一个方法,新的SuperType实例依然会继承来自SuperType.prototype的getSuperValue()方法。
通过原型链实现继承时,本质就是对象构造函数的原型重写,不可以再使用创建对象字面量的方法再次重写原型方法,后面的重写会覆盖前面的重写。
原型链的问题:
一是父类型的构造函数中有引用类型的属性时,子类型的原型是父类型的一个实例,那么子类型的原型中也就有了这个引用类型的属性,则所有子类型的对象实例共享了这一引用类型的属性。
二是在创建子类型的对象实例时,不能向超类型的构造函数传递参数。
有鉴于上面两个问题,实际开发中很少会单独使用原型链。
借用构造函数(constructor stealing):也叫伪造对象或经典继承。
用法很简单:在子类型构造函数的内部调用父类型的构造函数
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
//借用构造函数继承
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("yellow");
var instance2 = new SubType();
alert(instance1.colors); //red blue green yellow
alert(instance2.colors); //red blue grern
借用call()
或者apply()
方法改变函数运行环境,实现继承。
传递参数:借用构造函数模式可以在子类型构造函数中向父类型的构造函数中传递参数。
示例:
function SuperType(name) {
this.name = name;
}
function SubType() {
//借用构造函数继承
SuperType.call(this,"Ethan");
//子类型实例属性
this.age = 22;
}
var instance = new SubType();
alert(instance.age); //22
alert(instance.name); //Ethan
参数name
通过call()
方法传递到父类型的构造函数中。需要注意的是先调用父类型的构造函数再写实例属性,这样做的目的是确保父类型的构造函数不会重写子类型的实例属性。
借用构造函数的问题:如果仅仅是借用构造函数,还是无法避免构造函数模式中的问题——方法也在构造函数中定义,那么方法复用也就无从谈起了。而且在超类型的构造函数的原型中定义的方法,子类型的实例也访问不到,因为子类型的构造函数的原型实例是默认没有这个方法的,在子类型的构造函数中调用了父类型的构造函数,但不会在搜索属性(或者方法)时搜索到父类型构造函数的原型,只会搜索子类型构造函数的原型(即默认的原型)。这有点像把父类型构造函数中的代码移植到了子类型构造函数中。
有鉴于以上的问题,借用构造函数模式也很少单独使用。
组合继承(combinationinheritance):有时也叫做伪经典继承,指的是将原型链和借用构造函数模式的技术组合到一起,从而发挥两者之长的一种模式。其思路是:使用原型链进行对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承。示例:
function SuperType(name) {
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
return this.name;
};
function SubType(name, age) {
//借用构造函数继承
SuperType.call(this, name);
//子类型实例属性
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function () {
return this.age;
}
var instance1 = new SubType("Ethan", 22);
instance1.colors.push("yellow");
alert(instance1.colors); //"red", "blue", "green","yellow"
alert(instance1.sayName()); //Ethan
alert(instance1.sayAge()); //22
var instance2 = new SubType("James", 20);
alert(instance2.colors); //"red", "blue", "green"
alert(instance2.sayName()); //James
alert(instance2.sayAge()); //20
组合继承的重点在于继承方法,被继承的方法(在这里是sayName方法)被写在了父类型构造函数的原型中,如果要继承到这个方法,就必须将子类型构造函数的原型赋值为父类型的实例(原型链)。
组合继承是JavaScript中最常用的一种继承方式。
原型式继承
没有严格意义上的构造函数,基于现有的对象实现继承。要求必须要有一个对象作为另一个对象的基础。ECMAScript新增的Object.create()
方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和一个为新对象定义额外属性的对象(这个参数是可选的)。在只传入一个参数的情况下,Object.create()
和object()
是相同的。示例:
var person = {
name : "Ethan",
friends: ["friend1","friend2","friend3"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("friend4");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("friend5");
alert(person.friends); //friend1,friend2,friend3,friend4,friend5
Object.create()
方法的第二个参数和·Object.defineProperty()·方法的参数一样,同时通过属性自己的描述符定义的,以这种方式定义的任何属性都会覆盖原型上的同名属性:
var person = {
name : "Ethan",
friends: ["friend1","friend2","friend3"]
};
var anotherPerson = Object.create(person,{
name : {
value : "Greg"
}
});
alert(anotherPerson.name); //Greg
和使用原型模式一样,传入的第一个参数如果包含了引用类型的值(如一个数组),则这个引用类型的值将被所有对象实例共享。
寄生式继承(parasiticinheritance):思路和寄生式构造函数与工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。示例:
function createPerson(original) {
var clone = Object.create(original); //通过调用函数来创建一个新对象
clone.sayHi = function () { //以某种方式来增强这个对象
alert("Hi!");
}
return clone; //返回这个对象
}
var person = {
name : "Ethan",
friends: ["friend1","friend2","friend3"]
};
var anotherPerson = createPerson(person);
anotherPerson.sayHi();
注意:这里的sayHi()方法是一个对象实例方法,任何实例都可以重写他但不会影响到其他实例。
使用寄生式继承为对象添加函数,因为不能够做到函数复用而降低效率,这一点和构造函数类似。
下面是添加了一个可以复用的函数sayHi():
function createPerson(original) {
var clone = Object.create(original); //通过调用函数来创建一个新对象
return clone; //返回这个对象
}
var person = {
name : "Ethan",
friends: ["friend1","friend2","friend3"],
sayHi : function () {
alert("Hi!");
}
};
var person1 = createPerson(person);
person1.sayHi();
将要复用的函数写入Person中,Person作为需要创建的对象的原型,那么新创建的对象即可复用函数。
寄生式组合继承
组合继承中,由于调用了两次父类型的构造函数,会导致新对象的实例属性覆盖新对象原型中的同名属性。组合继承过程中会产生两组同名属性:新对象原型中一组(原型属性),新对象中一组(实例属性)。
解决方法就是寄生式组合继承:通过借用构造函数来继承属性,通过原型链的混成方式来继承方法。我们所需要的,就是父类型的原型副本而已。本质上,就是使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。示例:
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = ["yellow", "green", "blue"];
}
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);
};
寄生组合式继承的高效率体现在只调用了一次父类型的构造函数,并且因此避免了在子类型的原型上面创建不必要的、多余的属性。与此同时,原型链还能保持不变,能够正常使用instanceof
和isPrototypeOf()
。开发人员普遍认为寄生组合式继承是引用类型最理想的继承方式。
简单总结:
原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。