6.核心JavaScript笔记:面向对象(设计模式)

本系列内容由ZouStrong整理收录

整理自《JavaScript权威指南(第六版)》,《JavaScript高级程序设计(第三版)》

之前,我们使用对象直接量或者构造函数创建对象,简单直观,而且方便;但是有个明显的缺点:使用同一个接口创建很多对象,会产生大量重复代码

为了解决这个问题,开始使用各种模式来创建对象或者书写代码

一. 工厂模式

工厂模式抽象了创建具体对象的过程,用函数来封装创建对象的细节(函数充当了一个“工厂”,批量的“生产”相似的对象,所以叫工厂模式)

function test(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 p = test("zou", 25, "Student"); 

优点:无数次调用,易于创建多个相似对象

缺点:无法解决对象识别的问题(不能将对象标识为一种特定的类型)

二. 构造函数模式

构造函数可用来创建特定类型的对象,可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法

function Person(name, age, job){ 
	this.name = name; 
	this.age = age; 
	this.job = job; 
	this.sayName = function(){ 
		alert(this.name); 
	}; 
} 
var p = new Person("zou", 25, "Student");

构造函数始终都应该以一个大写字母开头,构造函数与其他函数的唯一区别,就在于调用它们的方式不同,任何函数,只要通过new操作符来调用,那它就可以作为构造函数

所以上面的函数,如果是直接调用,那就是相当于把属性和方法添加给window对象(非严格模式),如果通过call调用,则把属性和方法添加给指定对象

创建的对象继承的constructor属性指向Person构造函数

与工厂模式不同的是,没有显式创建对象;直接将属性和方法赋给了this对象;也没有return语句,实际上经历了以下4个步骤

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象(并赋值给变量)

优点:可以将对象实例标识为一种特定的类型

alert(p instanceof Person);   //true 
alert(p instanceof Object);   //true

缺点:定义的对象方法都要在每个实例上重建一遍(不同的Function实例),占内存

构造函数模式改进

创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面(this动态绑定的本质)

function Person(name){ 
	this.name = name; 
	this.sayName = sayName; 
} 
function sayName(){ 
	alert(this.name); 
} 
var p = new Person("strong"); 

将函数定义转移到构造函数外部,构造函数内仅包含一个指向函数的指针,使得不同实例共享在全局作用域中定义的同一个函数

优点: 解决了两个函数做同一件事的问题

缺点: 全局作用域中定义的函数只能被某个对象调用,这让全局作用域情何以堪。且如果对象包含很多方法,那么就要定义很多个全局函数,这个自定义的引用类型就没有封装性可言

三. 原型模式

每个函数都有一个原型(prototype)属性——指向一个原型对象(也就是通过调用构造函数而创建的那个对象实例的原型对象, 该对象的用途是包含由特定类型的所有实例共享的属性和方法——基于原型的继承)

注:对象实例和原型对象的关系是:obj.constructor.prototype

// 不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中
function Person(){ 
} 
Person.prototype.name = "Nicholas"; 
Person.prototype.sayName = function(){ 
	alert(this.name); 
}; 
var person1 = new Person(); 
var person2 = new Person();
alert(person1.sayName == person2.sayName); //true

优点:所有实例共享了原型对象的属性和方法,防止了不必要的内存浪费

缺点:冗余输入,封装性不好

原型模式改进

前面例子中每添加一个属性和方法就要敲一遍Person.prototype。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象

function Person(){ 
} 
Person.prototype = { 
	name : "Nicholas",
	sayName : function () {
		alert(this.name);
	}
};

最终结果相同,但有一个例外:constructor属性不再指向Person了,我们在这里使用的语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新的自定义对象的constructor属性(指向Object构造函数)

var friend = new Person(); 
alert(friend instanceof Object);  //true 
alert(friend instanceof Person);  //true 
alert(friend.constructor == Object);  //true 

如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值

function Person(){ 
} 
Person.prototype = { 
	constructor : Person
	...
}; 

注:以这种方式重设constructor属性会导致它的[Enumerable]特性被设置为true。默认情况下,原生的constructor属性是不可枚举的

但是可以使用Object.defineProperty()方法将属性设为不可枚举(ES5)

function Person(){ 
} 
Person.prototype = { 
	name : "Nicholas", 
	sayName : function () { 
		alert(this.name); 
	} 
}; 
//重设构造函数,只适用于ECMAScript 5兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", { 
	enumerable: false, 
	value: Person 
}); 

原型的动态性

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此

其原因可以归结为实例与原型之间的松散连接关系。当我们查找某个属性时,首先会在实例中搜索该属性,在没找到的情况下,会继续搜索原型。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的属性并返回

尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系

function Person(){ 
} 
var friend = new Person(); 
Person.prototype = { 
	constructor: Person, 
	name : "Nicholas", 
	sayName : function () { 
		alert(this.name); 
	} 
}; 
friend.sayName(); //error 

原型模式的优缺点

优点:所有实例共享了原型对象的属性和方法,防止了不必要的内存浪费

缺点:原型模式让所有对象实例共享(原型对象的)属性和方法,但原型模式省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性

缺点:其次,原型中所有属性是被很多实例共享的,这种共享对于函数和基本类型值非常合适,对于包含引用类型值的属性来说,会出现“牵一发而动全身”的不良影响

function Person(){ 
} 
Person.prototype = { 
	constructor: Person,  
	friends : ["Shelby", "Court"], 
}; 
var person1 = new Person(); 
var person2 = new Person(); 
person1.friends.push("Van"); 
alert(person2.friends); //"Shelby,Court,Van" 

假如我们的初衷就是像这样在所有实例中共享一个数组,那么对这个结果我没有话可说。可是,实例一般都是要有属于自己的全部属性的

四. 组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式(最常见最常见)

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性

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

优点:每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存

五. 动态原型模式

把所有信息都封装在构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型

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("strong", 26, "Student");

这段代码只会在初次调用构造函数时才会执行。此后,原型已经完成初始化,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说非常完美。其中,if 语句检查的可以是初始化之后应该存在的任何属性或方法——不必用一大堆if语句检查每个属性和每个方法;只要检查其中一个即可。对于采用这种模式创建的对象,还可以使用instanceof操作符确定它的类型

注:使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系

六. 寄生构造函数模式

寄生构造函数模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象(很像是典型的构造函数模式)

function Person(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 friend = new Person("strong", 29, "s");

这个模式像是工厂模式和构造函数模式的结合体

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

关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同

为此,不能依赖 instanceof 操作符来确定对象类型。由于存在上述问题,我们建议在可以使用其他模式的情况下,不要使用这种模式。

七. 稳妥构造函数模式

稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序改动时使用

稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同

  • 一是新创建对象的实例方法不引用 this
  • 二是不使用 new 操作符调用构造函数

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

以这种模式创建的对象中, 除了使用 sayName()方法之外,没有其他办法访问 name 的值;这样,变量 friend 中保存的是一个稳妥对象,而除了调用 sayName()方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境

与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此 instanceof 操作符对这种对象也没有意义

你可能感兴趣的:(JavaScript)