与其他语言相比,JavaScript 中的“对象”总是显得更加特殊,一些在学习 JavaScript 面向对象时往往会有很多疑惑:
- 为什么 JavaScript(知道 ES6)有对象的概念,但是却没有像其他语言那样有类的概念?
- 为什么在 JavaScript 对象里可以自由添加属性,而其他的语言却不能?
甚至,在一些争论中,有人说“JavaScript 并非面向对象的语言,而是基于对象的语言”;实际上,基于对象和面向对象两个形容词都出现在了 JavaScript 标准的各个版本中。
什么是面向对象?
对象这个词语倒是很容易理解,那么在中文语境下“对象”这个词语在JavaScript中其实是很难理解其真正含义的;事实上,Object(对象) 在英文中是一切事物的总称,这和面向对象编程的抽象思维有互通之处;而中文的“对象”却没有这样的普适性,在我们学习编程的过程中更多的是把它当做一个专业的名词来解释。
那么,我们来看看对象究竟是什么?
对象的概念其实在人的幼年时期形成,它远远早于我们编程逻辑中常用的值、过程等概念;为什么这样说呢,比如:我们一开始总是先认识到某一个苹果能吃(这里的某一个苹果就是一个对象),继而认识到所有的苹果都可以吃(所有苹果就是一个类),再到后来我们才意识到三个苹果和三个梨的区别,进而产生数字“3”(值)的概念。
在《面向对象分析与设计》书中,Grady Booch 做了总结,他认为,从人类的认知角度来说,对象应该是下列食物之一:
- 一个可以触摸或者可以看见的东西
- 人的智力可以理解的东西
- 可以知道思考或行动(进行想象或施加行动)的东西
有了对象的自然定义后,下面就可以描述编程语言中的对象了。在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象,最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java 等流行的编程语言,而 JavaScript 早年选择了一个更为冷门的方式:原型;在 ES6 出现之前,大量的 JavaScript 程序员试图在原型体系的基础上,把 JavaScript 变得更像是基于类的编程,进而产生了很多所谓的“框架”,比如:prototypeJS、dojo;事实上,它们成为了某种 JavaScript 的古怪方言,甚至产生了一系列互不相容的社群,显然这样做的收益远远小于损失的。
如果从运行时的角度来谈论对象,就是在讨论 JavaScript 实际运行中的模型,这是由于任何代码执行都必定绕不开运行时的对象模型。
JavaScript 对象的特征
对象的本质特征(参考 Grandy Booch 《面向对象分析与设计》),总结来看有如下几点:
- 对象具有唯一标识性:既是完全相同的两个对象,也并非同一个对象
- 对象有状态:对象具有状态,同一个对象可能处于不同状态之下
- 对象具有行为:即对象的状态,可能因为它的行为产生变迁
- 唯一标识性
一般而言,语言的对象唯一标识性都是用内存地址来体现的,对象具有唯一的内存地址所以具有唯一的标识;所以,创建的两个看起来一模一样的两个对象实际上是两个不同的对象。
1 | var o1 = {a:1} |
- 对象的状态和行为
不同语言用不同的术语来描述它们,比如在 C++ 中它们为“成员变量”和“成员函数”,Java 中则称为“属性”和“方法”。
在 JavaScript 中,将状态和行为统一抽象为“属性”,下面的示例就是普通属性和一个函数作为属性的例子:
1 | var o = { |
所以,在 JavaScript 中,对象的状态和行为其实都被抽象成了属性。在实现了对象基本特征的基础上,JavaScript 中对象独有的特色是:对象具有高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态的行为和能力;例如,JavaScript 允许在运行时像对象添加属性,而其他语言不行,例如:
1 | var o = {a:1}; |
为了提高抽象能力, JavaScript 的属性被设计成比别的语言更复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。
JavaScript 对象的两类属性
对 JavaScript 来说,属性并非只是简单的名称和值,JavaScript 用一组特征(attribute)来描述属性(property)。
- 第一类属性 —— 数据属性
具有以下几个特征:
- value: 属性的值
- writable: 决定属性能否被赋值
- enumerable: 决定for in能否枚举该属性
- configurable: 决定该属性能否被删除或者改变特征值
在大多数情况下,只需要关注数据的值即可。
- 第二类属性 —— 访问器(getter/setter)属性
具有以下几个特征:
- getter: 函数或 undefined,在取属性值时被调用
- setter: 函数或 undefined,在设置属性值时被调用
- enumerable: 决定 for in 能否枚举该属性
- configurable: 决定该属性能否被删除或者改变特征值
通常定义的属性会产生数据属性,其中的 writable、enumerable、configurable 都默认为 true;可以使用内置函数 Object.getOwnPropertyDescripter() 来查看;如下:
1 | var o = {a:1} |
如果想改变属性的特征,或者定义访问器属性,可以使用 Object.defineProperty();如下:
1 | var o = { a: 1} |
在改变对象的默认属性后,对其的操作跟值相关。
在创建对象时,也可以使用 get 和 set 关键字来创建访问器属性;如下:
1 | var o = { |
访问器属性跟数据属性不同,每次访问属性都会执行 getter 和 setter 函数。这里的 getter 函数返回了1,所以 o.a 每次都得到1。
这样,我们就理解了,实际上 JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者 Symbol 为 key,以数据属性特征值或者访问器属性特征值为 value。而在 JavaScript 中,能够以 Symbol 为属性名,到这里,基本上就可以理解 “JavaScript 不是面向对象”这样的说法了,这是因为 JavaScript 的对象设计跟目前主流基于类的面向对象差异很大;可事实上,这样的对象系统虽然特别,但是 JavaScript 提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式,所以它也是正统的面向对象语言。所以,我们应该在理解其设计思想的基础上充分挖掘它的能力,而不是机械的模仿其他语言。