JavaScript 面向对象 —— 封装


作者:Seiya

时间:2019年07月08日


前言


JavaScript 的所有数据都可以看成对象,那是不是我们已经在使用面向对象编程了呢?当然不是。如果我们只使用 Number、Array、string 以及基本的{...}定义的对象,还无法发挥出面向对象编程的威力。

JavaScript 的面向对象编程和大多数其他语言如 Java、C# 的面向对象编程都不太一样。JavaScript 不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。



原始封装模式

如果我们要把"属性"(property)和"方法"(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么做呢?


假定我们把猫看成一个对象,它有"名字"和"颜色"两个属性。我们需要根据这个原型对象的规格(schema),生成两个实例对象。如下所示:

var cat1 = {};
cat1.name = '大毛';
cat1.color = '黄色';

var cat2 = {};
cat2.name = '二毛';
cat2.color = '黑色';

这就是最简单的封装了,把两个属性封装在一个对象里面。但是,这样的写法有两个缺点:

  • 如果多生成几个实例,写起来就非常麻烦;

  • 实例与原型之间,没有任何办法,可以看出有什么联系;



工厂模式


写一个函数,解决代码重复的问题:

function creatCat(name, color) {
	var o = new Object();
	o.name = name;
	o.age = color;
	o.sayName = function() {
		alert(this.name);
	};
	return o;
}

然后我们可以通过调用函数生成实例对象:

var cat1 = creatCat('大毛', '黄色');
var cat2 = creatCat('二毛', '黑色');

多用于创建多个含有相同属性,和方法的对象,避免代码的重复编写。 但是,这种方法的问题依然是,cat1 和 cat2 之间没有内在的联系,不能反映出它们是同一个原型对象的实例。



构造函数模式


为了解决从原型对象生成实例的问题,Javascript 提供了一个构造函数(Constructor)模式。

提示:

任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。


所谓"构造函数",其实就是一个普通函数,但是内部使用了 this 变量。对构造函数使用 new 运算符,就能生成实例,并且 this 变量会绑定在实例对象上。举例如下:

function Cat(name, color) {
	this.name = name;
	this.age = color;
	this.sayName = function() {
		alert(this.name);
	};
}

然后,我们就可以使用上面的函数生成实例对象:

var cat1 = new Cat('大毛', '黄色');
var cat2 = new Cat('二毛', '黑色');

new 关键字的执行逻辑可以抽象成如下表示:

var obj = {};
obj.__proto__ = Cat.prototype;
Cat.call(obj);
return obj;

可以简单描述为:

- (1) 创建一个新对象;

- (2) 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);

- (3) 执行构造函数中的代码(为这个新对象添加属性);

- (4) 返回新对象;

这时 cat1 和 cat2 会自动含有一个 constructor 属性,指向它们的构造函数。

alert(cat1.constructor == Cat); 	//true
alert(cat2.constructor == Cat); 	//true

Javascript 还提供了一个 instanceof 运算符,验证原型对象与实例对象之间的关系:

alert(cat1 instanceof Cat); 	//true
alert(cat2 instanceof Cat); 	//true

alert(cat1 instanceof Object); 	//true
alert(cat2 instanceof Object); 	//true

构造器模式解决了对象识别的问题,但它的缺点是:每个方法都在每个实例上重新创建了一次,于是存在一个浪费内存的问题。

拿上面的例子来讲:

alert(cat1.sayName == cat2.sayName);	 // false

表面上好像没什么问题,但是实际上这样做,有一个很大的弊端。每一次生成一个实例,都必须为重复的内容,多占用一些内存。这样既不环保,也缺乏效率。



原型模式


Javascript 规定,每一个构造函数都有一个 prototype 属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。我们可以把那些不变的属性和方法,直接定义在 prototype 对象上,如下所示:

function Cat(){ }

Cat.prototype={
	constructor: Cat,
	name: "二毛",
	color: '黑色',
	type: '猫科动物',
	eat: function(){alert('吃老鼠')}
}

然后,我们可以使用上面的函数生成实例:

