3

故事缘由

对象这个词,在我们🐒界,再熟悉不过了,虽然不会天天挂在口中,但是却如影随形般在我们身旁,上班要“面向对象”,下班还要忙着找“对象👩”,逢年过节回家,长辈的亲朋好友问,“你的职业是什么?”,“程序🐵”,“噢~应该还没对象吧”😓,看来“程序🐵”注定离不开“对象”了。有一天,一个问题环绕在脑海中。如果别人让我解释什么是对象?我该如何说好。对象,感觉好抽象,虽然每次撸代码都会用到,但对象的具体概念还真心没去研究过。因此写下这篇博文,深究下对象的来龙去脉,一👀看穿JS对象。

理解对象之前,我们先回顾下JS的数据类型

说到数据类型,不得不吐槽一下,觉得这玩意就是一个坑,编译型语言还好,可以在编译过程中发现类型错误,可是当遇上JS这种解释型语言,只能默默哭泣😭了,天坑的,JS还有隐式转换😱,想当初自己多次跪倒在这里,想着如果有个万能类型多好,就像电脑一样只用识别0和1,不对,从这角度去想,感觉有数据类型还是蛮幸福的,起码便于程序的阅读和开发,不然,只有0和1的世界多无趣呀。

言归正传,这里回顾下JS的6种数据类型,其中5个是基本数据类型(Boolean,String,Number,Undefined,Null),还有一种复杂的数据类型(Object)。有的人会问了,我们是来研究对象的,和数据类型啥关系呀!没关系?哪里没关系了,关系可大着呢,请继续往👇看。

数据类型和对象的关系

JS中5个基本数据类型,他们和对象是没半毛钱关系的,为啥?文字告诉我们啦,他们叫基本数据类型,既然都是基本了,那么就没有对象化的能力啦,只有那个复杂的数据类型(Object)才能和对象扯上关系。我们知道,Object是方法与属性的集合,所以对象只是对复杂数据类型而言的。那么什么是对象呢?请继续往👇看。

什么是对象❓

根据ECMA的定义,无序属性的集合,其属性可以包含基本值、对象或者函数,从这句话中,我们可以提炼出
对象的三个特点
①无序
②键值对
③值可以是基本值(数据类型为基本数据类型的值),对象或者函数。
所以当别人问你什么是对象时,你可以这样说,对象是针对复杂数据类型而言的,它具有无序性,是一个名值对,其值可以是数据或函数,这就是对象。
~~完啦,对象是不是超级简单?这样就结束了吗?没有,光知道定义没啥用,还要了解他怎么用,这样才能一👀看穿JS对象嘛~

对象应该怎么用❓

对象的唯一用法就是创建它,不创建没意义,就好比你写了一个函数,不去调用也是没意义的。不过具体怎么创建不是我们这一章的主题,我们继续深入研究对象里面都有些什么?怎么控制它的名值对的?让我们回到对象的定义,在定义中提到 “属性...属性包含...”,看来第一个问题很简单,对象的内部就是属性,接下来,我们继续刨下去,看看对象的属性有什么特性?如何进行名值对的控制?

let obj = {
    name: '命名最头痛'  // 这里的name就是对象的属性
}

对象属性有什么特性❓

这里引用红皮书的一句话:
ECMA-262第5版在定义只有内部才用的特性时,描述了属性的各种特征。
ECMA-262定义这些特性是为了实现JavaScript引擎用的,因此在JavaScript中不能直接访问它们。

ECMA还告诉我们,属性有两种类型,数据属性和访问器属性,它们都有各自的特性

数据属性的特性:(觉得官方的太复杂,这里用通俗易懂的文字解释一下)
[[Configurable]]: 可以理解为开关,这个描述符控制着整个属性,当他为true时候,就可以操控这个属性了,反之,这个属性的内容就定死了,不能修改,也不能删除了。 默认为true
[[Enumerable]]: 能否被for-in枚举。 默认为true
[[Writable]]: 能否修改属性值。 默认为true
[[Value]]: 属性值,默认为undefined
访问器属性的特性:
[[Configurable]]: 同数据属性
[[Enumerable]]: 同数据属性
[[Get]]: 在读取属性时调用的函数,默认值undefined (非必须)
[[Set]]: 在写入属性时调用的函数,默认值undefined (非必须)

看到这些,是不是已经被转晕了,对象,属性,还有属性类型,什么鬼,下面我画一个图,就很好理解了🤔
clipboard.png

一句话说明白:一个对象可以包含多个属性,每个属性都有各自的 “唯一” 的属性类型,

现在,你应该知道对象属性的特性了吧。
这时又有人问了,属性的类型知道了,但是这两个有什么区别呢?怎么用呢?
这两种类型唯一的区别在于是否能直接存储数值,上面已经阐述了对象最后的亮点还是值上,这两种类型分别对值进行直接/间接的操作,前者有[[value]]特性可以直接存储值,这点没悬念,但后者没有这个特性,只能通过[[get]],[[set]]特性间接控制值。

好,我们继续攻破对象的最后一个问题

如何进行名值对的控制❓

我们带着这个问题去思考,对名值对进行控制,那就是要控制它的属性→它的数据类型→它的数据类型特性,所以一个对象的本质就是控制他的属性类型特性,那么如何控制它的属性类型特性呢?

