JavaScript 中的面向对象
js中面向对象的概念和其他语言(c++、java等)不同,js严格来说没有“类”的概念,而是用原型链的形式实现了多态、继承和封装。因此,能否理解原型链一定程度上直接决定了是否真正理解了js中的面向对象。
一、创建对象
使用 Object 构造函数或对象字面量都可以创建单个对象。为了使用统一接口创建多个对象,同时避免代码的冗余,人们开始使用工厂模式的一种变体。
1、工厂模式
工厂模式的思想是设计一种函数,将对象的创建、初始化等细节封装在函数中。
function createPerson(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
alert(this.name);
}
return o;
}
var person1 = createPerson("Greg", 27);
工厂模式解决了多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。于是构造函数模式应运而生。
2、构造函数模式
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = function() {
alert(this.name);
}
}
var person1 = new Person("Greg", 27);
在这个例子中,Person() 函数取代了 createPerson() 函数。它们的不同之处在于:
- 没有显示地创建对象
- 直接将属性和方法赋值给 this 对象
- 没有 return 语句
要创建 Person 的新实例。必须使用 new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
- 执行构造函数中的代码
- 返回新对象
构造函数模式也有一定缺点。在前面的例子中,person1 和 person2 都有一个名为 sayName() 的方法,但这两个方法不是同一个 function 的实例。因此,可以像下面这样,把函数定义转移到构造函数外部。
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
var person1 = new Person("Greg", 27);
这样做确实解决了两个函数做同一件事的问题,可是把函数定义在全局作用域中就破坏了封装性。于是,出现了原型模式。
3、原型模式
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,这个对象包含由特定类型的所有实例共享的属性和方法。通过使用原型对象,我们可以让所有对象实例共享它所包含的属性和方法。
function Person() {
}
Person.prototype = {
name: "Greg",
age: 27,
sayName: function() {
alert(this.name);
}
}
var person1 = new Person();
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都取得相同的属性值。尤其是对于包含引用类型的属性来说,问题就比较突出了。
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Greg",
age: 27,
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(preson1.friends === person2.friends); //true
4、组合使用构造函数模式和原型模式
构造函数模式用于定义实例属性,原型模式用于定义方法和共享属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用。
function Person(name, age) {
this.name = name;
this.age = age;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor: Person,
sayName: function() {
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29);
var person2 = new Person("Greg", 27);
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
5、动态原型模式
动态原型模式把所有信息都封装在了构造函数中。
function Person(name, age) {
this.name = name;
this.age = age;
if(typeof this.sayName != "function") { //判断sayName()方法是否存在,如果不存在则创建。这段代码只会在初次调用构造函数时才会执行。
Person.prototype.sayName = function() {
alert(this.name);
}
}
}
var friend = new Person("Greg", 27);
friend.sayName();
使用动态原型模式时,不能使用对象字面量重写原型。如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。
6、寄生构造函数模式
function Person(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
alert(this.name);
}
return o;
}
var person = new Person("Greg", 27);
除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。
关于寄生构造函数模式,返回的对象与构造函数或者构造函数的原型属性之间没有关系,为此,不能依赖 typeof 操作符来确定对象类型。
7、稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者防止数据被其他应用程序改动时使用。
function Person(name, age) {
var o = new Object();
var name = name, age = age;
o.sayName = function() {
alert(name);
}
return o;
}
var person = new Person("Greg", 27);
这样,变量 friend 中保存的是一个稳妥对象,而除了调用 sayName() 方法外,没有别的方式可以访问其数据成员。
二、继承
1、原型链
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象内部的指针。假如我们让原型对象等于另一个类型的实例。那么,此时的原型对象将包含另一个原型的指针。如此层层递进,就构成了实例与原型的链条,即原型链。
function SuperType() {
this.property = true;
}
SuperType.propertype.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
在上面的代码中,没有使用 SubType 默认提供的原型,而是给它换了一个新原型;这个新原型就是 SuperType 的实例。
原型链的问题
包含引用类型值的原型属性会被所有实例共享,这也是为什么要在构造函数中,而不是在原型属性中定义属性的原因。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
}
//继承了 SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.color.push("black");
alert(instance1.colors); //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green,black"
这个例子中的 SuperType 构造函数定义了一个 colors 属性,该属性包含一个数组(引用类型值)。当 SubType 通过原型链继承了 SuperType 之后,SubType.prototype 就变成了 SuperType 的一个实例,因此它也拥有了一个它自己的 colors 属性,结果是 SubType 的所有实例都会共享这一个 colors 属性。
2、借用构造函数
这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
//继承了 SuperType
SuperType.call(this); //通过使用 apply() 或 call() 方法可以在新创建的对象上执行构造函数
}
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, "Greg");
//实例属性
this.age = 27;
}
var instance = new SubType();
alert(instance.name); //"Greg"
alert(instance.age); //27
3、组合继承
顾名思义,组合继承指的是将原型链和借用构造函数的技术结合到一块。其思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
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("Greg", 27);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Greg"
instance1.sayAge(); //27
var instance2 = new SubType("Nicholas", 29);
alert(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Nicholas"
instance2.sayAge(); //29
4、原型式继承
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);
anotherPerson.name = "Linda";
anotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
ECMAScript5 通过新增 Object.create() 方法规范化了原型式继承。
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, {
name: {
value: "Linda"
}
});
anotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
5、寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。
function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
alert("Hi");
};
return clone;
}
var person = {
name: "Greg",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
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;当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和 colors。于是,这两个属性就屏蔽了原型中的两个同名属性。
解决方法是寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
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);
};
三、对象中简单类型与引用类型的不同表现
1、在构造函数中的属性
使用构造函数创建对象时,每个对象开辟自己的堆空间,对构造函数中所有类型的属性执行深拷贝并存入堆空间。
也就是说,使用构造函数创建出来的对象,其属性相互独立,互不影响。
示例:
function Person() {
this.name = 'emiria';
this.age = [14, 104];
}
var person1 = new Person();
var person2 = new Person();
person1.age.push(200)
console.log("person1's age:", person1.age); // [14, 104, 200]
console.log("person2's age:", person2.age); // [14, 104]
2、在原型对象中的属性
原型对象中的属性由所有实例共享,但实例并不能重写原型中的属性。
如果我们对实例赋值一个属性,而该属性名与实例原型中的某个属性名同名,那么我们就在实例中创建了该属性,而非对实例中的属性进行重写。
示例:
function Person() {
}
Person.prototype.hobby = ['sing', 'write'];
var person1 = new Person();
var person2 = new Person();
person1.hobby = ['play games', 'also play games'];
console.log("person1's hobby:", person1.hobby); // ["play games", "also play games"]
console.log("person2's hobby:", person2.hobby); // ["sing", "write"]
注意:不能重写原型中的属性不代表不能改写。
事实上,我们可以改写原型中的属性,并且由于实例共享原型中的属性,因此改写的影响会辐射到所有继承自该原型的实例。
示例:
function Person() {
}
Person.prototype.hobby = ['sing', 'write'];
var person1 = new Person();
var person2 = new Person();
person1.hobby.push('play games');
console.log("person1's hobby:", person1.hobby); // ["sing", "write", "play games"]
console.log("person2's hobby:", person2.hobby); // ["sing", "write", "play games"]
参考书目:《JavaScript高级程序设计》