JaveScript虽然说是支持面向对象的编程方法,但是JavaScript的面向对象在其他的强类型的面向对象语言(如Java,C#)甚至在其他弱类型的脚本语言(如Python)其面向对象的特性可以用捉襟见肘来形容,需要通过一定的技巧才能勉强实现其他面向对象语言的特性。通过这一系列文章来介绍一下JavaScript面向对象的编程方法,也供我复习一下原生JavaScript的特性。
说到面向对象编程肯定少不了对象这一概念,对象作为一系列属性和方法的封装,体现了面向对象编程的封装性。JavaScript中所有对象都继承于Object
,z最基础的创建对象以及添加属性和方法的方式就是
var o=new Object();
o.value1="1";
o.value2="2";
o.getValue1=function(){
return this.value1;
};
但这种方式并没有体现出封装性的特点,添加属性和方法的场合很零散,不容易管理。所以接下来会介绍JavaScript创建具有封装性的对象的几种方法。
如果只是想了解创建对象的最实用方法可以直接跳到组合使用构造函数模式和原型模式。
具有封装性对象的构造的常用模式主要包括:
工厂模式是属于最简单的一种构造对象的模式。其思想很简单,就是使用一个函数,在函数内部创建一个Object
对象,然后将属性和方法赋予这个对象,接着返回这个对象的引用,具体如下:
function Person(name,age,job){
var o=new Object();
o.name=name;
o.age=age;
o.job=job;
o.sayName=function(){
return this.name;
}
}
var person1=function("John",18,"Police");
这种方法很轻地解决了对象封装性的问题,但没有起到识别对象的作用。什么是识别对象呢?就是在这个额例子中你并不知道这是一个Peron
对象,它只会反应出它是一个Object
对象。详细见下面截图。为了解决这个问题就出现了构造函数模式。
JavaScript中的构造函数跟传统面向对象语言的构造函数不是同一个概念。
任何函数只要使用
new
操作符来调用,那它就可以作为构造函数。——《JavaScript高级程序设计》
我们知道Function
也是一种特殊的Object
,所以我们也可以给Function
对象添加相应的属性,然后再加上封装性的要求,然后就可以通过一下代码使用构造函数模式重写Person
对象的代码
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
this.sayName=function(){
return this.name;
}
}
var person1=new Person("John",18,"Police");
这样就能解决对象识别的问题,从下图我们可以看到person1
是Person
对象的实例。
那么构造函数模式是不是完美了呢,并不是。构造函数模式虽然能够完美的解决对象识别和封装性的问题,但是它并没有解决共有属性的统一引用的问题。例如我创建两个Person
对象的实例person1
,person2
,他们两个都有方法sayName()
的引用但它们两个不是同一个引用,而是分别创建的两个方法。
为了解决统一引用的问题有人想了一种方法,如下
function sayName(){
return this.name;
}
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=jobl
this.sayName=sayName;
}
var person1=new Person("John",18,"Police");
var person2=new Person("Jerry",18,"Doctor");
这种方法虽然解决了统一引用的问题,但破坏了对象的封装性。为了解决统一引用问题,就出现了原型模式
原型prototype
是一个指向一个对象的指针,每一个函数都有这么一个指针。prototype
对象是按照特定的规则产生的。在这里我们只需要了解一点,对象实例可以访问对象原型中的值,但不可以重写原型中的值。通过以下代码可以验证
function Person(){}
Person.prototype.name="Jerry";
person1=new Person();
person2=new Person();
console.log(person1.name);//Jerry
console.log(person2.name);//Jerry
person2.name="John";
console.log(person1.name);//Jerry
console.log(person2.name);//John
当你修改person2
实例的name
属性时并不会修改对象原型的值,只会给对象实例新增加一个属性并为他赋予新值。
原型模式创建对象就像上面的代码一样就行,但单纯的原型模式创建出的对象实例并没有自己私有的属性,所有属性都是共有的,这就和构造函数模式构成了两个极端了,一个是完全共享,一个是完全私有。为了权衡这两者之间的关系我们就将这两者混合起来出现了组合使用构造函数模式和原型模式。
这种模式是创建自定义对象最常见的模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共有属性。
首先通过构造函数模式定义实例属性
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
}
然后通过原型模式定义方法和共有属性以及定义构造函数
Person.prototype={
constructor:Person,
sayName:function(){
return this.name;
}
}
然后就可以创建实例了
var person1=new Person("John",18,"Police");
var person2=new Perons("Jerry",19,"Doctor");
最终的效果如下
这种构造函数与原型混成的模式,是目前ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式
——《JavaScript高级程序设计》
既然我们已经有了接近完美的方法,那么接下来的几种方式,就是在特定情况下使用的方式。
动态原型模式是对组合使用构造函数模式和原型模式的改良,虽然上述的模式很好,但是对象的构造函数和原型是独立存在的,对于传统的面向对象语言来说还是不足。所以有人就想办法在构造函数中初始化原型。这种模式的合兴是通过检查某个应该存在的方法或这共有属性判断原型是否需要初始化。通过这种模式重写Person
对象
function Person(name,age,job){
this.name=name;
this.age=age;
this.job=job;
if(typeof this.sayName!="function"){
Person.prototype.sayName=function(){
return this.name;
};
}
}
这样就有更好的封装性,并且保持了同时使用构造函数和原型的优点。
寄生构造函数模式是比较特殊的模式,存在的问题和工厂模式存在的问题一样,但却有着跟构造函数模式一样的写法。这种模式在可以使用其他模式的情况下不使用用。使用的场景一般只用在为原生的对象添加其它自定义方法时使用,如我们创建一个特殊的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");
console.log(colors.toPipedString());//"red|blue|green"
这里我们就创建了SpecialArray
对象,比普通的Array
对象多了toPipedString()
方法。
稳团构造函数模式主要是用来构造稳妥对象的,
所谓的稳妥对象,指的是没有公共属性,而且其方法也不引用
this
对象。——《JavaScript高级程序设计》
稳妥构造函数模式与寄生构造函数模式类似,不同点是:创建对象的实例方法不引用this
,不适用new
操作符调用构造函数。
代码如下
function Person(name,age,job){
var o=new Object();
o.sayName=function(){
return name;
}
return o
}
在这里除了实例的sayName()
方法外,没有其他任何的方法访问到对象的name
属性。这个模式保证了即使有其他的代码给这个对象添加方法或者成员,但也不可能有别的方法访问传入到构造函数中的原始数据。所以稳妥构造函数模式能够提供一定的安全性。
通过这篇博客我们浏览了JavaScript中面向对象设计中关于创建具有封装性对象的一般模式。也了解了接近最优的创建方法,以及一些创建特殊用途对象的方法。看完之后可能会有人说ES6已经支持class
关键字了不必要再死抠ES5的历史问题了,其实class
关键字只是一个语法糖
ES2015 classes are a simple sugar over the prototype-based OO pattern. ——Learn ES2015·Babel
所以对JS构造对象的理解对于理解ES6还是是有一点用处的。
参考资料《JavaScript高级程序设计》