JavaScript-Object Oriented
JavaScript 对象与对象属性
1 | let obj = {v: 1, o: undefined} |
虽然,属性值都是 undefined,但 特性名不存在与特性值赋为 undefined 是不同的, 可以用 in
运算符,查看对象是否有某个属性
JavaScript 通过一些属性时都会带有一些特征值,ECMA-262 定义的,只有内部使用的,外部不能访问的特性,初期是为了实现 JavaScript 引擎使用的。具体如下表
数据属性
characteristic | description |
---|---|
[[Configurable]] | 能否通过 delete 删除属性,然后重新定义属性,能否修改属性的特性,能否把属性修改为访问器属性 |
[[Enumerable]] | 是否可以通过 for-in 循环枚举属性 |
[[Writable]] | 能否修改属性的值 |
[[Value]] | 包含这个属性的数据值 |
使用 Object.defineProperty 定义属性特性
访问器属性
访问器属性不包含数据,只有 getter,setter 函数。读取访问器属性时会调用 getter 函数,这个函数负责返回有效的值,setter 负责写入,处理新数据
characteristic | description |
---|---|
[[Configurable]] | 能否通过 delete 删除属性,或修改为数据属性 |
[[Enumerable]] | 是否能通过 for-in 枚举属性 |
[[Get]] | 读取属性时调用 |
[[Set]] | 设置属性时调用 |
工厂模式——解决复用
1 | function baby(name, age) { |
构造函数 + new——解决对象识别问题
解决对象识别问题
Audrey instanceof object
结果 true,使得Audrey instanceof baby
为 true
ECMASript 中的构造函数可以用来创建指定类型的对象
构建构造函数的理解
对比工厂模式,按以下方式创建一个构造函数:
- 依照(Object oriented)思想,区别普通函数,方法名首字母大写。如
Baby
; - 不需要
new object()
或 声明{}
- 将属性和方法赋值给
this
。从这里可以看出,this
首先是一个对象。[关于 this 的理解,可从 JavaScript 执行上下文的视角看](https://helenzhanglp.github.
io/2020/11/18/%E6%B5%8F%E8%A7%88%E5%99%A8%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E2%80%94%E2%80%94Javascript%E6%89%A7%E8%A1%8C%E6%9C%BA%E5%88%B6/) - 不需要
return
语句
1 | function Baby(name, age) { |
构造函数调用 —— 普通调用
1 | // 普通调用 |
构造函数当普通函数调用时,在 window 环境中调用,this 指向 window 对象,属性绑定在 window 上
构造函数调用 —— call/apply 在部分
1 | var Cat = new Object() |
在局部范围内调用
构造函数调用 —— new 实例
1 | // new 关键字实例化构造函数 |
new 关键字 + 函数(函数名以大写字母开头),为封装对象建构的流程的函数称为 构造函数
this 是 new 关键字的实例,构造函数执行结束后,作为结果 return
构造函数不需要写 return 语句,如果有写,就返回 return 的值
像 JavaScript 标准 API 中,像 Number
即可以当普通函数 Number('OxFF')
调用。
又可以使用构造函数方式调用 new Number(OXFF)
。
ES6 中,可以使用 new. target
检测构造函数中是否明确撰写 return 使用 new 构建的实例,new.target 代表了构造函数或类本身;否则就是 undefined
构造函数的每一个方法都要在新的实例上创建一次
graph TD obj[Object] --> |`工厂模式 + 入参解决利用问题`| FP[工厂模式 factoryPattern] FP --> |对象识别| CF[new + Constructor Function] CF --> |`需要解决方法或属性共享的问题`| Proto[prototype]
原型模式
每个函数都有一个 prototype,它是一个指针,指向一个对象(由特定类型的所有实例共享的属性和方法),即函数的原型对象
所有原型对象都会自动获取一个 constructor,constructor 包含一个指向 prototype 属性所在函数的指针Audery.constructor === Baby
对象 constructor 是用来标识对象类型
graph TD function[function] --> prototype[function.proptotype] prototype --> object[objectPrototype.constructor] object --> function instance["(new function())[__proto__||[[prototype]]]"]--> object style instance fill:#f99,stroke:#333,stroke-width:4px
原型相关方法
API | description |
---|---|
Fn.prototype.isPrototypeOf(fn1) | 实例 fn1 的原型是不是 Fn.prototype |
Object.getPrototypeOf(fn1) | 返回 Fn.prototype 原型对象 |
fn1.hasOwnProperty(‘attribute’) | 检测属性是否存在于实例中,返回 true 表示该属性存在于实例中 |
Object.getOwnPropertyDescriptor() | 获取实例属性操作符 |
Object.keys() | 返回对象所有可枚举的属性的字符串数组 |
Object.getOwnPropertyNames() | 获取所有实例属性,无论是否可枚举 |
实例和原型中均定义相同属性,实例属性会覆盖原型属性。搜索某个值是先搜索实例属性再搜索原型属性。确实需要访问原型属性,可以用 delete 删除实例属性
in 操作符
对象能够访问指定属性时返回 true。不管是实例属性还是原理对象属性
1 | name in audery // true |
in + fn1.hasOwnProperty 判断属性存在于原型
1 | hasOwnProperty(property, newInstance) { |
for-in 操作符
for-in 操作符,返回所有能够通过对象访问的、可枚举的实例属性和原型属性。覆盖了原型中可枚举的原型属性的实例属性可枚举,可用 for-in 遍历IE 早期存在 bug
构造函数原型的几种写法
1 | function Person(){} |
Cat 实例的写法,导致 constructor 不指向 function Cat()
1 | function Cat(){ |
ES6 类模拟
模拟 static
常量需要仿 static,如 Math.PI
模拟 私有属性
JavaScript 没有 private 之类的语法,可以通过 Closure 模拟
私有属性只有访问权限,不修改
1. 用 getter 将变量变为私有属性
1 | function Account(name, balance) { |
2. 用 Object.defineProperty()
定义属性
Object.defineProperty()
参数一,接收一个对象;参数二,接收想设定的特性名称;参数三是属性描述,采用选项对象的方式来指定属性
defineGetter() 和 defineSetter() 兼容 safari3, chrome 1
1 | function Account(name,balance) { |
Object.defineProperties()
设置多个私有属性
1 | function Account(name, balance) { |
特性描述器
从 ES5 开始,每个特性都由 value, writable, enumerable, configurable 4 个特性设定。
特性 | 描述 |
---|---|
value | 特性值 |
writable | 特性值是否可改 |
enumerable | 特性是否可枚举 |
configurable | 能否用 delete 删除,或用 Object.defineProperty/Object.defineProperties 修改属性 |
查询或设定属性时,以上 4 个属性会聚合在对象上,称为特性描述器(Property descriptor)
有关描述器特性的几个 API
API | 描述 |
---|---|
Object.getOwnPropertyDescriptor() | 取特性描述器信息,具体如上表 |
Object.defineProperty() | 定义属性,使用见上 demo-Account-1 |
Object.defineProperties() | 一次性定义多个属性,使用见上 demo-Account-2 |
详解对象定义与属性描述器
1. 直接对对象新增特性
1 | let obj = {name: 'helen'} |
{value: “helen”, writable: true, enumerable: true, configurable: true}
2. Object.defineProperty 定义特性
1 | let obj = {} |
`use strict`+`writable:false` 时,修改变量,引发 TypeError。{value: “helen”, writable: false, enumerable: false, configurable: false}
`use strict`+`configurable:false`时,删除或使用 Object.defineProperty 等修改时,引发 TypeError。
1 | // Account demo |
Object.defineProperty 或 Object.defineProperties 定义的属性,默认 writable: false, enumerable: false, configurable: false 所以,只有方法可枚举
利用 writable 属性写一个不可枚举数组,具体代码见 gitHub
扩充、弥封、冻结
API | Description |
---|---|
Object.preventExtensions() | 限定对象扩充。指定对象,将对象标示为无法扩充,调用传回对象本身 |
Object.isExtensible() | 测试对象是否可以扩充。 |
Object.seal() | 对象密封,密封对象不能扩充或删除对象上的特性,也不能修改描述器,但可以修改特性值(writable: true) |
Object.isSeal() | 判断对象是否被密封 |
Object.freeze() | 解冻对象 |
Object.isFreeze() | 判断是否被解冻 |
Object.preventExtensions(),限定扩充的对象只是无法新增。如果对象属性 writable: true ,则可以修改。若 configurable: true,可以使用 delete 删除。设置为无法扩充的对象,则无法重设为可扩充对象
原型对象
构造函数与 prototype
1 | function a() {} |
每个函数都有 prototype 特性a.prototype instance a
得到的是 falsea.prototype instance object
得到的是 true
那么基本上,函数的 prototype 是 object 的实例
1 | let A = new a() |
使用 new 关键字实例函数,将返回一个对象,该对象的 proto 特性 与 该函数的 prototype 共同指向 constructor
实例的 proto 特性用来取函数的原型对象ES5 建议使用 Object.getPrototyepOf() 取实例的原型对象
1 | A.__proto__ === Object.getPrototypeOf(A) // true |
hasOwnProperty() 判断实例本身是否具有某个属性