Js 面向对象-属性描述符
上篇讲的是 js 函数式编程, 结合了 js 作用域, this 指向, call, apply, bind 模拟实现, 纯函数, 柯里化, 组合函数等各种实现, 可以说算是深入了 js 的 30% 的核心东西了.
从本篇开始的 30%, 就专注于 js 的面向对象, 看看 js 是如何 巧妙设计实现 面向对象的, 就是 原型 和 原型链, 最为关键的一步是实现继承的理解, 这个掌握了, 可以说再去看其他传统面相对象语言, 如 java , 简直分分钟拿捏.
js 本身是一个单线程的动态语言, 有点和 python 差不多, 类似脚本语言, 面相对象就比较弱, 但是通过巧妙设计, 也能实现面向对象的思想, 觉得确实还是很了不起的.
面向对象
编程世界就是对现实世界的模拟实现.
回顾我的一些编程语言使用情况, 从最开始用 C 语言来学习入门到放弃, 然后 R 语言来做统计/矩阵计算, 接着用 Python 来做数据分析, 接着用 sql 来做结构化报表, 接着用 js 来做数据可视化, 接着用 java / scala 来处理大数据, 接着用 Go / Rust 语言来作为数据 api 服务后台, 现在又在深入学习一下 js, 想着也是在数据报表和数据产品领域做得更深入一些. 过程中和这些语言打交道, 有面向过程, 面向对象, 面向接口, 面向集合, 面向函数, 面相 AI.
所以对于面向对象这部分来说, 封装, 继承, 多态都已十分熟悉了. Java 是一个纯面向对象语言, 干啥都是要搞个 class, 然后实例化对象, 对象之间的交互, 从这个角度看, java 中的类更像是一个 数据结构. 对于 js 和 python 来说, 二者更像是脚本语言, 只不过生态异常强大而已, 面向对象, 过程都可, 但似乎在业务逻辑处理上, 面向过程多一些. 对于 Go / Rust 这两个新时代的强大语言, 摒弃了类 java , C++ 的一套面向对象编程范式, 更倾向于 组合 的方式来实现 面向对象的思想, 也是很强大呀.
认识 js 对象
在 js 中, 对象被设计成一组 属性的无序集合, 类似哈希表, 有 key -value 组成. key 是一个标识符, value 可以是任意类型, 也可以是其他对象或者函数类型. 如果 值是一个函数, 则我们可以称之为是 对象的方法.
最开始接触 js 对象的时候, 经常将它和 Python 中的 Dict 搞混, 甚至一度认为 js 中的对象就是 python 中的字典.
**创建对象 **最常见的方式就是通过 字面量 的方式. 注意这里的 key 会自动转为字符的, 因此可以 不加引号. 这个比较流行.
// 这个比较直观
var obj = {
name: 'youge',
age: 18,
height: 1.80,
running: function() {
console.log(this.name + ' is running')
}
}
但用过其他面向对象的语言也会习惯用 new 的方式, 因为在 java 中就都要先来 new 一个对象.
// Object() 是一个内置函数 (构造函数)
var obj = new Object()
然后往里面添加东西就好了.
// 添加属性
obj.name = 'youge'
obj.age = 18
obj.height: 1.80
// 添加方法
obj.running = function() {
console.log(this.name = ' is running')
}
js 对象属性操作
在 js 中对象表现为 key-value 形式, value 可以是任意类型, key 则要求是 唯一标识的, key 被称为属性:
- 字符串, 可以且最为常见
- 数值, 可以且会被自动转为数值
- Symbol
- 而
对象, 数组, 函数不能作为键, (Map 有的可以)
先来看一下对属性的操作, 无非也是 增删改查.
// js 对象属性操作
var obj = {
name: 'youge',
age: 18
}
// 获取属性 [[get]]
console.log(obj.name) // youge
// 新增和修改属性, 存在则替换, 没有则新增 [[set]]
obj.name = 'cj'
console.log(obj.name) // cj
// 删除属性, 不存在不抛异常
delete obj.name
console.log(obj) // { age: 18 }
// 其实可以对属性的操作进行限制
// 1. 不允许, 某个属性被赋值
// 2. 不允许, 某个属性被删除
// 3. 不允许, 某个属性被遍历
// 遍历
for (var key in obj) {
console.log(key) // age
}
上面这种对象的创建通过字面量的形式,肯定没法控制, 但是通过 属性描述符的方式就可以精准控制了.
- 通过属性描述符, 可以精准添加 或者 修改对象的属性
- 属性描述符需要使用
Object.defineProperty()的内置函数进行控制
Object.defineProperty
这个方法, 会直接在一个对象上定义一个新的属性, 定义阶段可以对属性进行描述, 并返回此对象
Object.defineProperty(obj, prop, descriptor)
- obj: 要定义属性的对象
- prop: 要定义或修改的属性的名称或 Symbol
- descriptor: 要定义或修改的属性的描述符
- 直接修改的原对象, 返回原对象 (就指针引用而已)
var obj = { name: 'youge', age: 18 }
// 属性描述符是一个对象
var obj2 = Object.defineProperty(obj, "height", {
// 很多配置的
value: 1.8
})
console.log(obj) // { name: 'youge', age: 18 }
// 修改的原对象, 返回的是一个引用而已
console.log(obj2 === obj) // true
发现, 我们添加的 height 属性好像没打印出来, 这是正常的, 因为我们的配置没有搞全而已, 但已经配置上了的.
console.log(obj.height) // 1.8
属性描述符
它是一个对象, 类似配置文件一样, 来对属性进行行为限制的, 根据功能不同可以分为两类:
- 数据属性 描述符
- 存取属性 描述符
问了 AI 是这么通俗理解的, 数据属性, 像一个普通抽屉, 直接放东西, 可以设置, "能否改, 都否看, 能否拆"; 而存取属性, 则像一个 只能门卫, 你读它时帮你算, 你写它时帮你检查, 不直接存取数据.
| 特性 | 数据属性(Data Property) | 存取属性(Accessor Property) |
|---|---|---|
| 存值方式 | 直接存值(value) |
通过 get 和 set 函数 |
| 能否修改值 | 看 writable |
看有没有 set 函数 |
有没有 value |
✅ 有 | ❌ 没有 |
有没有 get/set |
❌ 没有(或为 undefined) |
✅ 有 |
| 常见用途 | 存普通数据 | 实现“计算属性”、数据验证、隐藏逻辑 |
更专业一点是这样的: (横着看哈)
| Configurable | Enumerable | Value | Writable | Get | Set | |
|---|---|---|---|---|---|---|
| 数据描述符 | Y | Y | Y | Y | N | N |
| 存取描述符 | Y | Y | N | N | Y | Y |
就 configurable 和 enumberable 都是可以配置的, 数据描述符可以直接写和改数据 (writable, value)
而 存取描述符则更像是一个守卫角色, 通过 set, get 来间接修改和访问数据, 外部程序可以通过 监听拦截 我们对属性和数据的操作 (get, set ) 从而做 something
Value + writable VS get() + set() 二者是不能并存的, 不然就又当又立了. 要么直接访问, 要么通过方法调用.
而这其实就是 vue 响应式系统的底层原理 (defineProperty -> Proxy )
数据属性-描述符
[[configurable]
它用于控制属性的可配置性, 决定是否可以修改或者删除, 是否可改变属性的其他特性 (enumrable, writable ...)
直接定义是它的值是 true, 通过属性定义则为 false.
当 configurable 为 true :
- 可通过
delete删除该属性 - 可通过 Object.defineProperty() 修改属性
当 configurable 为 false :
- 不可用
delete删除该属性 - 不可用 Object.defineProperty() 修改属性的 configurable 和 enumerable 特性
- 若为 数据属性, writable 为 true 时, 还是可以修改 value
- 若为 存取属性, writable 为 true 时, 还是可以使用 get / set
[[enumerable:]
它用于控制一个属性能否被看到, 被遍历到, 即 for-in, Object.keys() 可以用
直接定义是它的值是 true, 通过属性定义则为 false.
但是, 若知道 key 名字, 还是能访问!!!
应用场景01: 隐藏内部状态或者私有数据
const config = {}
Object.defineProperty(config, 'apiKey', {
value: '12345-secret-key',
enumerable: false,
configurable: false,
get: false
})
// 外部遍历 是看不到 apiKey 的, 也不会暴露到日志或接口
for (var key in config) {
console.log(key)
}
// 但是, 若知道 key 名字, 还是能访问!!!
// key 改为 Symbol 很难猜; 或者用 闭包 + 模块化; 或用 ts 隐藏
console.log(config.apiKey)
应用场景02: 定义只用于代码内部的方法或者标记
// 表示用户 "已登录", 但不想让别人遍历看到!
// 知道这个标记字段的人, 那可以.
var user = {}
Object.defineProperty(user, '_isLogged', {
value: true,
enumerable: false
})
应用场景03: 实现 "计算属性" 但不想暴露 getter 函数本身
// 实现了计算属性, 但内部细节是遍历不到的
var person = {}
Object.defineProperty(person, 'getSalary', {
value: function() { return this._salary * 1.2 },
enumerable: false
})
[[value]] + [[writable]]
是直接处理数据的的方式, value 直接表示值是什么, writable 表示这个值能否被修改或者替换.
// value
// 账户上的钱
var account = {}
Object.defineProperty(account, 'balance', {
value: 1000 // 有1k
})
console.log(account.balance) // 1000
// writable 默认是 false
account.balance = 100;
console.log(account.balance) // 还是1000
如果将 writable 设置为 true 则就可以直接修改 value 了.
// value + writable
// 账户上的钱
var account = {}
Object.defineProperty(account, 'balance', {
value: 1000, // 有1k
writable: true
})
// 当 writable 为 true, 则可修改
account.balance = 100;
console.log(account.balance) // 变为100
存取属性-描述符
前两个 configurable 和 enumerable 属性和功能和数据属性是一样的. 区别在于对于值的管理, 用的是 get/set.
-
Get: 读取这个属性时, 不是直接拿, 而是 "自动运行 get() 函数" 来获取
-
Set: 修改这个属性时, 不是直接赋值, 而是 "自动运行 set() 函数" 来赋值
它像一个智能的门卫, 控制属性的读和写行为, 而 vue2 的响应式原理, 就是通过 劫持 set / get 还有 writable 的行为从而实现数据的响应式.
应用场景01: 计算属性
// 计算属性
get fullName() {
return this.firstNmae + this.secondName
}
应用场景02: 数据验证
// 数据验证
set age(value) {
if (typeof value !== 'number' || value < 0 || value > 150) {
console.log('年龄不合法!')
return
}
}
应用场景03: 格式化输入/输出
set phone(value) {
this._phone = value.replace(/\D/g, '') // 仅保留数数字
}
应用场景04: 触发副作用 (更新页面, 日志)
set theme(value) {
document.body.className = value
localStorage.setItem('theme', value)
console.log('主题已更换')
}
这里同数据属性进行一个对比
| 特性 | value + writable | get() + set() |
|---|---|---|
| 本质 | 直接存一个值 | 不存值, 靠函数计算或处理其他属性 |
| 值来源 | value 字段定义 | 从 get() 函数返回值来 |
| 修改值 | Writable: true | 有实现 set() 函数 |
| 存真实数据 | 是的, 直接存值在内存 | 否, 通常是读写其他属性 |
| 用途 | 存储普通数据, 性能高 | 存计算属性, 校验, 副作用等, 性能损耗 |
| 同时使用 | 不能和 set/get 共存 | 不能和 value / writable 共存 |
一个属性要么是数据属性(value + writable),要么是存取属性(get + set),不能又当又立的啦.
// get, set
// 私有属性间接访问 + 动作拦截
var obj = {
// 约定的私有属性
_address: '长安镇'
}
// 存取属性描述符
Object.defineProperty(obj, "address", {
enumerable: true,
configurable: true,
// 不让直接访问 _address, 通过函数调用去访问
get: function() {
console.log('_address 被访问啦~')
return this._address
},
set(value) {
console.log('_address 被重置啦~')
this._address = value
}
})
// 调用 get
console.log(obj.address)
// 调用 set, 没有实现则不生效, 不报错
obj.address = '虎门镇'
console.log(obj.address) // 虎门镇
对象基础补充
本篇的重点是了解面向对象, 掌握 js 中的对象和 js 对象的数据属性描述符 和 存取属性描述符, 就差不多了, 余下的部分是对上面知识的拓展补充, 了解一下即可.
常用对象属性方法
获取对象的属性描述符:
- Object.getOwnPropertyDescriptor(obj, prop)
- Object.getOwnPropertyDescriptors()
// 获取描述符信息
var obj = { name: 'youge'}
Object.defineProperty(obj, "age", {
enumerable: true,
configurable: true,
value: 18,
writable: false
})
// 获取 obj 的 age 属性描述符信息
// { value: 18, writable: false, enumerable: true, configurable: true }
console.log(Object.getOwnPropertyDescriptor(obj, 'age'))
// 获取 obj 的 所有属性描述符信息
console.log(Object.getOwnPropertyDescriptors(obj))
{
name: {
value: 'youge',
writable: true,
enumerable: true,
configurable: true
},
age: { value: 18, writable: false, enumerable: true, configurable: true }
}
禁止对象拓展新的属性: Object.preventExtensions(obj)
// 实现原理: 遍历对象每个属性, 批量设置 configurable, writable 等
for (var key in obj) {
Object.defineProperty(obj, key, {
// 关键:
configurable: false,
enumerable: true,
writable: true,
value: obj[key]
})
}
// 禁止拓展属性 Object.preventExtensions()
var obj = { name: 'youge' }
// 禁止添加属性
Object.preventExtensions(obj)
obj.age = 18
console.log(obj) // { name: 'youge' }
禁止配置和删除属性 Object.seal(obj) , 实际上是调用 preventExtensions() , 并将 configurable 设置 fale.
// 禁止拓展和删除属性
var obj = { name: 'youge' }
Object.seal(obj)
delete obj.name
obj.age = 18
console.log(obj) // { name: 'youge' }
禁止修改现有属性: Object.freeze(obj), 实际上是调用 seal, 并将现有属性的 writable 设置 false
// 冻结现有属性
var obj = { name: 'youge' }
Object.freeze(obj)
delete obj.name
obj.name = 'cjj'
obj.age = 18
console.log(obj) // { name: 'youge' }
至此, js 对象的基础认识部分就到这了, 下篇继续将批量创建对象的工厂模式和认识原型, 一步步来吧.

浙公网安备 33010602011771号