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 操作符对这种对象也没有意义。