面向对象的程序设计

对象

var person = new Object();

或者

var person={}


属性类型

数据类型

[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特 性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的 这个特性默认值为 true。

[[Enumerable]]:表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定 义的属性,它们的这个特性默认值为 true。

[[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的 这个特性默认值为 true。

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


要修改属性默认的特性,使用 **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" 

访问器属性

访问器属性不包含数据值;它们包含一对儿 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 

_year 前面 的下划线是一种常用的记号,用于表示只能通过对象方法访问的属性。

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


定义多个属性

由于为对象定义多个属性的可能性很大,ECMAScript 5 又定义了一个 Object.defineProperties()方法。

这个方法接收两个对象参数:第一 个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对 应

var book = {};
Object.defineProperties(book, { 
    
 _year: { 
 value: 2004,
 writable:true
 }, 
 
 edition: { 
 value: 1 
 }, 
    
 year: { 
 get: function(){
 	return this._year; 
 }, 
 set: function(newValue){ 
	 if (newValue > 2004) { 
 		this._year = newValue; 
 		this.edition += newValue - 2004; 
 			} 
		}
	} 
}); 

以上代码在 book 对象上定义了两个数据属性(_year 和 edition)和一个访问器属性(year)。


读取属性的特性

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" 

对于数据属性_year,value 等于最初的值,configurable 是 false,而 get 等于 undefined

对于访问器属性 year,value 等于 undefined,enumerable 是 false,而 get 是一个指向 getter 函数的指针


创建对象

虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同 一个接口创建很多对象,会产生大量的重复代码。

工厂模式

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"); 
  • 没有显式地创建对象;
  • 直接将属性和方法赋给了 this 对象;
  • 没有 return 语句。

要创建 Person 的新实例,必须使用 new 操作符。

以这种方式调用构造函数实际上会经历以下 4 个步骤:

(1) 创建一个新对象;

(2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);

(3) 执行构造函数中的代码(为这个新对象添加属性);

(4) 返回新对象。


person1 和 person2 分别保存着 Person 的一个不同的实例。这两个对象都 有一个 constructor(构造函数)属性,该属性指向 Person

alert(person1.constructor == Person); //true 
alert(person2.constructor == Person); //true 

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


将构造函数当作函数

任何函数,只要通过 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" 

构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个 实例上重新创建一遍。

this.sayName = new Function("alert(this.name)");

从这个角度上来看构造函数,更容易明白每个 Person 实例都包含一个不同的 Function 实例

通过把函数定义转移到构造函数外 部来解决这个问题

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 

可以通过 isPrototypeOf()方法来确定对象之 间关系

alert(Person.prototype.isPrototypeOf(person1)); //true 
alert(Person.prototype.isPrototypeOf(person2)); //true

ECMAScript 5 增加了一个新方法,叫 Object.getPrototypeOf()

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

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。

如果我们 在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该 属性将会屏蔽原型中的那个属性。

添加同名属性只会阻止我们访问原型中的那个属性,但不会修改原型中的属性。

使用 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"——来自原型

通过使用 hasOwnProperty()方法,什么时候访问的是实例属性,什么时候访问的是原型属性就 一清二楚了


原型与in操作符

有两种方式使用 in 操作符:

单独使用

alert("name" in person1);

当该属性存在于实例中还是存在于原型中,调用"name" in person1都返回 true,


for-in 循环中使用

for( let i in person)
{
	alert(person[i])
}

在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中 既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性


原型的动态性

我们对原型对象所做的任何修改都能够立即从实例上 反映出来——即使是先创建了实例后修改原型也照样如此

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

原型存在的问题

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, 
 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 和方 法 sayName()则是在原型中定义的。

是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自 定义类型的方法


动态原型模式

可以通过 检查某个应该存在的方法是否有效,来决定是否需要初始化原型

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()方法不存在的情况下,才会将它添加到原 型中。这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修 改了


寄生构造函数模式

这种模式 的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象

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"

关于寄生构造函数模式,返回的对象与构造函数或者与构造函数的原型属 性之间没有关系


稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。

一是新创建对象的 实例方法不引用 this

二是不使用 new 操作符调用构造函数

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


继承

其实现继承主要是依靠原型链 来实现的。

原型链

基本模式

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 

instance 继承了SubType 的属性,包括constructor

constructor属性指向该对象对应的构造函数

instance 指向 SubType 的原型, SubType 的原型又指向 SuperType 的原型。

要注意 instance.constructor 现在指向的 是 SuperType


确定原型和实例的关系

alert(instance instanceof Object); //true 
alert(instance instanceof SuperType); //true 
alert(instance instanceof SubType); //true 

第二种方式是使用isPrototypeOf()方法

只要是原型链中出现过的原型,都可以说是该 原型链所派生的实例的原型,因此 isPrototypeOf()方法也会返回 true,

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; 
 }, 
 someOtherMethod : function (){ 
 return false; 
 } 
}; 
var instance = new SubType(); 
alert(instance.getSuperValue()); //error!

原型链的问题

原型链最主要的问题来自包含引用类型值的原型

解决方式如下


借用构造函数

函数只不过是在特定环境中执行代码的对象, 因此通过使用 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 

原型式继承

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"); 
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie

包含引用类型值的属性始终都会共享相应的值,没做到期望的效果


寄生式继承

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

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

寄生组合式继承

高效率体现在它只调用了一次 SuperType 构造函数,防止了原生链和构造函数都有相同的属性。

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; //指定对象
}

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