JavaScript里也可以像Java等面向对象的语言世界里创建自定义的类型,但是由于JavaScript中不支持使用class关键字来创建自定义的类型,因此我们只能另辟蹊径……下面我们一起来看在JavaScript中如何使用OO。
1工厂模式
调用createPerson就可以创建一个对象,看起来我们已经创建了一个自定义的类型,该类型可以用来表示Person,但是问题是,怎么让解析引擎知道这是一个Person呢?很抱歉,这种工程模式解决不了这种问题,所有的实例都是Object类型。
2.构造函数模式
为了解决对象识别的问题,JavaScript中又出现了一种新的模式,即这里的构造函数模式,构造函数其实就是一个普通的函数,除了函数名称首字母一般要大写外,其他的和普通的函数没什么区别。我们用构造函数模式改造是上面的工厂模式:
与工程模式相比,这次我们没有在构造函数中显示地创建一个Object,也没有return语句,并且把所有的属性与方法赋值给this。而在创建实例的时候,使用了我们可爱的new操作符,那我们来看下new操作符是怎么创建出一个Person的实例的:
- 构建一个新的对象,这时这个对象可不单单是Object了,它还是Person,将该对象的[[Prototype]]设置为对应构造函数的原型对象;
- 将构造函数的作用域赋值给该对象;
- 执行构造函数中的代码;
- 返回该对象。
这样构造出来的对象便可以通过instanceof操作符来识别了:
刚刚也说了,构造函数其实和普通的函数一模一样,只是函数名称首字母被大写了而已。如果在调用构造函数的时候没用使用new操作符,岂不是将所有的属性和方法添加到window上了,这样可不好,为此我们重写Person的构造函数:
当丢失了new操作符的时候,我们强制返回一个Person对象,这样全局作用域就不会被污染了。
构造函数模式的缺点:
所有的属性和方法都不会被共享,p1和p2都是Person的对象,但是他们中的sayName虽然完成的功能相同,但却是两个不同的Function实例,这是非常没有必要的。况且sayName内部有tihs,就更没有必要在执行代码前将函数绑定到特定的对象上面。
3.原型模式
在创建一个函数的时候,JavaScript引擎就会根据一组规则为该函数创建一个prototype属性,该属性是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。现在我们用原型模式来改造构造函数模式:
由上图可发现,这个原型对象其实就是一个特殊的Person对象,特殊到哪里了呢?
- 它是由JavaScript引擎根据一定的规则帮我们创建的;
- 它默认只包含constructor属性(其他属性,方法都是从Object继承而来),而这个constructor指向包含该prototype的函数(这里便是Person函数);
- 强制将该原型对象的[[Prototype]]指向Object函数的原型对象。
现在我们就把要在各个实例中共享的属性和方法添加到该对象中。
它是怎么做到在各个实例中共享的呢?由159行的代码,我们可以看出,每个Person的实例都包含一个指向Person的原型的指针(__proto__),嗯,就是这样做到共享的。
有没有注意到刚刚所说的这个prototype的用途?它只是用来存储包含它的构造函数的所有实例所共有的属性和方法。那它就没必要必须是一个特殊的Person对象了,并且每次为其添加属性和方法总是要写Person.prototype.……很麻烦,因此我们可以这样写:
现在,Person构造函数的原型已经不再是一个Person的特殊实例,而是一个Object。现在有一个问题,这个Object对象中没有constructor属性了。但是还是能访问到这个属性:
onsole.info(p.constructor)//function Object() |
注意这可以不是该Person的原型对象中的constructor,它是Object的原型对象的constructor。
为什么能拿到Object类型的原型对象的constructor属性呢?这是因为在JavaScript获取属性和方法的值的过程其实就是一次搜索的过程,看一个更明显的例子:
console.info(str=p1.toString());//[object Object]
我们并没有在构造函数中定义toString函数,也没有再Person的原型中定义该函数,那是怎么获取的呢?
首先在构造函数中找,接着在原型对象的中找,再在该原型对象所在函数的原型对象中找,以此类推,直到找到为止。
如果这个constructor的值很重要的话,添加上就是了,但是这会导致其[[Enumerable]]特性被设置为true。而原生的该特性的值为false。
最重要的一句话:原型对象是引用类型。
4.动态原型模式
如果你是用过Java、C#或者其他的面向对象的语言,你可能觉得这种将构造函数和原型独立起来的做法很别扭,那么动态原型模式就是解决这一问题的:
这种方法构造对象可以说是非常完美的。注意if语句可以是初始化之后应该存在的任何属性或方法,但是请注意,不必用一大堆的if来检查每个属性和方法,只要检查其中一个即可。它是用来说明在第一次调用构造函数的时候执行if里面的代码,以后就不要再执行这些代码了。
特别注意:使用动态原型模式构造对象时,不能使用对象字面量来重写原型。这是又是为什么呢?
我们来看一下这串代码的执行过程,首先JavaScript引擎会将Person函数放在最开始创建出来,这是它的原型对象便是我们所说的那个特殊的Person对象(正如在161行代码获取到的一样),执行到163行代码,JavaScript得到的指令时去新建一个Person实例,回想一下JavaScript创建对象的过程:1.创建一个新的对象;对了,问题的关键就在这里了,JavaScript引擎创建的这个对象中已经包含了[[Prototype]]这个内部指针。它现在指向这个特殊的Person实例。2.将构造函数的作用域赋值给该对象;3.执行构造函数中的代码,当执行了151-157的代码时,Person.prototype的指向已将变了,现在它指向这个对象字面量了。而目前的p1的[[Prototype]]还是指向原来的原型对象。这就导致了我们从p1中访问不到sayName方法。当执行164行代码时,JavaScript引擎再去新建一个Person实例,这时Person.prototype已经在新建p1时被替换了,它顺理成章的拥有了这个原型对象。当执行到151-157的代码时由于原型对象中有sayName方法,这段代码没有被执行,因此,创建出来的p2才是符合我们愿望的对象。