JavaScript与OOP

本文研究下JavaScript中的OOP。
首先要明确的是,JS不算强类型语言,所以OOP的实现方式必然与C++或Java不同。后者是基于类的OOP,而JS是基于原型的OOP。
OOP有三大主枝:数据抽象、继承、多态,让我们分别看一下。

数据抽象

JS中的对象天然就是一个实例。当key的val是数据时,就是成员变量;当key的val是函数对象时,就是成员方法。
JS中没有类,但是存在一个构造函数的概念,可以用如下方式产生一个实例

说是构造函数,但也仅仅是一个普通的函数而已,构造更多是语义层面上的
构造函数只负责初始化对象(添加变量和方法),不返回任何值。其作用的对象就在this上

调用示例

构造函数配合new使用,new会生成一个空对象,然后以this的形式把这个空对象传给构造函数。
当构造函数运行完后,加工过的对象给到等号左边的变量去赋值。
于是整体结果就好像是实例化了Animal类

但是这种实现方式有个弊端,那就是每个对象的方法都是函数对象,占用内存
可以使用prototype,让所有实例都使用同一个函数对象

当实例调用某个方法后,脚本环境会查找实例本身是否有该函数对象,没有的情况会上溯到类别的prototype,如果发现就调用那个。如果还没有就继续上溯

还存在一个封装的问题。JS里没有public/private的概念,所以要实现封装需要用到closure,会让问题复杂化(可参考Javascript: the good part一书)。
新标准推出了Object.freeze(obj),可以让外界不能增加、删除、更改obj的属性,可以作为封装使用。
在这里就使用命名规范好了,名字以_开始的都表示这是私有变量、方法,外界不应该动

继承

JS里并没有类,也就没有extend这种操作,但是依然可以使用prototype模拟出继承。

有几个套路:
首先定义派生类构造函数,该函数中使用call/apply来调用基类构造函数(初始化基类成员),然后是初始化派生类额外的成员
其次实例化一个基类对象并赋值给派生类的prototype。因为上溯会不断通过prototype寻找,所以这个操作实际造成的效果是派生类获得了基类的方法(更严格地说是基类定义的函数对象)
最后派生类可以在prototype里添加额外的方法或者override。因为都是函数对象所以定义了新的自然就把旧的顶替了。

运行结果:Tom true true meow

多态

JS里没有多态,也不需要多态。
多态是存在于强类型语言里的,调用基类指针的方法会变为派生类方法。
JS里派生类的方法都是函数对象,所以构造出了实例自然就是派生类的方法了。实际上JS里没法调用基类中已被override的方法。
函数传参也是没有类型的,把派生类实例传入然后调用对应方法,就是执行派生类里的函数对象。
从行为方式来看,跟多态没有区别

Prototype

JS里的核心概念是prototype,那么怎么来理解这个东西?
prototype是一个具体的对象,提供了类别默认的属性和行为方式。

也许跟传统OOP语言做个对比更容易理解。
传统语言里,类是一个概念,派生出的类依然是概念,概念之间有继承关系。将类实例化出具体的对象,然后加以使用。这就好比规范出笔和签字笔的概念,然后实例化签字笔类得到一个具体的签字笔,然后写字。
JavaScript里没有类,也就没有概念。原型本身就是一个具体的对象,可以拿来使用(但一般不会这样做,而是通过原型得到一个新副本再使用)。派生类仅仅是记录下与原型不一致的地方,然后指定原型在哪里。这就好比拿出一个具体的签字笔,然后宣布这个就是签字笔的原型,其他签字笔的行为方式都参考这个。新派生一个笔帽不同的签字笔,则这个新的签字笔对象就只有一个笔帽,如果要写字就去找到原型里写字的函数对象然后拿过来调用。