JavaScript面向对象系列:六、对象模式

前言

js有很多创建对象的模式,完成工作的方式也不是只有一种。可以随时定义自己的类型和自己的泛用对象。可以使用继承或者混入等其他技术令对象间行为共享。也可以利用js高级技巧来组织对象结构被改变。

私有成员和特权成员

js对象对象的所有属性都是公有的,且没有显式的方法指定某个属性不能被外界某个对象访问。然而,有时候可能不希望数据公有。

模块模式

模块模式是一种拥有私有数据的单件对象的模式。基本做法就是使用立即调用函数表达式(IIFE)来返回一个对象。IIFE是一种被定义后立即调用并产生结果的函数表达式,该函数表达可以包括任意数量的本地变量,它们在函数外不可见。因为返回的对象被定义在函数内部,对象的方法可以访问这些数据。(IIFE定义的所有对象都可以访问通用的本地变量)以这种方式访问私有数据的方法被称为特权方法。

基本格式如下

1
2
3
4
5
6
7
var yourObject = (function(){
//私有数据
return {
//公有方法和属性
};
})();

IIFE是js中一种很流行的模式,部分原因就是模块模式中的应用。

模块模式允许使用普通变量作为非公有对象属性。通过创建必报函数作为对象方法来操作它们。闭包函数就是一个可以访问其作用域外部数据的普通函数。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var person = (function (){
var age = 25;
return {
name:"laowang",
getAge:function(){
return age;
},
growOlder:function(){
age++;
}
};
})();
console.log(person.name); //"laowang"
console.log(person.getAge()); //25
person.age = 100;
console.log(person.getAge()); //25
person.growOlder();
console.log(person.getAge()); //26

模块模式还有一个变种叫暴露模块模式,它将所有的变量和方法都组织在IIFE的顶部,然后将它们设置到需要被返回的对象上。可以使用暴露模块模式改写上面的例子。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var person = (function (){
var age = 25;
function getAge(){
return age;
}
function growOlder(){
age++;
}
return {
name:"laowang",
getAge:getAge,
growOlder:growOlder
};
})();
console.log(person.name); //"laowang"
console.log(person.getAge()); //25
person.age = 100;
console.log(person.getAge()); //25
person.growOlder();
console.log(person.getAge()); //26

构造函数中的私有成员

模块模式在定义单个对象的私有属性上十分有效,但是对于那些同样需要私有属性的自定义类型,也可以在构造函数中使用类型的模式来创建每个实例的私有数据。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function Person (name){
var age = 25;
this.name = name;
this.getAge = function(){
return age;
}
this.growOlder = function(){
age++;
}
}
var person = new Person("laowang");
console.log(person.name); //"laowang"
console.log(person.getAge()); //25
person.age = 100;
console.log(person.getAge()); //25
person.growOlder();
console.log(person.getAge()); //26

如果需要所有实例可以共享的私有数据,可以结合模块模式和构造函数。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var Person = (function (name){
var age = 25;
this.name = name;
function InnerPerson (name){
this.name = name;
}
InnerPerson.prototype.getAge = function(){
return age;
}
InnerPerson.prototype.growOlder = function(){
age++;
}
return InnerPerson;
})();
var person1 = new Person("laowang");
var person2 = new Person("xiaowang");
console.log(person1.name); //"laowang"
console.log(person1.getAge()); //25
console.log(person2.name); //"xiaowang"
console.log(person2.getAge()); //25
person1.growOlder();
console.log(person1.getAge()); //26
console.log(person2.getAge()); //26

混入

js中大量使用了伪类继承和原型对象继承,还有另一种伪继承的手段叫混入。一个对象在不改变原型对象链的情况下得到了另一个对象的属性被称为混入。第一个对象(接收者)通过直接复制第二个对象(提供者)的属性从而接收了这些属性。

例如

1
2
3
4
5
6
7
8
function mixin(receiver,supplier){
for(var property in supplier){
if(supplier.hasOwnProperty(property)){
receiver[property] = supplier[property];
}
}
return receiver;
}

可以通过混入而不是继承给一个对象添加事件支持

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function EventTarget () {
}
EventTarget.prototype = {
constructor:EventTarget,
addListener:function(type,listener) {
//不存在数组就创建一个数组
if (!this.hasOwnProperty("_listeners")) {
this._listeners = [];
}
if (typeof this._listeners[type] == "undefined") {
this._listeners[type] = [];
}
this._listeners[type].push(listener);
},
fire:function(event) {
if (!event.target) {
event.target = this;
}
if(!event.type){
throw new Error("Event object missing 'type' propertype.");
}
if(this._listeners && this._listener[event.type] instanceof Array){
var listeners = this._listeners[event.type];
for(var i = 0,len = listeners.length; i < len; i++){
listeners[i].call(this,event);
}
}
},
removeListener:function(type,listener){
if(this._listener && this._listener[type] instanceof Array){
var listeners = this._listeners[type];
for(var i = 0,len = listeners.length; i < len; i++){
if(listeners[i] === listener){
listener.split(i,1);
break;
}
}
}
}
};

