学习JavaScript对象,当然会看看JavaScript标准对基于对象的定义,这个定义的内容是:“语言和宿主的基础设施由对象来提供,并且JavaScript程序即是一系列相互通讯的对象集合”。这听起来多么晦涩,至少我很难从中理解到任何JS对象的关键。
什么是面向对象
首先,什么是对象呢?由于翻译的原因,中文语境中很难理解“对象”的真正含义。实际上,Object(对象)在英文中,是一切事物的总称,这和面向对象编程的抽象思维有互通之处。而中文里的“对象”却没有这样的普适性,以往的时间里更多的是把它当做一个专业名词来理解。其实,我们需要理解一点,对象不是计算机领域凭空创造出来的概念,它是顺着人类思维模式产生的一种抽象。
人类思维模式:
在幼年期,我们总是先认识到某一个苹果能吃(这里的某一个苹果就是一个对象),继而认识到所有的苹果都可以吃(这里的所有苹果,就是一个类),再到后来我们才能意识到三个苹果和三个梨之间的联系,进而产生数字“3”(值)的概念。
有了对象的自然定义后,就可以描述编程语言中的对象了。在不同编程语言中,它们的设计者利用不同的语言特性来抽象描述对象,C++,Java等流行语言都是使用“类”的方式来描述对象,但JavaScript早年选择了一个更了冷门的方式:原型。JS设计时在“原型运行时”的基础引入new、this等语言特征,使之更像Java。
如果我们从运行时角度来谈论对象,就是在讨论 JavaScript 实际运行中的模型,这是由于任何代码执行都必定绕不开运行时的对象模型。
JavaScript 对象的特征
参考《面向对象分析与设计》,对象有如下几个特点:
- 对象具有唯一标识性:即是完全相同的两个对象,也并非用一个对象。
- 对象有状态:对象具有状态,同一对象可能处在不同状态之下。
- 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
我们先来看第一个特征,对象具有唯一标识性。一般而言,各种语言的对象唯一标识性都是用内存地址来体现的, 对象具有唯一标识的内存地址,所以具有唯一的标识。
var o1 = { a: 1 };
var o2 = { a: 1 };
console.log(o1 == o2); // false
o1 和 o2 初看是两个一模一样的对象,但是打印出来的结果是false。
关于对象的第二个和第三个特征“状态和行为”,不同语言会使用不同的术语来抽象描述它们,比如 C++ 中称它们为“成员变量”和“成员函数”,Java 中则称它们为“属性”和“方法”。而在JS中,将状态和行为统一抽象为“属性”。
下面这段代码其实就展示了普通属性和函数作为属性的一个例子,其中 o 是对象,d 是一个属性,而函数 f 也是一个属性,尽管写法不太相同,但是对 JavaScript 来说,d 和 f 就是两个普通属性。
var o = {
d: 1,
f() {
console.log(this.d);
}
};
在实现了对象基本特征的基础上, JavaScript 中对象独有的特色是:对象具有高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力。
譬如,JavaScript 允许运行时向对象添加属性,这就跟绝大多数基于类的、静态的对象设计完全不同。下面这段代码就展示了运行时如何向一个对象添加属性,一开始我定义了一个对象 o,定义完成之后,再添加它的属性 b,这样操作是完全没问题的。
var o = { a: 1 };
o.b = 2;
console.log(o.a, o.b); //1 2
为了提高抽象能力,JavaScript 的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。对 JavaScript 来说,属性并非只是简单的名称和值,JavaScript 用一组特征(attribute)来描述属性(property)。
数据属性
数据属性具有四个特征:
- value:属性值
- writable:决定属性能否被赋值
- enumerable:决定for in 能否枚举该属性
- configurable:决定该属性能否被删除或改变特征值
访问器(getter/setter)属性
它也有四个特征:
- getter:函数或undefined,在取属性值时被调用
- setter:函数或undefined,在设置属性值时被调用
- enumerable:决定for in 能否枚举该属性
- configurable:决定该属性能否被删除或改变特征值
访问器属性使得属性在读和写时执行代码,它允许使用者在写和读属性时,得到完全不同的值,它可以视为一种函数的语法糖。
语法糖(Syntactic sugar)
它意指那些没有给计算机语言添加新功能,而只是对人类来说更“甜蜜”的语法。语法糖往往给程序员提供了更实用的编码方式,有益于更好的编码风格,更易读。不过其并没有给语言添加什么新东西。语法糖是一种便捷的写法,编译器会帮我们做转换;而且可以提高开发编码的效率,在性能上也不会带来损失。
用于定义属性的代码会产生数据属性,其中writable、enumerable、configurable都默认为true,可以使用内置函数Object.getOwnPropertyDescriptor来查看,如下代码:
var 0 = {a : 1};
o.b = 3;
Object.getOwnPropertyDescriptor(o, "a"); //{value: 1, writable: true, enumerable: true, configurable: true}
Object.getOwnPropertyDescriptor(o, "b"); //{value: 3, writable: true, enumerable: true, configurable: true}
如果想要改变属性的特征,或者定义访问器属性,我们可以使用Object.defineProperty,如下代码:
var o = {a : 1};
Object.defineProperty(o, "b", {value: 1, writable: false, enumerable: false, configurable: false})
Object.getOwnPropertyDescriptor(o, "b"); //{value: 1, writable: false, enumerable: false, configurable: false}
o.b = 5;
console.log(o.b); //1
创建对象是,也可以使用get 和 set 关键字来创建访问器属性,代码如下:
var o = {
get a(){
return 1;
}
};
console.log( o.a ); //1
访问器属性跟数据属性不同,每次访问属性都会执行 getter 或者 setter 函数。这里我们的 getter 函数返回了 1,所以 o.a 每次都得到 1。所以呢,实际上 JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者 Symbol 为 key,以数据属性特征值或者访问器属性特征值为 value。