var cat1 = new Cat();
var cat2 = new Cat();

alert(cat1.type); 		// 猫科动物
cat1.eat(); 			// 吃老鼠

这时所有实例的 type 属性和 eat() 方法,其实都是同一个内存地址,指向 prototype 对象,因此就提高了运行效率。

alert(cat1.eat == cat2.eat); 	//true

原型模式省略了构造函数初始化参数这个环节,原型中所有属性都被很多实例共享,共享对函数非常合适,基本属性也还行。通过在实例上添加同名属性,可隐藏原型中的对应属性值;

缺点:

他的共享属性,对于包含引用类型值的属性 如果实例重新赋值没什么影响,和基本类型一样,如果是操作修改 就有些问题了,会使得所有实例获取到的该属性都被修改, 所以也不单独使用


原型模式的验证方法

  • isPrototypeOf

    这个方法用来判断,某个 proptotype 对象和某个实例之间的关系:

    alert(Cat.prototype.isPrototypeOf(cat1)); 	//true
    alert(Cat.prototype.isPrototypeOf(cat2)); 	//true
    

  • hasOwnProperty

    每个实例对象都有一个 hasOwnProperty() 方法,用来判断某一个属性到底是本地属性,还是继承自 prototype 对象的属性:

    alert(cat1.hasOwnProperty('name')); 	//false
    alert(cat2.hasOwnProperty('type')); 	//false
    

  • in

    in 运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性:

    alert("name" in cat1); 		// true
    alert("type" in cat1); 		// true
    


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


创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。它有两个好处:

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

  • 这种混成模式还支持向构造函数传递参数


function Cat(name,color){
	this.name = name;
	this.color = color;
}

Cat.prototype.type = "猫科动物";
Cat.prototype.eat = function(){alert("吃老鼠")};

然后,我们可以使用上面的函数生成实例:

var cat1 = new Cat('大毛', '黄色');
var cat2 = new Cat('二毛', '黑色');

alert(cat1.type); 		// 猫科动物
cat1.eat(); 			// 吃老鼠


动态原型模式


动态原型模式把所有信息都封装在了构造函数中,通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点:

function Cat(name,color){
	this.name = name;
	this.color = color;
	/* 这里对一个方法判断就可以,然后初始化所有的,没必要都每个方法都判断 */
	if(typeof this.eat != "function"){
		Cat.prototype.eat = function(){ alert("吃老鼠")}
		......
	}
}

注意:

此处不能使用对象字面量形式重写原型, 因为这中写法是先创建的实例,然后在修改的原型,要是用对象字面量重写原型,会切断现有实例和新原型之间的联系, 导致方法实例上无此方法;



寄生构造函数模式


这种模式和工厂模式差不多,只是用了 new 初始化实例:

function Cat(name, color) {
	var o = new Object();
	o.name = name;
	o.age = color;
	o.eat = function() {
		alert("吃老鼠");
	};
	return o;
}

我们可以通过 new 关键字进行调用:

var cat1 = new Cat('大毛', '黄色');

cat1.eat()		// 吃老鼠

这种模式返回的对象与构造函数和构造函数的原型没有任何关系。也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。



稳妥构造函数模式


所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:

  • 新创建对象的实例方法不引用 this;

  • 不使用 new 操作符调用构造函数


function Cat(name, color) {
	var o = new Object();
	o.eat = function() {
		alert("吃老鼠");
	};
	o.say = function() {
		alert(name);
	}
	return o;
}

我们可以这样调用:

var cat1 = new Cat('大毛', '黄色');

cat1.say();		// 大毛

注意:

在以这种模式创建的对象中,除了使用 say() 方法之外,没有其他办法访问 name 的值。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。

稳妥构造函数模式提供的这种安全性,使得它非常适合在某些安全执行环境。与寄生构造函数模式类似,使用稳妥构造函数模式创建的对象与构造函数之间也没有什么关系,因此 instanceof 操作符对这种对象也没有意义。

最后更新时间: 2019-7-9 12:30:31