第六章 面向对象程序设计
1. 理解对象 :
//创建对象的第一种方法
var person = new Object();
person.name = "Jack";
person.age = 29;
person.job = "Software Enjineer";
person.sayName = function(){
alert(this.name);
};
//创建对象的第二种方法(常用的方法)
var person = {
name: "Jack",
age: 29,
job: "Software Enjineer",
sayName: function(){
alert(this.name);
}
}
2. 属性类型 :
数据属性 :
数据属性包含一个数据值的位置,在这个位置可以读取和写入值,该属性有4个描述其行为的 特性。
-
configurable : 表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,能否把属性修改成为访问器属性,默认值为 true。
-
enumerable : 表示能否通过 for-in 循环返回属性,默认值为 true。
-
writable : 表示能否通过 for-in 循环返回属性,默认值为 true。
-
value : 表示能否通过 for-in 循环返回属性,默认值为 true。
PS:向前面那个对象定义的属性,他们的 configurable,enumerable,writable 的特性都被设置为 true,而 value 属性被设置为特定的值 Jack。
——————————接下来看一个可以修改默认属性特性的方法————————————
Object.defineProperty() : 用于修改属性默认的特性,该方法接受三个参数(属性所在的对象,属性的名字,一个描述符对象),描述符对象指的就是(configurable,enumerable,writable,value)其中的一个或多个。
var person = {}; //声明一个对象
Object.defineProperty(person,"name",{
writable: false, //不可修改属性的值
value: "Sam" //给name赋值
});
alert(person.name); //Sam
person.name = "Jack";
alert(person.name); //Jack
//类似的规则也可适用于不可配置的属性
var person = {};
Object.defineProperty(person,"name",{
configurable: false, //不可配置
value: "Jack"
});
alert(person.name); //Jack
delete person.name;
alert(person.name); //Jack
//需要说明的是一旦把属性改为不可配置就不能再把它变回可配置了,此时,再调用Object.defineProperty()方法修改除writable之外的特性,都会导致错误
访问器属性(类似于C#中的访问器):
访问器属性不包含数据值,它们包含一对 getter 和 setter 函数(这两个都不是必须的),读取访问器属性时,调用 getter 函数;写入访问器属性时,调用 setter 函数并传入新值,这个函数决定如何让处理数据,访问器属性有如下4大特性。
-
configurable : 表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,能否把属性修改成为访问器属性,默认值为 true。
-
enumerable : 表示能否通过 for-in 循环返回属性,默认值为 true。
-
get : 表示能否通过 for-in 循环返回属性,默认值为 true。
-
set : 表示能否通过 for-in 循环返回属性,默认值为 true。
//访问器属性不能直接被定义,必须通过Object.defineProperty()来定义
var book = {
_year: 2004; //下划线表示只能通过对象方法访问的属性
edition: 1;
};
Object.definedProperty(book,"year",{
get: function(){
return this._year;
}
set: function(newValue){
if(newValue > 2004){
this._year = newValue;
edition = newValue - 2004;
}
}
});
3. 定义多个属性 :
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;
}
}
}
});
4. 创建对象 :
————————————接下来让我们进入重头戏,创建对象———————————————
目前在JavaScript中最常用的创建对象的模式是 原型模式,其他的一些模式如工厂模式,构造函数模式由于各自的缺点已经很少被使用,(我没说原型模式没有缺点),那么咱们就重点来介绍 原型模式。
在开始之前,先来说说什么是JavaScript中的构造函数(为什么要这样说,是因为JavaScript中的构造函数和其他语言中的不同),JavaScript的构造函数并不是作为类的一个特定方法存在的;当任意一个普通函数用于创建一类对象时,它就被称作构造函数,或构造器。构造函数本身也是一个函数,只不过可以用它来 创建对象 而已。
//先来看一个构造函数
function Person(name,age,job){ //构造函数命名首字母大写
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
};
var person1 = new Person("Jack",29,"Software Enginner");
var person2 = new Person("Sam",27,"Doctor");
//要想创建一个新实例,必须使用new操作符,因为你创建的是一个对象
//person1,person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person,这点要记着...
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
好,下面我们正式进入原型模式
我们创建的每个函数都有一个 prototype(原型) 属性,这个属性是一个 指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法(看到这里你肯定会懵,继续往下看)。
使用原型对象的好处 :
可以让所有实例对象共享它所包含的属性和方法,换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
//下面来看个例子
function Person(){
Person.prototype.name = "Jack";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
};
var person1 = new Person();
person1.sayName(); //Jack
var person2 = new Person();
person2.sayName(); //Jack
alert(person1.sayName == person2.sayName); //true
下面我用一张图来让大家理解原型对象 :
原型对象这张图展示了Person构造函数,Person的原型属性以及Person现有的两个实例之间的关系。
另外,虽然这两个实例都不包含自己的属性和方法,但我们却可以调用原型中的属性和方法,这是通过查找对象属性的过程来实现的。
那么查找对象属性的过程又是怎样的呢?
当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索会从对象实例本身开始。如果在实例中找到了具有给定名子的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象。
那么根据这个过程我们就可以很好地将原型对象属性的值给屏蔽掉,换上我们希望的值。
function Person(){
Person.prototype.name = "Jack";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
};
var person1 = new Person();
person1.name = "Sam";
alert(person.name); //Sam,来自实例
var person2 = new Person();
alert(person2.name); //Jack,来自原型
当为对象实例添加一个对象时,这个属性会屏蔽原型对象中保存的属性,也就是不影响原型对象中保存的属性。
function Person(){
Person.prototype.name = "Jack";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
};
var person1 = new Person();
person1.name = "Sam";
alert(person1.name); //Sam
delete person1.name;
alert(person1.name); //Jack
hasOwnProperty() : 该方法可以检测一个属性是存在于实例中还是存在于原型中,因为是从 Object 继承来的,所以给定属性在 对象实例 中时,返回 true。
function Person(){
Person.prototype.name = "Jack";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
};
var person1 = new Person();
alert(person1.hasOwnProperty("name")); //false
person1.name = "Sam";
alert(person1.hasOwnProperty("name")); //true
delete person1.name;
alert(person1.hasOwnProperty("name")); //false
通过使用 hasOwnProperty() 方法,什么时候访问的是实例属性,什么时候访问的是原型属性就一清二楚了,下图展示了上面例子在不同情况下的实现与原型的关系。
hasOwnProperty原型与in操作符 :
有两种方式使用 in 操作符,单独使用和在 for-in 里面使用,在单独使用时,in 操作符会在通过对象能够访问属性时返回 true,无论属性在实例还是原型中。
function Person(){
Person.prototype.name = "Jack";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
};
var person1 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "Sam";
alert(person1.hasOwnProperty("name")); //true
alert("name" in person1); //true
上述代码执行的整个过程中,调用 ”name” in person 返回的值总是 true,我们可以同时使用 hasOwnProperty() 和 in 操作符,来确定该属性到底是存在于队对象中,还是存在于原型中。
function hasPrototypePerperty(object,name){
return !hasOwnProperty(name) && (name in object);
}
function Person(){
Person.prototype.name = "Jack";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
};
var person = new person();
alert(hasPrototypeProperty(person,"name")); //true
person.name = "Sam";
alert(hasPrototypeProperty(person,"name")); //false
Object.keys() :
取得对象上所有可枚举的 实例 属性,接受 一个对象 作为参数,返回 一个包含所有可枚举属性的数组。
function Person(){
Person.prototype.name = "Jack";
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 person1 = new Person();
person1.name = "Sam";
person1.age = 27;
alert(Object.keys(person1)); //name.age
//如果你想要得到所有属性,不管能不能被枚举,可以使用Object.getOwnPropertyNames()方法
alert(Object.getOwnPropertyNames(Person.prototype));
//constructor,name,age,job,sayName
更简单的原型方法 :
function Person{
};
Person.prototype = {
name: "Jack",
age: 29,
job: "Software Enjineer",
sayName: function(){
alert(this.name);
};
};
//需要注意的是如果这样写,constructor属性不在指向Person了,如果想让他指向Person,我们可以手动添加
function Person{
};
Person.prototype = {
constructor: Person,
name: "Jack",
age: 29,
job: "Software Enjineer",
sayName: function(){
alert(this.name);
};
};
//但是还跟原来的有点不一样,哪点不一样呢?我们发现现在constructor变成可枚举的了,要想把它变回来,可以使用我们前面介绍的enumerable特性,将它设置为false
function Person{
}
Person.prototype = {
name: "Jack";
age: 29;
job: "Software Enjineer";
sayName: function(){
alert(this.name);
}
};
Object.defineProperty(Person.prototype,"constructor",{
enumerable: false,
value: Person
});
原型的动态性 :
由于在原型中查找值的过程是一次搜索,因此我们对原型对象作出的任何修改都能够在实例上反映出来————即使先创建了实例也是如此(可以按照指针来理解)。
var friend = new Person();
Person.prototype.sayHi = function(){
alert("Hi");
};
friend.sayHi(); //Hi
//但是我们不能重写原型对象,那样会导致实例指不到新定义的原型对象,仍然指向你先前定义的原型对象
var friend = new Person();
Person.prototype = {
constructor: Person,
name: "Sam",
age: 27,
job: "Doctor",
sayHi: function(){
alert("Hi");
};
};
friend.sayHi(); //error
下图演示了上面代码的过程:
重写原型对象最后说两句 :
原型模式的重要性不仅体现在创建自定义类方面,就连原声的引用类型,都是采用这种模式创建的。所有原声引用类型(Object,Array,String等等)都在其构造函数的原型上定义了方法。下面举两个例子。
alert(typeof Array.prototype.sort); //function
alert(typeof String.prototype.substring); //function
前面花了大量的篇幅来介绍原型模式,但是原型模式还不是目前使用最为广泛的,目前使用最为广泛的是构造函数模式加原型模式。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Tom","Marry"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
};
};
var person1 = new Person("Jack",29,"Software Engineer");
var person2 = new Person("Sam",27,"Doctor");
person1.friends.push("Frank");
alert(person1.friends); //Tom,Marry,Frank
alert(person2.friends); //Tom,Marry
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("Jack",29,"Software Engineer");
friend.sayName(); //Jack
5. 继承 :
JavaScript中叙述了 原型链 的概念,并将其作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
咱们来简单回顾一下构造函数,原型,和实例的关系。
每个 构造函数 都有一个 原型对象,原型对象里有一个指向 构造函数 的指针,而 实例 都包含一个指向 原型对象 的内部指针。
那么假如我们让 原型对象 作为另一个引用类型的 实例,结果会怎样呢?显然,此时的 原型对象 会包含一个指向 最开始那个原型对象 的指针,相应的,最开始的原型对象 中也包含着一个指向 最开始的构造函数 的指针。
这就是原型链的基本概念...
function SuperType(){
this.property = true;
};
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function Subtype(){
this.subproperty = false;
};
//继承了SuperType
SubType.prototype = new SuperType();
SubType.protptype.getSubValue = function(){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
原型链
在上面的代码中,我们没有使用 SubType 默认提供的原型,而是给它替换了一个新原型;这个新原型就是 SuperType 的实例。于是,新原型不仅具有作为一个 SuperType 的实例拥有的所有属性和方法,而且其内部还有一个指针,指向 SuperType 的原型。
最终的结果就是这样,instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型。getSuperValue() 方法仍然还在 SuperType.prototype 中,但 property 则位于 SubType.prototype 中。这是因为 property 是一个实例属性,而 getSuperValue() 则是一个原型方法。既然 SubType.prototype 现在是 SuperType 的实例,那么 property 当然也就位于该实例中了。
别忘记了默认的原型。
事实上,前面的例子展示的原型链少一环。因为所有的引用类型都是 Object,而这个继承也是通过原型链实现的。所有的函数的默认原型都是 Object 的实例。
完整原型链原型链的问题 :
如果有包含引用类型的原型(比如数组),那么数组一旦改动就将一起整条链上的数组发生变化,而这一点我们有时候是不希望看到的。
解决方法 :
借用构造函数:
通过使用 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,"Jack");
//实例属性
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //Jack
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.sayAge = function(){
alert(this.age);
};
var instance1 = new SubType("Jack",29);
instance1.colors.push("Black");
alert(instance1.colors); //red,blue,green,black
instance1.sayName(); //Jack
instance1.sayAge(); //29
var instance2 = new SubType("Sam",27);
alert(instance2.colors); //red,blue,green
instance2.sayName(); //Sam
instance2.sayAge(); //27
组合继承避免了原型链和借用构造函数的缺陷,融合了他们的优点,成为了JavaScript中最受欢迎的继承模式。
小结 :
-
工厂模式 : 使用简单的函数创建对象,为对象添加属性和方法,然后返回对象,这个模式后来被构造函数模式所取代。
-
构造函数模式 : 可以创建自定义引用类型,可以像创建内置对象实例一样使用 new 操作符,不过,构造函数的缺点就在于他的每个成员都无法得到复用,包括函数。
-
原型模式 : 使用构造函数的 prototype 属性来指定那些应该共享的属性和方法。
-
组合使用构造函数和原型模式 : 使用构造函数定义实例属性,使用原型定义共享的属性和方法。
网友评论