javascript面向对象(一)

javascript面向对象(一)

javascript对象

面向对象是编程界老生常谈的问题,也总有人争论javascript到底是不是一门面向对象的语言。在我看来,面向对象定义在语言上不如定义在编程思维上。一段javascript的逻辑是不是面向对象的,取决于写这段代码的方式。

那么怎样才能写出面向对象的javascript代码呢?首先需要知道什么是javascript中的对象。
与其他语言相比,javascript中的"对象"有些另类。

一.对象的特征

  1. 对象具有唯一标识性(内存地址)
  2. 对象具有状态 (值属性,对象属性)状态保存
  3. 对象具有行为 (函数,方法) 消息发送 行为是改变自生状态的行为

我们不应该受到语言描述的干扰,在设计对象的状态和行为时,我们总是遵循"行为改变状态"的原则。
也就是说对象的行为基本都要改变自身。比如,狗咬人,应该是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()的行为就完全不一样。

三.对象的本质

如何抽象"一堆"的数据,使得它们能被方便和有效的管理。
“最高的抽象"层级在一个"有限的存储空间"里面,其实只能表达为一个"块”。块是对有限空间的边界分解,那么对应的也就有了"块"的概念。
在一个有限的空间中,如何找到一个"块"
从"块"的相关位置触发,以位置关系来看,就只有两个解:

  1. 为所有 连续 的块添加一个 连续 的"索引"
  2. 为所有 不连续 的块添加一个唯一的"名字"

这里的重点在于连续和不连续。

  • 索引对应了连续性本身,表达为可计算的特性"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

delete快照
delete快照

参考文献

V8 是怎么跑起来的 —— V8 中的对象表示 – ThornWu
快属性和慢属性:V8采用了哪些策略提升了对象属性的访问速度?-- 极客时间 图解googleV8专栏
JavaScript对象:面向对象还是基于对象?-- 极客时间 重学前端专栏
[a, b] = {a, b}:让你从一行代码看到对象的本质 – 极客时间 javascript核心原理解析

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值