六、对象模式
6.1 私有成员和特权成员
通过命名规则区分私有成员,在不希望公有的属性名字前加下划线(如this._name)。也还有其他很多方法不依赖命名规则。
6.1.1 模块模式
模块模式是一种用于创建拥有私有数据的单件对象的模式。其基本做法是使用
立即调用函数表达式(IIFE)来返回一个对象。
IIFE是一种被定义后立即调用并产生结果的函数表达,表达式尾部使用了 小括号() 运算符。该函数表达式可以包括任意数量的本地变量,它们在函数外不可见。IIFE仅存在于被调用的瞬间,一旦执行后立即被销毁。
特权方法是指通过IIFE中定义的对象的方法来访问
私有数据(本地变量)。
下面是模块模式的基本格式。
var obj = (function(){
//本地变量
return{
//定义要返回的对象,包含属性和方法,用逗号分隔
};
}());
模块模式允许使用普通变量作为非公有对象属性。
闭包函数是一个可以访问其作用域外部数据的普通函数。
下面的代码中,通过创建闭包函数作为对象方法来操作他们。
var person = (function(){
var age = 26; //定义在这里的变量age是该对象的私有数据,不能被外界直接访问
return{
name: "liang",
getAge: function(){ //这是闭包函数,现在作为person对象的方法
return age;
},
growOlder: function(){
age++;
}
};
}());
console.log(person.name); //liang
console.log(person.getAge()); //26
person.growOlder();
console.log(person.getAge()); //27
模块模式还有另一个变种,叫暴露模块模式。它将所有的变量和方法都组织在IIFE的顶部,然后将他们设置到需要被返回的对象上。
var person = (function(){
var age = 26;
function getAge(){
return age;
}
function growOlder(){
age++;
}
return{
name: "liang",
getAge: getAge,
growOlder: growOlder
};
}());
console.log(person.name); //liang
console.log(person.getAge()); //26
person.growOlder();
console.log(person.getAge()); //27
6.1.2 构造函数的私有成员
在构造函数中使用类似于模块模式的模式来创建每个实例的私有数据。构造函数创建一个本地作用域并返回this对象。
function Person(name){
var age = 26;
this.name = name;
this.getAge = function(){
return age;
};
this.growAge = function(){
age++;
};
}
var person1 = new Person("liang");
var person2 = new Person("zhu");
console.log(person1.name); //liang
console.log(person1.getAge()); //26
person1.growAge();
console.log(person1.getAge()); //27
person1.age = 35;
console.log(person1.age); //35
console.log(person1.getAge()); //27
console.log(person2.getAge()); //26
//age是person1和person2的私有数据,改变其中一个不会影响另一个
//与原型对象要区分开来,prototype中存储的数据是被实例共享的
//与原型对象要区分开来,prototype中存储的数据是被实例共享的
如果需要让所有实例能共享私有数据,可以结合模块模式和构造函数。
var Person = (function(){
var age = 26;
//构造函数innerPerson
function innerPerson(name){
this.name = name;
}
innerPerson.prototype.getAge = function(){
return age;
};
innerPerson.prototype.growOlder = function(){
age++;
};
return innerPerson;
}());
var person1 = new Person("liang");
var person2 = new Person("zhu");
console.log(person1.getAge()); //26
console.log(person2.getAge()); //26
person1.growOlder();
console.log(person1.getAge()); //27
console.log(person2.getAge()); //27
上面代码中,innerPerson构造函数被定义在一个IIFE中。变量age被定义在构造函数外,被两个原型对象的方法调用。IIFE返回innerPerson构造函数作为全局作用域里的Person构造函数。最终,Person的全部实例得以共享age变量。
6.2 混入
除了伪类继承和原型对象继承,还有一种伪继承的手段叫混入。
一个对象在不改变原型对象链的情况下得到了另一个对象的属性被称为混入。
下例是传统的利用函数实现的混入:利用for循环使第一个对象直接复制第二个对象的属性。
//定义mix()
function mixin(receive, supplier){
for(var property in supplier){
if(supplier.hasOwnProperty(property)){ //如要复制不重复的属性加!(property in receive) &&
receive[property] = supplier[property];
}
}
return receive;
}
下面是一个支持事件的自定义类型。这个EventTarget类型可以为任何对象提供基本的事件处理。你可以添加和删除监听者,也可以在对象上直接触发事件。
//定义EventTarget类型
function EventTarget(){
}
EventTarget.prototype = {
constructor: EventTarget,
addListener: function(type,listener){
//事件监听者存储在_listeners中,仅在addListener()第一次被调用时创建
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' property.");
}
if(this._listeners && this._listeners[event.type] instanceof Array){
var listeners = this._listeners[event.type];
for(var i=0,len=listeners; i<len; i++){
listeners[i].call(this,event);
}
}
},
removeListener: function(type,listener){
if(this._listeners && this._listeners[type] instanceof Array){
var listeners = this._listeners[type];
for(var i=0, len=listeners.length; i<len; i++){
if(listeners[i] === listener){
listeners.splice(i,1);
break;
}
}
}
}
};
下面是EventTarget的实例。
var target = new EventTarget();
target.addListener("message", function(event){
console.log("Message is " + event.data);
});
target.fire({
type: "message",
data: "Hello world!"
});
要让另一个对象支持事件,通过创建一个EventTarget的实例并添加任何需要的属性。
//...定义EventTarget类型...
var person = new EventTarget();
person.name = "liang";
person.sayName = function(){
console.log(this.name);
this.fire({ type: "namesaid", name: name});
};
person现在实际上是一个EventTarget而不是一个Object或其他自定义类型。另外,它还需要花时间再添加一些新属性。使用伪类继承能解决这个问题。如下例:
//...定义EventTarget类型...
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("liang");
console.log(person instanceof Person); //true
console.log(person instanceof EventTarget); //true
上面这个代码还是不够简洁,可以通过混入简化,使用最少的代码将新属性复制到原型对象中,如下例。
//...定义mixin()...
//...定义EventTarget类型...
function Person(name){
this.name = name;
}
//Person.prototype混入EventTarget的一个新实例来获取事件行为
mixin(Person.prototype,new EventTarget());
//Person.prototype混入constructor和sayName()来完成原型对象的组装
mixin(Person.prototype, {
constructor: Person,
sayName: function(){
console.log(this.name);
this.fire({ type:"namesaid",name: name });
}
});
var person = new Person("liang");
console.log(person instanceof Person);
//true
console.log(person instanceof EventTarget);
//false
当你需要使用一个对象的属性,但不想要伪类继承的构造函数。可以直接使用混入来创建自己的对象。
//...定义mixin()...
//...定义EventTarget类型...
var person = mixin(new EventTarget(), {
name: "liang",
sayName: function(){
console.log(this.name);
this.fire({ type: "namesaid",name: name )};
}
});
EvenTarget实例混入了一些新的属性来创建person对象而没有改变person的原型对象链。
以这种方式混入需注意,提供者的访问器属性会变成接收者的数据属性,不小心就会改写它们。这是因为接收者的属性是被赋值语句而不是Object.defineProperty() 创建,提供者的属性当前的值被读取后赋值给接受者的同名属性。
如果想要访问器的属性被复制成访问器属性,需要一个不同的mixin() 函数,如下。
// ...定义EventTarget类型...
//定义新的mixin() 函数
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 "liang"
},
sayName: function(){
console.log(this.name);
this.fire({ type:"namesaid",name:name });
}
});
console.log(person.name); //liang
person.name = "zhu";
console.log(person.name); //liang
Object.keys()获得supplier的所有可枚举属性,然后用forEach() 方法迭代,对提供者的每一个属性获得其属性描述符。
然后通过Object.defineProperty() 添加给接受者,确保所有的属性相关信息都被传递给接受者,而不只是属性的值。
6.3 作用域安全的构造函数
一个作用域安全的构造函数有没有new都可以工作,并返回同样类型的对象。比如内建构造函数Array和RegExp不要new 操作符也可以工作,它们被设计为作用域安全的构造函数。
下面例子,不使用new 操作符会出现的问题:
function Person(){
this.name = name;
}
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = Person("liang");
//不使用new操作符
var person2 = new Person("zhu");
console.log(person1 instanceof Person);
//false
console.log(person2 instanceof Person);
//true
console.log(typeof person1);
//undefined
console.log(typeof person2);
//Objectd
这段代码现在运行在非严格模式,如果在严格模式下这么做会抛出错误。
当用new调用一个函数时,this指向的新创建的对象已经属于该构造函数所代表的自定义类型。因此,可以在函数内用instanceof来检查自己是否被new调用。
function Person(name){
if(this instanceof Person){
//使用了new 时执行
this.name = name;
}else{
//没有使用new 时执行
return new Person(name);
}
}
//根据new是否使用,来执行函数不同行为。(用来保护那些偶然忘记使用new的情况)
Object, Array, RegExp 和 Error是作用域安全的构造函数。