官方给出一个控制方法 Object.defineProperty(),这个方法也是用来创建对象新属性
这个方法接收三个参数 属性所在的对象,属性的名字,描述符对象

官方还提供了一个读取属性特性的方法Object.getOwnPropertyDescriptor()
这个方法接收两个参数 属性所在的对象,属性名(不要忘了带上'')

下面我们来聊聊这几方法是如何用的,如何实现名值对的控制的

废话不多说,直接上🌰,没有什么是一个🌰解决不了的,如果不行,那就两个。

定义单个属性

定义方式:

// 直接在对象上定义
let obj1 = {
    name: '命名最头痛'  // 这里的name就是对象的属性
}
console.log(obj1.name) // 命名最头痛

// 通过Object.defineProperty定义
let obj2 = {}
Object.defineProperty(obj2, 'name', { // 这里创建一个name属性,并在特性中给这个属性赋值
    value:'命名最头痛'
})
console.log(obj.name) // 命名最头痛

你应该会发现,这两个结果是一样的,那么这两个等价吗?答案肯定是No啦,如果结果是一样的,官方何必多此一举提供多一个方法呢,那么这两个区别是什么呢?

①直接在对象上定义属性,只能定义数据属性,且他的[[Configurable]],[[Enumerable]],[[Writable]]特性为默认值全为true,即(在对象上直接定义时,内部特性默认值皆为true

let obj = {
    name: '直接在对象上定义' 
}

console.log(Object.getOwnPropertyDescriptor(obj,'name')) 
//{
//  configurable: true, 
//  enumerable: true
//  value: '直接在对象上定义'
//  writable: true
//}

②通过Object.defineProperty()方法定义
数据属性时,[[Configurable]],[[Enumerable]],[[Writable]]特性默认值为false,[[value]]特性默认值为undefined。
访问器属性时,[[Configurable]],[[Enumerable]]特性默认值为false,若没有定义get或set时,前者意味着不能读,后者意味着不能写。
即(通过Object.defineProperty()定义,内部特性默认值皆为false

let obj = {}
Object.defineProperty(obj, 'name', {
    value: '通过Object.defineProperty()方法定义'
})

console.log(Object.getOwnPropertyDescriptor(obj,'name'))
//{
//  configurable: false, 
//  enumerable: false
//  value: '通过Object.defineProperty()方法定义'
//  writable: false
//}

③访问器属性不能直接定义,只能通过Object.defineProperty()方法定义。
④Object.defineProperty()第三个参数为空对象时,默认为数据属性,其[[value]]为undefined

下面我们看一个访问器属性的常见用法
是时候搬出Vue的双向绑定了

    let obj = {
        _val: 0,
        changeVal: 0
    };
    
    Object.defineProperty(obj, 'val',{
        get() {
            return this._val
        },
        set(newVal) {
            this._val = newVal
            this.changeVal = newVal + 1
        }
    })
    
    obj.val = 1
    console.log(obj.changeVal)   // 2
    console.log(obj.val) // 1

这个🌰很好理解吧,对val属性赋新值时,会触发set函数,然后修改_val和changeVal,执行相应的运算,在读取val属性时,会触发其内部的get函数,这个函数返回_val的值。

这个也能通过函数实现呀,确实如此,只不过这个属性将这个特性封装起来罢了。

细心的朋友这时又有疑虑了,这样定义属性太麻烦了,能不能一次性定义多个属性呢?答案是Yes,请继续往👇看。

定义多个属性
官方提供定义多个属性的方法Object.defineProperties()
这个方法接收两个参数 属性所在的对象,要添加属性的集合
通过这个方法可以一次性定义多个属性,并分别设置他们的特性

一个🌰告诉你用法

let obj = {}

Object.defineProperties(obj, {
    newAge: {
        writable: true,
        value: 10
    },
    _age: {
        writable: true,
        value: 18
    },
    age: {
        get: function() {
            return this._age
        },
        set: function(val) {
            this.newAge = val
        }
    }
})

小结

  • 对象只对复杂数据类型而言
  • 对象三个特点:①无序 ②键值对 ③值可以是基本值,对象或者函数
  • 对象属性有两种类型:数据属性和访问器属性,他们都有各自的特性
  • 一个对象可以包含多个属性,每个属性都有各自的 “唯一” 的属性类型
  • Object.defineProperty()方法用来创建对象新属性或修改对象旧属性
  • Object.getOwnPropertyDescriptor()方法用于读取对象属性的特性
  • Object.defineProperties()方法用于为一个对象一次性定义多个属性,并且可以分别设置他们的特性

尾声

在写这篇文章过程中突然想到一个问题,无论在面试还是提及到对象时,除了属性外,我们还会说到另外一个词,方法,但是官方的定义中只有属性,这个所谓的方法从何而来呢?我的理解是,对象有且仅有属性,所谓的方法只不过是因为属性值可以是function罢了。

既然了解了什么是对象,面向对象的世界就打开了,下面我们继续探索对象世界,请移步到一眼看穿👀JS对象的创建


命名最头痛
385 声望655 粉丝

a == b ? a : b