JavaScript为什么使用原型模式而不是类模式
导言: 作为JavaScript初学者的本菜鸡而言,刚一开始接触这门语言我就被他的原型模式给吓到了。并且在相当长的一段时间之内,我都完全不能理解或者不能接受这个模式。直到最近经过多方调查和思考才有所明悟。本篇文章就来记录一下我对JavaScript为什么使用原型模式而不是用类模式这个问题的一点看法。
为什么是原型模式面向对象
是啊,为什么是原型模式呢?类模式那么的优雅,那么容易理解,当今世界扛把子级别的语言无论是c++,Java还是Python等,全部都是基于类的面向对象。基于类的面向对象就类似于铸模和铸件的关系。我们精心维护好这样的铸模,然后生成一堆的铸件,这些铸件就可以愉快的运行在各行各业,起到相应的功能。当铸件出了一些问题之后没有关系,我重新在用铸模搞出一个铸件就可以了。也有可能铸模跟不上时代的发展了,也没有关系,我重新调整一下铸模的结构,这样以后就能生产出紧跟行业最新需求的铸件了。基于类的面向对象是多么的好啊,有了它我们就可以建设美丽新世界了!
甚至我一度以为,面向对象就是指的基于类的面向对象。直到我遇到了邪恶的JavaScript,一下子让我三观尽碎!
不过我还是存有一点侥幸心理,或许可能是那个JavaScript设计者设计这门语言时间过于仓促,没有考虑到基于类的面向对象这种设计是多么优雅,直到我后来读到下面这段内容:
在Brendan Eich为JavaScript设计面向对象系统时,借鉴了Self和Smalltalk这两门基于原型的语言。之所以选择基于原型的面向对象,不是因为时间匆忙,它设计起来相对简单,而是因为从一开始Brendan Eich就没有打算在JavaScript中加入类的概念。
苍天啊!JavaScript居然没有类的概念。到了ES6出了class关键字之后,也不过是基于原型的一种语法糖。可是你都没有类了,为什么还要搞new关键字,简直是脱裤子放屁。后来我又读到一段话:
然而很不幸,因为一些公司政治原因,JavaScript 推出之时受管理层之命被要求模仿 Java,所以,JavaScript 创始人 Brendan Eich 在“原型运行时”的基础上引入了 new、 this 等语言特性,使之“看起来更像 Java”。
好吧!不管怎么说,就是基于原型了。但是为什么要选原型呢?下面谈谈我的理解。
我想设计模式的差异可能是因为前端和后端面临的情况不太一样。对于在服务端运行的程序而言,更应该追求稳定性。而在客户端运行的程序,经常要面临和用户交互、渲染等任务,动态性的成分更多一些。因此服务端的程序使用基于类的面向对象,走铸模、铸件路线。我们做修改基本只改模具,不要铸件发挥自己的主观能动性。比如你在写Python或者Java程序的时候,你生成实例化对象之后,很少会要求对象变来变去吧。
而JavaScript希望对象尽可能发挥自己的主观能动性,干脆直接把模具扔了。玩起了原型模式和原型继承。没有模具那该如何搞对象呢?这个我们后面会讲到,首先来看一下JavaScript对象的主观能动性吧。
let o = {
a: 1
}
o.b = 2
console.log(o)
因为JavaScript没有类的束缚,所以对象可以尽情的添加属性(在Java语言中,我们将类中的函数称为方法,但是JavaScript只有属性没有方法这种叫法,函数也是一种属性),对于上述代码,写成添加函数这样的形式也没有问题:
o.b = function() {
console.log('haha')
}
你可以看看o的输出结果。如果是用class约束一下呢?或者class真的约束的了吗?
class Animal {
constructor(name) {
this.name = name
}
getName() {
return this.name
}
}
let dog = new Animal('dog')
console.log(dog.getName())
dog.bark = function() {
console.log('wangwang')
}
console.log(dog.bark())
可以看一下输出结果。这是Javascript基于原型的灵活性。此外,可能是为了进一步增加灵活性吧,JavaScript还增加了访问器属性。
let o = {
get a() {
return 1
}
}
console.log(o.a)
访问器属性跟数据属性不同,每次访问属性都会执行 getter 或者 setter 函数。这样我们每次访问a属性时都会有对应的输出。(有没有一种proxy的感觉?)
原型模式特点
正是基于原型的灵活性,所以JavaScript选择了原型模式而没有使用类对象模式。接下来我们来看一下原型模式的特点。
前面我们提到,基于类的面向对象可以比作为铸模和铸件的关系,我们更关注与模具而不是铸件。我想了半天,没有想到一个十分形象和恰当的比喻,有一个不太恰当的。可以将原型这种模式比作吸血鬼发展新的吸血鬼。为什么这样比喻呢?上面我们举例子也提到了,原型模式希望我们更加关注对象,而不是模具。那么每一个吸血鬼都有自己的充分主观能动性。比如电影《暮光之城》中的吸血鬼,还可以轰轰烈烈的谈恋爱,撒狗粮。但是对于工厂生产的铸件而言,比较类似于僵尸,没有办法很好发挥对象的主观能动性。(比如类似于僵尸没有理智,而吸血鬼有比较强的理智)
如何创建一个新的对象呢?答案是通过对象克隆的方式。而不是通过类实例化对象的方式。(比如类似于每一个有充分主观能动性的吸血鬼,咬人之后就会发展出一个新的吸血鬼),需要注意的是,我们不要被new关键字所迷惑了,以为是通过类实例化对象的方式来创建新的对象。比如下面代码:
let obj1 = new Object()
// 或者是写成这样
let obj2 = {}
引擎内部都会从Object.prototype上面克隆出来一个对象,我们最终得到的就是这样克隆出来的一个对象。还是举吸血鬼的例子,就是让Object这个吸血鬼,咬了一个人,然后这个人就变成了有Object属性的吸血鬼了。这个新的吸血鬼可能也会去谈恋爱,总之是具有很高的主观能动性的。
关于new关键字我再展开说一下,当一个对象被new关键字修饰的时候,会首先调用这个对象的构造函数,也就是constructor。我们要是普通对象,是不是就不能克隆出对象呢?答案不是这样,还可以使用Object.create(),每一个吸血鬼都可以发挥自身特点,克隆出一个新的吸血鬼。
let cat = {
say() {
console.log('miao-miao')
}
}
let anotherCat = Object.create(cat)
需要注意的是,你不能写成这样
let anotherCat = cat
这句话的意思是把anotherCat指向cat的内存地址。而不是复制。
参考资料
[1] 《JavaScript设计模式与开发实战》
