JS之面向对象程序设计

1 理解对象

创建自定义对象的最简单方式就是创建一个 Object 的实例,然后再为它添加属性和方法,如下所示。

var person = new Object(); 
person.name = "Nicholas"; 
person.age = 29; 
person.job = "Software Engineer"; 
person.sayName = function(){ 
	alert(this.name); 
}; 
var person = { 
	name: "Nicholas", 
	age: 29, 
	job: "Software Engineer", 
	sayName: function(){ 
		alert(this.name); 
	} 
}; 

1.1 属性类型

ECMAScript 中有两种属性:数据属性和访问器属性。

  • 1. 数据属性

数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有 4 个描述其行为的
特性。

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
  • [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true。
  • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined。

对于像前面例子中那样直接在对象上定义的属性,它们的[[Configurable]]、[[Enumerable]]和[[Writable]]特性都被设置为 true,而[[Value]]特性被设置为指定的值。例如:

var person = { 
	name: "Nicholas" 
}; 

这里创建了一个名为 name 的属性,为它指定的值是"Nicholas"。也就是说,[[Value]]特性将被设置为"Nicholas",而对这个值的任何修改都将反映在这个位置。
要修改属性默认的特性,必须使用 ECMAScript 5 的 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" 

这个例子创建了一个名为 name 的属性,它的值"Nicholas"是只读的。这个属性的值是不可修改的,如果尝试为它指定新值,则在非严格模式下,赋值操作将被忽略;在严格模式下,赋值操作将会导致抛出错误。

  • 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 

在这个方法之前,要创建访问器属性,一般都使用两个非标准的方法:__defineGetter__()和__defineSetter__()。这两个方法最初是由 Firefox 引入的,后来 Safari 3、Chrome 1 和 Opera 9.5 也给出了相同的实现。使用这两个遗留的方法,可以像下面这样重写前面的例子。

var book = { 
	_year: 2004, 
	edition: 1 
}; 
//定义访问器的旧有方法
book.__defineGetter__("year", function(){ 
	return this._year; 
}); 
book.__defineSetter__("year", function(newValue){ 
if (newValue > 2004) { 
	this._year = newValue; 
	this.edition += newValue - 2004; 
} 
}); 
book.year = 2005; 
alert(book.edition); //2 

1.3 定义多个属性

由于为对象定义多个属性的可能性很大,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; 
			} 
		} 
	} 
}); 

1.4 读取属性的特性

使用 ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有 configurable、enumerable、get 和 set;如果是数据属性,这个对象的属性有 configurable、enumerable、writable 和 value。例如:

    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" 

2 创建对象

2.1 工厂模式

工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到在 ECMAScript 中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,如下面的例子所示。

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

2.2 构造函数模式

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

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

alert(person1.constructor == Person); //true 
alert(person2.constructor == Person); //true 
  • 将构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。例如,前面例子中定义的 Person()函数可以通过下列任何一种方式来调用。

// 当作构造函数使用
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" 

2.3 原型模式

我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 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()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用 isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回 true,如下所示:

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

ECMAScript 5 增加了一个新方法,叫 Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。例如:

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

要取得对象上所有可枚举的实例属性,可以使用 ECMAScript 5 的 Object.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" 
  • 更简单的原型语法
function Person(){ 
} 
Person.prototype = { 
	name : "Nicholas", 
	age : 29, 
	job: "Software Engineer", 
	sayName : function () { 
		alert(this.name); 
	} 
}; 

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("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()则是在原型中定义的。而修改了 person1.friends(向其中添加一个新字符串),并不会影响到 person2.friends,因为它们分别引用了不同的数组。这种构造函数与原型混成的模式,是目前在 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

2.5 动态原型模式

有其他 OO 语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。来看一个例子。

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

2.6 寄生构造函数模式

可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改 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" 

2.7 稳妥构造函数模式

道格拉斯·克罗克福德(Douglas Crockford)发明了 JavaScript 中的稳妥对象(durable objects)这个概念。所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序(如 Mashup程序)改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:一是新创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数。按照稳妥构造函数的要求,可以将前面的 Person 构造函数重写如下。

function Person(name, age, job){

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

     //返回对象
     return o;
 }

注意,在以这种模式创建的对象中,除了使用 sayName()方法之外,没有其他办法访问 name 的值。可以像下面使用稳妥的 Person 构造函数。

var friend = Person("Nicholas", 29, "Software Engineer"); 
friend.sayName(); //"Nicholas" 

3 继承

继承是 OO 语言中的一个最为人津津乐道的概念。许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。如前所述,由于函数没有签名,在 ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

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 

3.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" 
  • 传递参数

相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。看下面这个例子。

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 

3.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 

3.4 原型式继承

道格拉斯·克罗克福德在 2006 年写了一篇文章,题为 Prototypal Inheritance in JavaScript (JavaScript中的原型式继承)。在这篇文章中,他介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数:

function object(o){ 
	function F(){} 
	F.prototype = o; 
	return new F(); 
} 

在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。来看下面的例子。

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"
var instance2 = new SubType("Greg", 27);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

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"
var person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = Object.create(person, { 
 name: { 
 value: "Greg" 
 } 
}); 
 
alert(anotherPerson.name); //"Greg" 

3.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" 

3.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 构造函数的代码。在第一次调用 SuperType 构造函数时,SubType.prototype 会得到两个属性:name 和 colors;它们都是 SuperType 的实例属性,只不过现在位于 SubType 的原型中。当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和 colors。于是,这两个属性就屏蔽了原型中的两个同名属性。
有两组 name 和 colors 属性:一组在实例上,一组在 SubType 原型中。这就是调用两次 SuperType 构造函数的结果。好在我们已经找到了解决这个问题方法——寄生组合式继承。所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型
原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示。

function inheritPrototype(subType, superType){ 
	var prototype = object(superType.prototype); //创建对象
	prototype.constructor = subType; //增强对象
	subType.prototype = prototype; //指定对象
} 

这个示例中的 inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用 inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了,例如:

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

猜你喜欢

转载自blog.csdn.net/qq_27870421/article/details/89504843
今日推荐