面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。
在JS中,每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员定义的类型。
6.1 理解对象
6.1.1 属性类型
ECMAScript中有两种属性:数据属性和访问器属性。
- 数据属性
数据属性有4个描述其行为的特性:
[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,
或者能否把属性修改为访问器属性。默认为true。
[[Enumerable]]:表示能否通过for-in循环返回属性。默认为true。
[[Writable]]:表示能否修改属性的值。默认为true。
[[Value]]:包含这个属性的数据值。读取属性值时,从这个位置读;写入属性值的时候,
把新值保存在这个位置。默认为undefined。
比如:
var person = {
name: 'Nick'
}
对于上面这个对象来说,其[[Value]]被设置为"Nick",[[Configurable]]、[[Enumerable]]和[[Writable]]则都为其默认值true。
要修改属性默认的特性,必须使用ECMAScript5的 Object.defineProperty()方法,这个方法接收三个参数:属性所在的对象,属性的名字和一个描述符对象。其中,描述符对象的属性必须是:Configurable、Enumerable、Writable和Value。
var person = {};
Object.defineProperty(person, 'name', {
writable: false,
value: 'Nick'
})
alert(person.name); //"Nick"
person.name = 'Greg';
alert(person.name);//"Nick"
- 访问器属性
访问器属性不包含数据值,包含getter和setter函数。getter函数负责返回有效的值;setter函数负责决定如何处理数据。
访问器属性有如下4个特性:
[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,
能否修改属性的特性,或者能否把属性修改为访问器属性。默认为true。
[[Enumerable]]:表示能否通过for-in循环返回属性。默认为true。
[[Get]]:在读取属性时调用的函数。默认值为undefined。
[[Set]]:在写入属性时调用的函数。默认值为undefined。
var book = {
_year: 2014,
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;
console.log(book) ==> {edition: 2, _year: 2005}
6.1.2 定义多个属性
ECMAScript 5定义了一个 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;
}
}
}
})
6.1.3 读取属性的特性
使用 ECMAScript 5的 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");
console.log(descriptor)
6.2 创建对象
6.2.1 工厂模式
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('Nick', 29, 'Doctor');
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
6.2.2 构造函数模式
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('Nick', 29, 'Doctor');
var person2 = new Person('Jack', 30, 'Teacher');
要创建 Person 的新实例,必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤:
(1) 创建一个新对象;
(2) 将构造函数的作用域赋给新对象(因此this就指向了这个新对象);
(3) 执行构造函数中的代码(为这个新对象添加属性);
(4) 返回新对象
创建自定义的构造函数意味着将来可以将它的实例标识为一种特定类型;而这正是构造函数模式胜过工厂模式的地方。
- 将构造函数当做函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。
任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。
例如,前面例子中定义的 Person() 函数可以通过下列任何一种方式来调用。
//当做构造函数使用
var person = new Person('Nick', 29, 'Doctor');
person.sayName();
//作为普通函数调用
Person('Greg', 27, 'Doctor'); //添加到 window
window.sayName(); ==> 'Greg';
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, 'Kristen', 25, 'Nurse');
o.sayName(); ==> 'Kristen';
- 构造函数的问题
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面例子中,person1和person2都有一个名为sayName()的方法。
6.2.3 原型模式
使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
function Person(){
}
Person.prototype.name = "Nick";
Person.prototype.age = 29;
Person.prototype.job = 'Software Enginner';
Person.prototype.sayName = function() {
alert(this.name);
}
var person1 = new Person();
person1.sayName(); //"Nick"
var person2 = new Person();
person2.sayName(); //"Nick"
person1.sayName == person2.sayName ==> true;
以上与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个 sayName() 函数。
要理解原型模式的工作原理,必须先理解 ECMAScript 中原型对象的性质。
- 理解原型对象。
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor(构造函数)属性,这个属性是一个指向 prototype 属性所在函数的指针。就像前面那样,Person.prototype.constructor 指向 Person。通过这个函数,我们还可继续为原型对象添加其他属性和方法。
如何去检测 person1是否继承了 Person原型中方法?
可以使用 isPrototypeOf()
Person.prototype.isPrototypeOf(person1)
ECMAScript5 增加了一个新的方法,叫Object.getPrototypeOf()
用法:
Object.getPrototypeOf(person1);
一段代码执行某个对象中属性时,先在该对象中查找,如果没有找到,就会去原型链中查找。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。
function Person(){
}
Person.prototype.name = "wang";
Person.prototype.age = 27;
var person1 = new Person();
var person2 = new Person();
person1.name="jack";
person1.name ==> 'jack'; //来自实例
person2.name ==> 'wang'; //来自原型
使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是原型中。
存在于实例中,返回true 存在于原型中,返回false。
- 原型与 in 操作符
有两种情况会使用in操作符
(1) 在 for-in循环中使用
(2) 单独使用,在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。
function Person(){
}
Person.prototype.name = "wang";
Person.prototype.age = 27;
var person1 = new Person();
var person2 = new Person();
"name" in person1 ==> true
要取得对象上所有可枚举的实例属性,可以使用 Object.keys()
function Person(){
}
Person.prototype.name = "wang";
Person.prototype.age = 27;
Person.prototype.job = "teacher";
Person.prototype.sayName = function(){
alert(this.name);
}
var keys = Object.keys(Person.prototype) => ['name', 'age', 'job', 'sayName']
var p1 = new Person();
p1.name="Rob";
p1.age = 31;
var p1keys = Object.keys(p1) ==> ['name', 'age']
如果想得到所有实例属性,无论它是否可以枚举,都可以使用
Object.getOwnPropertyNames();
var keys = Object.getOwnPropertyNames(Person.prototype);
==> ['constructor', 'name', 'age', 'job', 'sayName']
- 更简单的原型语法
可以使用对象字面量方法来重写整个原型对象
function Person(){
}
Person.prototype = {
name: 'jack',
age: 29,
job: 'teacher',
sayName: function(){
alert(this.name)
}
}
上面操作相当于重写了默认的 prototype 对象,因此其 constructor 指向了 Object 构造函数,不再指向 Person 函数。
可以通过下面方法设置 constructor 让其指向 Person 函数。
Person.prototype = {
constructor:‘Person’,
name: 'jack',
age: 29,
job: 'teacher',
sayName: function(){
alert(this.name)
}
}
不过,又会带来新的问题,会导致它的 [Enumerable]被设置为 true。所以得使用 Object.defineProperty() 设置:
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
})
不过,defineProperty目前对于IE,只支持 IE9+,所以还是避免使用这种简化的原型语法吧。
- 原型的动态性。
var friend = new Person();
Person.prototype.sayHi = function() {
alert('hi');
}
friend.sayHi();
虽然 friend 实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。
原因是:实例和原型之间是一个松散连接关系,它们之间的连接不过是一个指针,而非一个副本。
- 原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。例如,在 Array.prototype 中可以找到 sort() 方法,而在 String.prototype 中可以找到 substring() 方法。
原型对象的问题
省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。
原型中的属性被所有实例共享,这一点,对于包含引用类型的属性来说,问题就比较突出了。
Person.prototype = {
constructor: "Person",
name: 'jack',
age: 29,
job: 'teacher',
friends: ['Shelby', 'Court'],
sayName: function(){
alert(this.name)
}
}
var person1 = new Person();
var person2 = new Person();
person1.friends.push('Van');
person1.friends ==> ["Shelby", "Court", "Van"]
person2.friends ==> ["Shelby", "Court", "Van"]
可以看到,为person1实例增加了 "Van", 会导致所有实例都共享了"Van"。
6.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(){
alert(this.name);
}
}
var person1 = new Person('Nick', 29, 'Software Enginner');
var person2 = new Person('Greg', 27, 'Doctor');
person1.friends.push('Van');
person1.friends ==> ["Shelby", "Court", "Van"]
person2.friends ==> ["Shelby", "Court"]
person1.sayName === person2.sayName ==> true
6.2.5 动态原型模式
把所有信息都封装在了构造函数里,通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点,换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
if( typeof this.name != 'function' ){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
var friend = new Person('nick', 29, 'soft');
friend.sayName()
使用 动态原型模式 模式时,不能使用对象字面量重写原型。
6.2.6 寄生构造函数模式
这种模式存在诸多问题,不建议使用。
6.2.7 稳妥构造函数模式
function Person(){
//创建要返回的对对象
var o = new Object();
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function() {
alert(name);
}
//返回对象
return o;
}
var friend = Person('Nick', 29, 'teacher');
friend.sayName();
存在问题:除了调用 sayName() 方法外,没有别的方式可以访问其数据成员。
不建议使用。
6.3 继承
6.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();
alert(instance.getSuperValue()) ==> true
上面继承,是通过将 SuperType 的实例赋给 SubType.prototype 来实现的。实现的本质是重写原型对象,代之一个新类型的实例。
换句话说,原先存在于 SuperType 的实例中的属性和方法,现在也存在于 SubType.prototype 中了。
需要注意的是, instance.constructor 现在指向的是 SuperType,这是因为原来 SubType.prototype 中的constructor被重写了的缘故。
通过实现原型链,本质上扩展了本章前面介绍的原型搜索机制,拿上面例子来说,调用 instance.getSuperValue() 会经历三个搜索步骤:
(1) 搜索实例
(2) 搜索SubType.prototype
(3) 搜索SuperType.prototype
- 别忘记默认的原型
大家要记住,所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype 。这也正是所有自定义类型都会继承
toString()、valueOf() 等默认方法的根本原因。
就像上面那个,SubType 继承了 SuperType, 而 SuperType 继承了 Object。当调用 instance.toString()时,实际上调用的是保存在 Object.prototype 中的那个方法。
- 确定原型和实例的关系
可以通过两种方法来确定原型和实例之间的关系
(1) instanceof
instance instanceof Object ==> true
instance instanceof SuperType ==> true
instance instanceof SubType ==> true
(2) isPrototypeof()
Object.prototype.isPrototypeof(instance) ==> true
SuperType.prototype.isPrototypeof(instance) ==> true
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 instances = new SuperType();
alert(instances.getSuperValue()) ==> true
- 原型链的问题
(1)
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType(){
}
SubType.prototype = new SuperType();
var instance1 = new SuperType();
instance1.colors.push('black');
instance1.colors ==> ["red", "blue", "green", "black"]
var instance2 = new SuperType();
instance2.colors ==> ["red", "blue", "green", "black"]
(2) 在创建子类型的实例时,不能向超类型的构造函数中传递参数。
由于以上的问题,实际中很少单独使用原型链
6.3.2 借用构造函数
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType(){
//继承了 SuperType
SuperType.call(this);
}
SubType.prototype = new SuperType();
var instance1 = new SuperType();
instance1.colors.push('black');
console.log(instance1.colors) ==> ["red", "blue", "green", "black"]
var instance2 = new SuperType();
console.log(instance2.colors) ==> ["red", "blue", "green"]
- 传递参数
function SuperType(name) {
this.name = name;
}
function SubType(){
//继承了 SuperType,同时还传递了参数
SuperType.call(this, 'Nich');
//实例属性
this.age = 29;
}
var instance = new SubType();
instance.name ==> 'Nich';
instance.age ==> 29
- 借用构造函数的问题
方法都在构造函数内部定义,因此函数复用就无从谈起了。
考虑到这些问题,借用构造函数的技术是很少单独使用的。
6.3.3 组合继承
指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长
其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
6.3.4 原型式继承
ECMAScript 5 新增 Object.create() 方法