javascript面向对象(一)
javascript对象
面向对象是编程界老生常谈的问题,也总有人争论javascript到底是不是一门面向对象的语言。在我看来,面向对象定义在语言上不如定义在编程思维上。一段javascript的逻辑是不是面向对象的,取决于写这段代码的方式。
那么怎样才能写出面向对象的javascript代码呢?首先需要知道什么是javascript中的对象。
与其他语言相比,javascript中的"对象"有些另类。
一.对象的特征
- 对象具有唯一标识性(内存地址)
- 对象具有状态 (值属性,对象属性)状态保存
- 对象具有行为 (函数,方法) 消息发送 行为是改变自生状态的行为
我们不应该受到语言描述的干扰,在设计对象的状态和行为时,我们总是遵循"行为改变状态"的原则。
也就是说对象的行为基本都要改变自身。比如,狗咬人,应该是hurt方法在人身上。因为bite并不改变狗的状态。
let o = {
d:1, // 属性(状态)
f(){
this.d = 2
} // 属性(行为)
}
二.javascript对象独有的特色
- javascript对象独有的特色是:对象具有高度的动态性,这是因为javascript赋予了使用者在运行时为对象增改状态和行为的能力。
- 为了提高抽象能力,javascript的属性被设计成比别的语言更加复杂的形式.他提供了数据属性和访问器属性等。
- 在Javascript运行时,原生对象的描述方式非常简单,只需要原型和属性两个部分。
javascript对象并非只有简单的名称和值,javascript用一组特征(attribute)来描述属性(property)。
数据属性(Data Property)
- value: 属性的值
- writable: 决定属性能否被赋值
- enumerable: 是否可遍历
- configurable: 决定该属性能否被删除或者改变特征值
- 数据属性writable、enumerable、configurable都默认为true。
我们可以使用内置函数Object.getOwnPropertyDescripter(obj,'xxx')
来查看。如果想要改变属性的特征或者定义访问器属性,我们可以使用Object.defineProperty(obj,'xxx',{})。
如果同时定义get(){}和value会报错。
访问器属性(Accessor Property)
- getter: 函数或者undefined,在取属性值时被调用
- setter:函数或undefined,在设置属性值时被调用
- enumerable:决定for in 能否枚举该属性
- configurable:决定该属性能否被删除或者改变特征值,configurable也管理自己,所以如果将configurable设置为false后,就无法改变为true了。
访问器属性使得属性在读和写时执行代码,它允许使用者在读和写属性时,得到完全不同的值,可以视为一种函数的语法糖。
在创建对象时,可以使用get和set关键字来创建访问器属性。
let o = { get a(){return 1} }
Object API
Object.defineProperty
Object.create/Object.setPrototypeOf/Object.getPrototypeOf
new/class/extends
new/function/prototype
Function Object
除了一般对象的属性和原型,函数对象上还有一个行为[[call]]
,使用f()时,就会访问对象的[[call]]
行为,如果不存在则会报错。new的时候会调用[[constructor]]
。比如 Date()与new Date()的行为就完全不一样。
三.对象的本质
如何抽象"一堆"的数据,使得它们能被方便和有效的管理。
“最高的抽象"层级在一个"有限的存储空间"里面,其实只能表达为一个"块”。块是对有限空间的边界分解,那么对应的也就有了"块"的概念。
在一个有限的空间中,如何找到一个"块"
从"块"的相关位置触发,以位置关系来看,就只有两个解:
- 为所有 连续 的块添加一个 连续 的"索引"
- 为所有 不连续 的块添加一个唯一的"名字"
这里的重点在于连续和不连续。
- 索引对应了连续性本身,表达为可计算的特性"a[i]",代表了a的下标_i_。
- 而名字对应于找到块这一目的本身,可以理解为find(),这个函数找到他需要计算的数据,name数据也就可以等价为b[find()]。
那么如果将i也理解为找到块,那么索引也可以当做名字,a[i],也可以理解成a[f()]
function f(){
return i
}
那么连续的块和不连续的块就都是通过一个函数来找到块了。
索引数组的这个函数就是取成员的索引。关联数组的这个函数就是用来取数组成员的名字。关联数组就是用一对”key/value“来创建数组。
所以在如何管理数据上,所有的数据只具有两种数据结构的构成,一种是索引数组,一种是关联数组。而索引数组是关联数组的一个特例,索引值就是他的名字。
- 数组(Array class)是一种对象(Object class)。
- 对象本质上的关联数组(Associative array)。
四.V8中的对象
在V8实现对象存储时,并没有完全采用字典的存储方式,这是出于性能的考量,因为字典是非线性的数据结构,V8为了提高存储和查找的效率,采用了一套复杂的存储策略。
常规属性和排序属性
对象中的数字属性称为排序属性,在V8中被称为elements,字符串属性就被称为常规属性,在V8中被称为properties。V8内部为了有效地提升存储和访问这两种属性的性能,分别使用了两种线性的数据结构来保存排序属性和常规属性。
快属性和慢属性
将不同的属性分别保存到 elements 属性和 properties 属性中,无疑简化了程序的复杂度,但是在查找元素时,却多了一步操作,比如执行 bar.B这个语句来查找 B 的属性值,那么在 V8 会先查找出 properties 属性所指向的对象 properties,然后再在 properties 对象中查找 B 属性,这种方式在查找过程中增加了一步操作,因此会影响到元素的查找效率。
对象内属性
所以V8添加了对象内属性,可以直接访问对象获取属性值,只不过对象内属性只有10个坑位,如果常规属性值超过了10个,那么多出来的属性就会以线性存储的方式存在properties中。
function Foo(property_num,element_num) {
//添加可索引属性
for (let i = 0; i < element_num; i++) {
this[i] = `element${i}`
}
//添加常规属性
for (let i = 0; i < property_num; i++) {
let ppt = `property${i}`
this[ppt] = ppt+'-value'
}
}
var bar = new Foo(10,10)
执行这段代码后,在chrome的memory面板中看到的内存快照为
可以看出10个常规属性和10个排序属性的时候,只创建了线性的elements,并没有创建properties。此时的常规属性都属于对象内属性。
快属性
var bar = new Foo(20,10)
我们将保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销。
将常规属性添加为20个的时候,这个时候再来看快照。
我们可以看到后加入的10个常规属性以加入了properties以线性的结构存储。
慢属性
如果一个对象的属性过多时,V8 为就会采取另外一种存储策略,那就是“慢属性”策略,但慢属性的对象内部会有独立的非线性数据结构 (字典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。
var bar = new Foo(30,10)
那么将常规属性改为30个,再来看看此时的快照。
可以看出当常规属性为30个时候,properties是非线性的数据结构了,如图中所示,101存储key,102存储value。
var bar = new Foo(10,10)
bar[1000] = 'test'
当执行下面代码的时候,查看快照,会发现,elements也不是线性结构了,因为bar[1000]的加入使数组成为了稀疏数组,为了节省空间,稀疏数组会转换为哈希存储的方式,而不再是用一个完整的数组描述这块空间的存储。
隐藏类
隐藏类的引入,将属性的 Value 与其它 Attribute 分开。一般情况下,对象的 Value 是经常会发生变动的,而 Attribute 是几乎不怎么会变的。
对象创建过程中,每添加一个命名属性,都会对应一个生成一个新的隐藏类。在 V8 的底层实现了一个将隐藏类连接起来的转换树,如果以相同的顺序添加相同的属性,转换树会保证最后得到相同的隐藏类。
要生成相同的隐藏类则需要 —— 从相同的起点,以相同的顺序,添加结构相同的属性(除 Value 外,属性的 Attribute 一致)。
delete操作
如果用delete操作到了并非是最后一个添加到对象中的属性,该属性又正好在快属性或者对象内属性中,那么快属性列表会变成慢属性列表,因为数据的索引发生了改变,无法再使用线性结构了。所以delete操作符很有可能会导致对象性能的变差。
function Foo () {}
var a = new Foo()
var b = new Foo()
for (var i = 1; i < 13; i ++) {
a[new Array(i+1).join('a')] = 'aaa'
b[new Array(i+1).join('b')] = 'bbb'
}
delete a.a
function Foo () {}
var a = new Foo()
var b = new Foo()
for (var i = 1; i < 13; i ++) {
a[new Array(i+1).join('a')] = 'aaa'
b[new Array(i+1).join('b')] = 'bbb'
}
delete a.aaaaaaaaaaa
参考文献
V8 是怎么跑起来的 —— V8 中的对象表示 – ThornWu
快属性和慢属性:V8采用了哪些策略提升了对象属性的访问速度?-- 极客时间 图解googleV8专栏
JavaScript对象:面向对象还是基于对象?-- 极客时间 重学前端专栏
[a, b] = {a, b}:让你从一行代码看到对象的本质 – 极客时间 javascript核心原理解析