javascript对象,面向对象还是基于对象

与其他语言相比,JavaScript 中的“对象”总是显得更加特殊,一些在学习 JavaScript 面向对象时往往会有很多疑惑:

  • 为什么 JavaScript(知道 ES6)有对象的概念,但是却没有像其他语言那样有类的概念?
  • 为什么在 JavaScript 对象里可以自由添加属性,而其他的语言却不能?

甚至,在一些争论中,有人说“JavaScript 并非面向对象的语言,而是基于对象的语言”;实际上,基于对象和面向对象两个形容词都出现在了 JavaScript 标准的各个版本中。

什么是面向对象?

对象这个词语倒是很容易理解,那么在中文语境下“对象”这个词语在JavaScript中其实是很难理解其真正含义的;事实上,Object(对象) 在英文中是一切事物的总称,这和面向对象编程的抽象思维有互通之处;而中文的“对象”却没有这样的普适性,在我们学习编程的过程中更多的是把它当做一个专业的名词来解释。

那么,我们来看看对象究竟是什么?

对象的概念其实在人的幼年时期形成,它远远早于我们编程逻辑中常用的值、过程等概念;为什么这样说呢,比如:我们一开始总是先认识到某一个苹果能吃(这里的某一个苹果就是一个对象),继而认识到所有的苹果都可以吃(所有苹果就是一个类),再到后来我们才意识到三个苹果和三个梨的区别,进而产生数字“3”(值)的概念。

在《面向对象分析与设计》书中,Grady Booch 做了总结,他认为,从人类的认知角度来说,对象应该是下列食物之一:

  1. 一个可以触摸或者可以看见的东西
  2. 人的智力可以理解的东西
  3. 可以知道思考或行动(进行想象或施加行动)的东西

有了对象的自然定义后,下面就可以描述编程语言中的对象了。在不同的编程语言中,设计者也利用各种不同的语言特性来抽象描述对象,最为成功的流派是使用“类”的方式来描述对象,这诞生了诸如 C++、Java 等流行的编程语言,而 JavaScript 早年选择了一个更为冷门的方式:原型;在 ES6 出现之前,大量的 JavaScript 程序员试图在原型体系的基础上,把 JavaScript 变得更像是基于类的编程,进而产生了很多所谓的“框架”,比如:prototypeJS、dojo;事实上,它们成为了某种 JavaScript 的古怪方言,甚至产生了一系列互不相容的社群,显然这样做的收益远远小于损失的。

如果从运行时的角度来谈论对象,就是在讨论 JavaScript 实际运行中的模型,这是由于任何代码执行都必定绕不开运行时的对象模型。

JavaScript 对象的特征

对象的本质特征(参考 Grandy Booch 《面向对象分析与设计》),总结来看有如下几点:

  • 对象具有唯一标识性:既是完全相同的两个对象,也并非同一个对象
  • 对象有状态:对象具有状态,同一个对象可能处于不同状态之下
  • 对象具有行为:即对象的状态,可能因为它的行为产生变迁
  1. 唯一标识性
    一般而言,语言的对象唯一标识性都是用内存地址来体现的,对象具有唯一的内存地址所以具有唯一的标识;所以,创建的两个看起来一模一样的两个对象实际上是两个不同的对象。
1
2
3
var o1 = {a:1}
var o2 = {a:1}
console.log(o1 == o2); // false
  1. 对象的状态和行为
    不同语言用不同的术语来描述它们,比如在 C++ 中它们为“成员变量”和“成员函数”,Java 中则称为“属性”和“方法”。
    在 JavaScript 中,将状态和行为统一抽象为“属性”,下面的示例就是普通属性和一个函数作为属性的例子:
1
2
3
4
5
6
var o = {
d: 1,
f() {
console.log(this.d);
}
}

所以,在 JavaScript 中,对象的状态和行为其实都被抽象成了属性。在实现了对象基本特征的基础上,JavaScript 中对象独有的特色是:对象具有高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态的行为和能力;例如,JavaScript 允许在运行时像对象添加属性,而其他语言不行,例如:

1
2
3
var o = {a:1};
o.b = 2;
console.log(o.a, o.b); // 1 2

为了提高抽象能力, JavaScript 的属性被设计成比别的语言更复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。

JavaScript 对象的两类属性

对 JavaScript 来说,属性并非只是简单的名称和值,JavaScript 用一组特征(attribute)来描述属性(property)。

  1. 第一类属性 —— 数据属性
    具有以下几个特征:
  • value: 属性的值
  • writable: 决定属性能否被赋值
  • enumerable: 决定for in能否枚举该属性
  • configurable: 决定该属性能否被删除或者改变特征值

在大多数情况下,只需要关注数据的值即可。

  1. 第二类属性 —— 访问器(getter/setter)属性
    具有以下几个特征:
  • getter: 函数或 undefined,在取属性值时被调用
  • setter: 函数或 undefined,在设置属性值时被调用
  • enumerable: 决定 for in 能否枚举该属性
  • configurable: 决定该属性能否被删除或者改变特征值

通常定义的属性会产生数据属性,其中的 writable、enumerable、configurable 都默认为 true;可以使用内置函数 Object.getOwnPropertyDescripter() 来查看;如下:

1
2
3
var o = {a:1}
Object.getOwnPropertyDescripter(o, 'a');
// {value: 1, writable: true, enumerable: true, configurable: true}

如果想改变属性的特征,或者定义访问器属性,可以使用 Object.defineProperty();如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var o = { a: 1}
Object.defineProperty(o, 'b', {
value: 2,
writable: false,
enumerable: false,
configurable: true
});

Object.getOwnPropertyDescriptor(o, 'b');
// {value: 2, writable: false, enumerable: false, configurable: true}

// 不能再次改变 b 的值
o.b = 3;
console.log(o.b);
// 2

在改变对象的默认属性后,对其的操作跟值相关。

在创建对象时,也可以使用 get 和 set 关键字来创建访问器属性;如下:

1
2
3
4
5
6
7
8
var o = {
get a() {
return 1;
}
}

console.log(o.a);
// 1

访问器属性跟数据属性不同,每次访问属性都会执行 getter 和 setter 函数。这里的 getter 函数返回了1,所以 o.a 每次都得到1。
这样,我们就理解了,实际上 JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者 Symbol 为 key,以数据属性特征值或者访问器属性特征值为 value。而在 JavaScript 中,能够以 Symbol 为属性名,到这里,基本上就可以理解 “JavaScript 不是面向对象”这样的说法了,这是因为 JavaScript 的对象设计跟目前主流基于类的面向对象差异很大;可事实上,这样的对象系统虽然特别,但是 JavaScript 提供了完全运行时的对象系统,这使得它可以模仿多数面向对象编程范式,所以它也是正统的面向对象语言。所以,我们应该在理解其设计思想的基础上充分挖掘它的能力,而不是机械的模仿其他语言。