js对象中支持事件十分有用。

1
2
3
4
5
6
var person = new EventTarget();
person.name = "laowang";
person.sayName = function () {
console.log(this.name);
this.fire({type:"namsaid",name:name});
}

这段代码中,person作为EventTarget的实例被创建出来,然后添加各种跟person相关的属性。可惜的是这意味着person实际上是一个EventTarget而不是一个Object或者其他自定义类型。另外,你还需要承受手工添加一批新属性的开销。解决这个问题的方法是使用伪类继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person (name) {
this.name = name;
}
Person.prototype = Object.create(EventTarget.prototype);
Person.prototype.constructor = Person;
Person.prototype.sayName = function() {
console.log(this.name);
this.fire({
type:"namesaid",
name:name
});
}
var person = new Person("laowang");
console.log(person instanceof Person); //true
console.log(person instanceof EventTarget); //true

这个例子中,一个新的Person类型继承自EventTarget。随后可以在Person的原型对象上添加你需要的方法。然而不够简洁。

更简洁的例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person (name) {
this.name = name;
}
minix(Person.prototype,new EventTarget());
minix(Person.prototype,{
constructor:Person,
sayName:function(){
console.log(this.name);
this.fire({
type:"namesaid",
name:name
});
}
});
var person = new Person("laowang");
console.log(person instanceof Person); //true
console.log(person instanceof EventTarget); //true

有时候可能需要一个对象的属性,但是不想用伪类继承的构造函数。可以使用混入来创建自己的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person (name) {
this.name = name;
}
var person = mixin(new EventTarget(),{
get name(){
return "laowang";
},
sayName:function(){
console.log(this.name);
this.fire({
type:"namesaid",
name:name
});
}
});
console.log(person.name); //"laowang"
person.name = "xiaowang";
console.log(person.name); //"xiaowang"

这段代码定义了仅有getter的访问器属性name。这意味着对该属性赋值应该不起作用。但是,由于在person对象里面该访问器属性变成了数据属性,你就有可能改写name的值。在调用minix()时,提供者name属性的值被读取后赋值给接受者name属性。在这个过程中没有机会定义一个新的访问器属性,从而使接收者的name属性成为了一个数据属性。

如果想要访问器属性被复制成访问器属性,需要一个不同的minix()函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function mixin (receiver,supplier) {
Object.keys(supplier).forEach(function(property) {
var descriptor = Object.getOwnPropertyDescriptor(supplier,property);
Object.defineProperty(receiver,property,descriptor);
});
return receiver;
}
var person = mixin(new EventTarget(),{
get name(){
return "laowang";
},
sayName:function() {
console.log(this.name);
this.fire({
type:"namesaid",
name:name
})
}
});
console.log(person.name); //"laoawng"
person.name = "xiaowang";
console.log(person.name); //"laowang"

作用域安全的构造函数

构造函数也是函数,所以可以不用 new 操作符直接调用它们来改变this的值。在非严格模式下,this被强制指向全局对象,这个做法会导致无法预知的结果。在严格模式下,构造函数会抛出错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person (name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
var person = Person("laowang");
console.log(person instanceof Person); //false
console.log(typeof person); //"undefined"
console.log(name); //"laowang"

很多內建构造函数,例如 ArrayRegExp 不需要new也可以工作,这是因为他们被设计成为作用域安全的构造函数。一个作用域安全的构造函数有没有new都可以工作,并返回同样的对象。

一个作用域安全的构造函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name){
if(this instanceof Person){
this.name = name;
}else{
return new Person(name);
}
}
var person1 = new Person("laowang");
var person2 = Person("laowang");
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Person); //true

总结

js有很多不同的方式创建和组装对象。虽然js没有一个正式的私有属性的概念,但是可以创建仅在对象内可以访问的数据或者函数。对于单件对象,你可以使用模块模式对外界隐藏数据。可以使用立即调用表达式定义仅可被创建的对象访问的本地变量和函数。特权方法是可以访问对象私有数据的方法。你还可以创建具有私有属性的构造函数,一种方法是在构造函数定义变量,另一种方法是使用IIFE来创建所有实例共享的私有数据。

混入是一种给对象添加功能,同时便面继承的强有力方式。混入将一个属性从一个对象复制到另一个,从而使得接收者在不需要继承提供者的情况下获得其功能。和继承不同,混入令你在创建对象后无法检查属性来源。因此,混入最适合被用于数据属性或者小函数。如果需要更强大的功能且知道该功能来自哪里,继承仍然是我们推荐的做法。

作用域安全的构造函数是可以不用new都可以被调用来生成新的对象实例的构造函数。这种模式之所以能工作,是因为this在构造函数一开始执行时就已经指向自定义类型的实例,可以根据new的使用与否来决定构造函数的行为。