深入理解JavaScript对象
可以参考阮一峰的4篇博客
- Javascript 面向对象编程(一):封装
- Javascript面向对象编程(二):构造函数的继承
- Javascript面向对象编程(三):非构造函数的继承
- Javascript定义类(class)的三种方法
以下内容出自《JavaScript高级程序设计》一书
1.理解对象
1.1 创建对象的方法
- new 创建
var person = new Object();
person.name = 'nike';
person.age = 29;
person.printSelf = function(){
alert(this.name+'\'s age is ' + this.age)
}
person.printSelf();
通过new创建一个新对象,然后为其添加属性和方法
function Person(){
this.name = ''
this.setName = function(name){
this.name = name;
}
}
var person1 = new Person();
person1.setName('xiaoMing');
console.log(person1.name);//xiaoMing
var person2 = new Person();
console.log(person2.name);//undefinded
- 对象字面量
var person = {
name : 'nike',
age : 29,
printSelf : function(){
console.log(this.name+'\'s age is ' + this.age);
}
}
person.printSelf();//nike's age is 29
2.对象属性
ECMAScript中有两种属性:数据属性和访问器属性。
2.1 数据属性
数据属性有4个描述其行为的特性
- Configurable:能否修改重新定义属性的特性,或者把数据属性修改为访问器属性,默认为true.当将这个特性设置为false的时候则不能再将其设置为true.
- Enumerable:能否通过for-in 循环返回属性。默认true
- Writable: 能否修改属性的指,默认true
- Value: 包含这个属性的数据值。默认值为undefined
ECMAScript必须使用Object.defineProperty()方法修改熟悉的特性。这个方法接受三个参数:属性所在的对象,属性名字和描述属性特性的对象。
var person = {};
Object.denfineProperty(person,'name',{
Writable:false,
value:'xiaoming'
});
console.log(person.name); //xiaoming
person.name = 'xx'; //不会报错
console.log(person.name);//xiaoming
在调用 Object.defineProperty()方法时,如果不指定,configurable、enumerable 和writable 特性的默认值都是 false。多数情况下,可能都没有必要利用 Object.defineProperty()方法提供的这些高级功能。不过,理解这些概念对理解 JavaScript 对象却非常有用。
2.2 访问器属性
访问器属性不包含数据值;它们包含一对儿 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
以上代码创建了一个 book 对象,并给它定义两个默认的属性:_year 和 edition。_year 前面的下划线是一种常用的记号,用于表示只能通对象方法访问的属性。而访问器属性 year 则包含一个getter 函数和一个 setter 函数。getter 函数返回_year 的值,setter 函数通过计算来确定正确的版本。因此,把 year 属性修改为 2005 会导致_year 变成 2005,而 edition 变为 2。这是使用访问器属性的常见方式,即设置一个属性的值会导致其他属性发生变化。
2.3 读取属性的特性
使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有 configurable、enumerable、get 和 set;如果是数据属性,这个对象的属性有 configurable、enumerable、writable 和 value。
var name = Object.getOwnPropertyDescriptor(person,'name');
console.log(name.value);
3.创建对象
虽然上面提到的两种方式都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个借口创建很多对象会产生大量重复代码。
3.1 工厂模式
ECMAScript中无法创建类,开发人员发明一种工厂模式的函数,用这个函数封装特定借口创建对象。
function creatPerson(name,age){
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function (){
console.log(this.name);
};
return o
}
var person1 = creatPerson("xiaoming",18);
var person2 = creatPerson('xiaohei',20);
工厂模式虽然可以创建很多相似对象,但是无法识别对像(怎么知道一个对象的类型)。构造函数模式解决了这个问题。
3.2 构造函数模式
用构造函数模式重写前面的例子。
function Person(name,age){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name);
}
}var person1 = new Person("xiaoming",18);
var person2 = new Person('xiaohei',20);
在这个例子中,Person()函数取代了 createPerson()函数。我们注意到,Person()中的代码除了与 createPerson()中相同的部分外,还存在以下不同之处:
- 没有显式地创建对象;
- 直接将属性和方法赋给了 this 对象;
- 没有 return 语句。
此外,还应该注意到函数名 Person 使用的是大写字母 P。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法主要是为了区别于 ECMAScript 中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。
要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下 4个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象。
在前面例子的最后,person1 和 person2 分别保存着 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
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。在这个例子中,person1 和 person2 之所以同时是 Object 的实例,是因为所有对象均继承自 Object(详细内容稍后讨论)。
3.2.1构造函数调用
构造函数与其他函数的唯一区别就是他们的调用方式不一样,但语法与普通函数一样(开发人员默认构造函数首字母大写)。任何函数用new调用
都可以当作构造函数。构造函数也可以像普通函数一样调用。
// 当作构造函数使用
var person = new Person("Nicholas", 29);
person.sayName(); //"Nicholas"
// 作为普通函数调用
var a = Person("Greg", 27); // 添加到 window
window.sayName(); //"Greg"
//在另外一个对象的作用域上调用
var o = {};
Person.call(o,"xiaoming",23);
o.sayName();//xiaoming
3.2.2构造函数模式存在的问题
构造函数虽好,但也有明显的缺点:就是每个方法都要在每个实例上重新创建一遍。上个例子中每个实例 的方法都不是同一个Function实例。
consloe.log(person1.sayName == person2.sayName); //false
没有必要创建两个完成同样任务的Function,并且有this的存在,不用在执行代码前把函数绑定在特定对象上。因此可以如下把函数定义移到构造函数外部来解决。
function Person(name, age){
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName(){
console.log(this.name);
}
var person1 = new Person("Nicholas", 29);
var person2 = new Person("Greg", 27);
console.log(person1.sayName === person2.sayName);//true
这样做虽然能解决每个实例都创建同样的function,但是又出现了一个问题:在全局作用域中的函数只能被某个对象调用有点让全局作用域名不副实。更加严重的问题是:如果对象需要定义很多方法,那就需要定义很多的全局函数,这样我们这个自定义的引用类型就没有封装性了。
这些问题可以用原型模式解决。
3.3 原型模式
每个对象都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象称为原型对象。这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。简单老说就是可以将一些共享的方法和属性添加(定义)在这个对象上,那么实例出来的所有对象都可以共享这些属性和方法。(类似java的static,只是类似)不必在构造函数中定义对象实例信息,而是将这些信息直接添加到原型对象中。
function Person(){};
Person.prototype.name = "xiaoming";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.sayName();//xiaoming
person2.sayName();//xiaoming
3.3.1 理解原型对象
无论什么时候只要创建一个函数,就会根据一组特定的规则为这个函数创建一个prototype属性,这个prototype属性指向函数的原型对象。默认情况下,原型对象内自动获取一个constructor(构造函数),这个属性包含一个重新指向这个原型对象的构造函数的指针。即
Person.prototype.constructor == Person.
创建自定义的构造函数时,其原型对象默认只会取得constructor属性,其他方法都是从Object继承过来的(父类,原型链)。
function Test(name){
this.name = name;
}
Test.prototype;
Person.prototype;
![](http://images2015.cnblogs.com/blog/1001045/201702/1001045-20170223153357991-1577622323.png)
如上所示,Test构造函数自己定义的属性并不在原型对象中,Person的原型对象绑定了属性即方法,所以它的原型对象除了从Object继承过来的方法外,还包括自己定义的属性和方法。
调用构造函数创建实例后,该实例的内部包含一个指针(内部属性),指向构造函数的原型对象。ECMAScript5管这个指针叫[[Propotype]]。在脚本中没有标准的访问方式,但在Chrom,Firefox,Safariz中每个对象都支持一个属性__proto__;在其他实现中,这个属性对脚本是不可见的。需要注意的是,这个属性是连接实例和构造函数的原型对象的,并不是构造函数本身。
![](http://images2015.cnblogs.com/blog/1001045/201702/1001045-20170223153413991-612975556.png)
![](http://images2015.cnblogs.com/blog/1001045/201702/1001045-20170223153420726-943948115.png)
以前面使用 Person 构造函数和 Person.prototype 创建实例的代码为例,图 6-1 展示了各个对象之间的关系。
![](http://images2015.cnblogs.com/blog/1001045/201702/1001045-20170223153442866-837579339.png)
6-1 展示了 Person 构造函数、Person 的原型属性以及 Person 现有的两个实例之间的关系。在此,Person.prototype 指向了原型对象,而Person.prototype.constructor 又指回了 Person。原型对象中除了包含 constructor 属性之外,还包括后来添加的其他属性。Person 的每个实例——person1 和 person2 都包含一个内部属性,该属性仅仅指向了Person.prototype;换句话说,它们与构造函数没有直接的关系。此外,要格外注意的是,虽然这两个实例都不包含属性和方法,但我们却可以调用 person1.sayName()。这是通过查找对象属性的过程来实现的.
虽然在所有实现中都无法访问到[[Protoype]],但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。
```javascript
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
ECMAScript5新增加了一个方法,叫做Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]].例如:
console.log(Object.getPrototypeOf(person1));
console.log(Object.getPrototypeOf(person1) === Person.prototype);//true
访问对象属性时,先查找实例本身是否存在该属性,如果有则访问该属性,如果没有则查找该对象指向的原型对象中是否存在,存在则访问不存在则查找该原型对象的该原型对象(原型(继承)链),直至查找到Object(Obejct的原型对象是null).如果一直未找到,则该属性为undefined。
综上所诉,属性的访问类似java的继承,实例的属性会覆盖原型对象的相同属性。例子如下:
function Person(){
this.name = 'xx';
}
Person.prototype.name = "oo";
var person1 = new Person();
console.log(person1.name);//xx
delete person1.name;
console.log(person1.name);//oo
使用 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
3.3.2 原型与in操作符
in在对象上有两种使用方式:单独使用和for-in循环中使用
-
单独使用
单独使用时,与hasOwnProperty()方法不同,后者判断实例本身是否存在该属性,而in操作符将范围提升到原型(对象能够访问到属性就返回 true,hasOwnProperty()只在属性存在于实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确
定属性是原型中的属性。)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
-
for-in循环
返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为 false 的属性)的实例属性也会在 for-in 循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的。for (var param in person1){console.log(param)} //name //age //job //sayName
Object.keys():
for-in返回所有能够访问的,可枚举属性。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"
3.3.3 更简单的原型语法
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
我们曾今提过,没创建一个函数都会自动创建他的prototype对象,这个对象自动获取constructor属性。但我们这里完全重写了默认的prototype对象,所以这里的原型对象的constructor不在指向原来的构造函数Person,而指向了Object。所以如果constructor属性很重要,我们应该设置会适当的值。
function Person(){
}
Person.prototype = {
constructor:Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
这么设置会将constructor属性的[[Enumerable]]特性设置为true.默认该属性是不可枚举的,所以最后应该这么写:
function Person(){
}
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
});
3.3.4 原型动态性
原型中查找值的过程是 一次搜索,因此我们对原型的修改都能立刻从实例上反映出来--即使是先创建了实例再修改原型也是如此。
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi"(没有问题!)
需要注意的是
尽管修改原型是具有动态性的,但是重写原型就不一样了:
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
下图展示了这个过程的内幕:
如上图所示:重写原型切断了现有原型与之前存在的任何实例之间的联系;它们引用的任然是最初的原型。
3.3.5 原生对象的原型
通过原生对象的原型,我们可以为该原生对象添加方法(原生对象的方法基本都定义在原生对象的原型上):
alert(typeof Array.prototype.sort); //"function"
alert(typeof String.prototype.substring); //"function"
以上可证原生对象的方法基本都定义在原生对象的原型上。
下面实例在为原生对象添加方法:
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true
注意:即使可以这样做,但并不推荐;如果因某个实现中缺少某个方法,就在原生对象的原型中添加这个方法,那么当在另一个支持该方法的实现中运行代码时,就可能会导致命名冲突。而且,这样做也可能会意外地重写原生方法。
3.3.6 原型模式存在的问题
原型模式省略了初始化过程,这样所有实例在默认情况下都将取得相同的属性值。虽然不方便但是并不是大问题,最大的问题在于其共享的本性所导致的。
原型中所有属性被很多实例共享, 这种共享对函数来说很合适,对属性来说也还行(可以在实例上添加一个同名属性覆盖原型中的属性)。对于包含引用类型值的属性来说,问题就突出了。
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
如上所示,原型对象的friends属性包含一个字符串数组。那么任何一个实例修改了fiends引用的数组,都会影响其他实例。
所以开发人员很少单独使用原型模式。
3.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(){
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
这个模式下,把所有实例共享的constructor属性和方法定义在原型中,而实例属性都是定义在构造函数中。这种模式是使用最广泛的模式,可以说定义引用类型的默认模式。
3.5 动态原型模式
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()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。
3.6寄生构造函数模式
这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;
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"
这种模式跟工厂模式一模一样。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 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");
alert(colors.toPipedString()); //"red|blue|green"
这个模式可以用来在原有的构造函数的基础上添加新的属性或者方法。(类似继承)。
关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。
3.7 稳妥构造函数模式
稳妥对象指的是没有公共属性,而且其他方法也不能引用this对象。稳妥对象最适合在一些安全环境中(这些环境禁止使用this和new),或者防止数据被其他应用程序(如Mashup程序)改动时使用。这种模式与寄生构造函数类似,不同点是:一、对象的实例方法不使用this对象;二、不使用new操作符调用构造函数。
function Person(name, age, job){
//创建要返回的对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
alert(name);
};
//返回对象
return o;
}
在这种模式下处了调用实例的sayName()方法之外,没有其他办法访问name的值。
这样,变量 friend 中保存的是一个稳妥对象,而除了调用 sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。
与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此 instanceof 操作符对这种对象也没有意义。
3.8 极简主义方法
参考阮一峰的博客
4 继承
继承是面向对象语言最为人津津乐道的概念。许多语言都支持两种继承方式:接口继承和实现继承。
接口继承只继承方法签名(只有方法,没有实现),实现继承则继承实际的方法。
由于函数没有签名,在ECMAScript中无法实现接口继承,只支持实现继承,其实现继承主要靠原型链实现。
4.1 原型链
ECMAScript 中将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向父类原型的指针,相应地,父类原型中也包含着一个指向父类构造函数的指针。假如这个父类原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。实现原型链有一种基本模式,其代码大致如下
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
以上代码定义了两个类型:SuperType 和 SubType。每个类型分别有一个属性和一个方法。它们的主要区别是 SubType 继承了 SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋给SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了(并且由于SubType.prototype是SuperType的实例,所以其内部有一个指向SuperType.prototypr的指针,所以存在原型链)。在确立了继承关系之后,我们给 SubType.prototype 添加了一个方法,这样就在继承了 SuperType 的属性和方法的基础上又添加了一个新方法。这个例子中的实例以及构造函数和原型之间的关系如图 6-4所示。
我们没有使用 SubType 默认提供的原型,而是给它换了一个新原型;这个新原型就是 SuperType 的实例。于是,新原型不仅具有作为一个 SuperType 的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了 SuperType 的原型。
SubType 的原型指向另一个对象——SuperType 的原型,而这个原型对象的 constructor 属性指向的是 SuperType,所以instance.constructor 现在指向的是 SuperType。
通过实现原型链,本质上扩展了前面介绍的原型搜索机制。当以读取模式访问一个实例属性时,首先会在实例中搜索该属性。如果没有找到该属性,则会继续搜索实例的原型。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。就拿上面的例子来说,调用
instance.getSuperValue()会经历三个搜索步骤:1)搜索实例;2)搜索 SubType.prototype;3)搜索 SuperType.prototype,最后一步才会找到该方法。在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。
4.1.1 Object(默认原型)
前面例子中展示的原型链还少一环。我们知道,所有引用类型默认都继承了 Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。这也正是所有自定义类型都会继承 toString()、valueOf()等默认方法的根本原因。图 6-5 为我们展示了该例子中完整的原型链。
一句话,SubType 继承了 SuperType,而 SuperType 继承了 Object。当调用 instance.toString()时,实际上调用的是保存在 Object.prototype 中的那个方法。
4.1.2 确定原型和实例的关系
过两种方式来确定原型和实例之间的关系:
1.instanceof 操作符
alert(instance instanceof Object); //true
alert(instance instanceof SuperType); //true
alert(instance instanceof SubType); //true
2.isPrototypeOf()方法
alert(Object.prototype.isPrototypeOf(instance)); //true
alert(SuperType.prototype.isPrototypeOf(instance)); //true
alert(SubType.prototype.isPrototypeOf(instance)); //true
4.1.3 定义新方法
为子类型重写超类型中的某个方法,或者添加超类型中不存在的某个方法需要注意:
1.必须在用父类的实例替换原型之后,再定义这两个方法
2.不能使用对象字面量创建原型方法,因为这样做就会重写原型链
4.1.4 原型链继承存在的问题
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。
前面原型模式创建对象介绍过包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。
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"
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。有鉴于此,实践中很少会单独使用原型链。
4.2 借用构造函数
在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。
这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用 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"
代码中“借调”了超类型的构造函数。通过使用 call()方法(或 apply()方法也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。结果,SubType 的每个实例就都会具有自己的 colors 属性的副本了。
4.2.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
为了确保SuperType 构造函数不会重写子类型的属性,应该先调用超类构造函数,在定义子类属性。
4.2.2 借用构造函数的问题
如果只借用构造函数,那么也将存在构造函数模式存在的问题——方法都在构造函数中定义,因此函数无法复用。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。考虑到这些问题,借用构造函数的技术也是很少单独使用的。
4.3组合继承
组合继承(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.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
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。 而且,instanceof 和 isPrototypeOf()也能够用于识别基于组合继承创建的对象。
4.4原型式继承
道格拉斯·克罗克福德介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
从本质上讲,object()对传入其中的对象执行了一次浅复制。来看下面的例子。
(浅复制和深复制都可以实现在已有对象的基础上再生一份的作用,但是对象的实例是存储在堆内存中然后通过一个引用值去操作对象,由此复制的时候就存在两种情况了:复制引用和复制实例,这也是浅复制和深复制的区别所在。
浅复制:浅复制是复制引用,复制后的引用都是指向同一个对象的实例,彼此之间的操作会互相影响
深复制:深复制不是简单的复制引用,而是在堆中重新分配内存,并且把源对象实例的所有属性都进行新建复制,以保证深复制的对象的引用图不包含任何原有对象或对象图上的任何对象,复制后的对象与原来的对象是完全隔离的
由深复制的定义来看,深复制要求如果源对象存在对象属性,那么需要进行递归复制,从而保证复制的对象与源对象完全隔离。然而还有一种可以说处在浅复制和深复制的粒度之间,也是jQuery的extend方法在deep参数为false时所谓的“浅复制”,这种复制只进行一个层级的复制:即如果源对象中存在对象属性,那么复制的对象上也会引用相同的对象。这不符合深复制的要求,但又比简单的复制引用的复制粒度有了加深。)
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.name); //"Nicholas" //name不是引用类型,所以并不会被改变
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 因为anotherPerson的原型对象是Person,所以它的friends属性和person的friends属性是同一个引用
object()产生的新对象将 person 作为原型,所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。这意味着 person.friends 不仅属于 person 所有,而且也会被 anotherPerson以及 yetAnotherPerson 共享。实际上,这就相当于又创建了 person 对象的两个副本。
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"
在没有必要兴师动众地创建构造函数,而只想让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的。不过别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
4.5 寄生式继承
寄生式(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"
这个例子中的代码基于 person 返回了一个新对象——anotherPerson。新对象不仅具有 person的所有属性和方法,而且还有自己的 sayHi()方法。
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。任何能够返回新对象的函数都适用于此模式。
这个模式和构造函数模式一样,方法不能复用。
4.6 寄生组合式继承(组合继承升级版)
主要思想--不再让子类的原型指向超类的一个实例,而是指向父类原型的副本(之所以不直接指向父类的原型是因为如果这么做了,子类和父类就是同一个类,修改子类的原型同时会修改父类的原型)。
前面说过,组合继承是 JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。第一次创建原型时在原型对象会有超类实例的所有实例。然后我们不得不在第二次子类构造函数内部再次调用构造函数重写这些属性再来看一看下面组合继承的例子。
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); //第二次调用 SuperType()
this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用 SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
在第一次调用 SuperType 构造函数时,SubType.prototype 会得到两个属性:name 和 colors;它们都是 SuperType 的实例属性,只不过现在位于 SubType 的原型中。当调用 SubType 构造函数时,又会调用一次SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和 colors。于是,这两个属性就屏蔽了原型中的两个同名属性。
图 6-6 展示了上述过程。
如图 6-6 所示,有两组 name 和 colors 属性:一组在实例上,一组在 SubType 原型中。这就是调用两次 SuperType 构造函数的结果。好在我们已经找到了解决这个问题方法——寄生组合式继承。
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型
的原型。寄生组合式继承的基本模式如下所示。
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 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用
instanceof 和 isPrototypeOf()。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。