笔者将从如下三方面进行简要的总结:
- 如何理解JavaScript的面向对象思想
- 如何创建对象
- 如何继承
如何理解JavaScript的面向对象思想?
ECMA-262中对象的定义:“无序属性的集合,其属性可以包含基本值、对象或者函数”。
这里将无序属性的一个集合称为对象,那么对象一定是某种数据结构,而能够容纳无序数据的最高效数据结构,最可能的就是散列表,也就是哈希表。令人困惑的是,这里没有首先引入类的概念,而是直接定义的对象,当你翻遍所有的规范,也不会发现JS有关于类的定义,这也是它与其他OO语言不同之处。
没有了类,那应该如何定义以及创建一个对象呢?简单地说,对象由构造函数创建,而属性则可以随时动态地添加。若要深究的话,构造函数也是一个对象,它也有被创建的周期。
JS对象相对而言还有一个属性类型的概念。ECMA-262第5版在定义只有内部采用的特性(attribute)时,描述了属性(property)的各种特征。ECMA-262定义这些特性是为了实现JS引擎用的,因此在JS中不能直接访问它们。虽然语法上JS是弱类型的语言,但是其内部类型是不是也划分得很清楚。而这些特性,你也可以理解为是类型的标签,是不是觉得有点像是C#语法中类的特性一样。只不过这里严格的说,是对象属性的标签。
属性类型,我们需要注意的是,它分为数据属性和访问器属性两种。
关于数据属性,举个简单例子:
var obj = {
fun : function() {
console.log('i am a function.');
}};
var re = Object.getOwnPropertyDescriptor(obj,'fun');
console.log(re);
将该段代码放入控制台运行后,显示如下:
从代码可以看出,通过对象字面量语法,obj对象的fun属性被赋值了一个匿名的函数表达式。而通过Object的getOwnPropertyDescriptor方法,可以看出这里的fun属性的特性值。需要注意的是,Value特性值存储的是匿名函数的指针(其实这里Object的值也是一个指针,它指向的是一个函数类型的对象)。
而访问器属性则不包含数据值了,它们包含一对儿getter和setter函数(不过,这两个函数都不是必需的)。其实这非常类似于C#中类的属性。
说到这里大家对JS的面向对象思想是不是有一个新的认识了呢。
如何创建对象?
C#中对象的创建是通过构造函数创建的,同理,JS中的对象也是依靠构造函数创建,JS中创建对象归纳起来主要有三个模式:
- 工厂模式
- 构造函数模式
- 原型模式
工厂模式
关于工厂模式,举个简单例子:
function createObj(name){
var o = new Object;
o.name = name;
o.printName = function(){
console.log(this.name);
}
return o;
}
var obj = createObj('ray');
console.log(obj);
这里需要注意的: new是一个操作符,跟在new后面的是表达式,所以可以是 new Object ; new Object() ;new fun;new fun()都是可以的。而前面也稍微提过,Object其实是一个全局的函数指针,而它指向的只能是基本对象的构造函数。this类似于C++中对象this指针,它指向的永远是调用this的该对象自己。
构造函数模式
构造函数模式的例子:
function Obj(name){
this.name = name;
this.printName = function(){
console.log(this.name);
};
}
var obj = new Obj('ray');
var obj2 = new Obj;
console.log(obj);
console.log(obj2);
结合前面的工厂模式,其实就是自定义了构造函数而已。而obj2调用构造函数的时候,就没有传入参数,所以显示结果是undefine。
其实工厂模式和构造函数的例子都很好理解,但是这两个模式是有缺点的。就是每次创建对象都要执行一次构造函数,如果构造函数创建的对象多了,显然不是很划算,要是构造函数只执行一次,后面的对象都共用这一次的方法不就妥了么?第三种模式则正好解决了这个问题。
原型模式:
先来看一下原型模式的例子:
function Obj(){
}
Obj.prototype.name = "ray";
Obj.prototype.printName = function(){
console.log(this.name);
}
var obj = new Obj;
obj.printName();
这里先引入一个事实,就是我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有对象共享的属性,我们把它叫做原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,而这个属性存储着一个指回该函数的指针。而由该构造函数创建的对象则包含了一个属性__proto__,该属性也同样指向原型对象。是不是很绕,看看下面的图就清楚了。
那么这三种模式,我们应该怎样使用呢?其实创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义对象属性,而原型模式用于定义共享的属性。请看下面的例子。
function Obj(name){
this.name = name;
}
Obj.prototype = {
constructor : Obj,
printName : function(){
console.log(this.name);
}
}
var obj = new Obj('ray');
obj.printName();
这里需要注意的是,如果采用对象字面量语法的话,必须要指明constructor的值,否则会指向Object构造函数。稍微解释下就是,我们知道每次创建一个函数,都会同时创建它的prototype对象,这个对象也会自动获得constructor属性。如果使用对象字面量语法,则本质上完全重写了默认的prototype对象,因此constructor属性也就变成新的属性了。
如何继承?
继承是OO语言中一个最为人熟悉的概念。许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。但是,JS中的函数实为对象,是没有签名的,所以在JS中无法实现接口继承。JS只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
原型链
那么什么是原型链呢?原型链主要是利用原型让一个引用类型继承另一个引用类型的属性,简单理解起来就是,“我要继承一个对象,我通过原型对象来引用它就是了”。请看如下代码:
function Father(){
this.familyName = 'wang'
}
Farther.prototype.sayFamilyname = function(){
console.log(this.familyName);
}
function Child(){
}
Child.prototype = new Father();
var man = new Child();
man.sayFamilyname();
console.log('this is my family name : ' + man.familyName);
简单用图来梳理一下:
可以清楚的看到,man通过引用Child的原型对象(等于Father的一个实例对象,含有_proto_指针)间接的引用了Father以及Father的原型对象。说白了,JS的继承其实就是通过指针指向来起作用的。