普通属性,排序属性和隐藏类
var obj = {}
obj.p1 = 'str1'
obj.p6 = 'str6'
obj.p2 = 'str2'
obj[1] = 'num1'
obj[6] = 'num6'
obj[2] = 'num2'
for(var p in obj){
console.log("property: ",obj[p])
}
//
property: num1
property: num2
property: num6
property: str1
property: str6
property: str2
两种属性: 字符串作为键的和数字作为键的属性
键被遍历的顺序似乎是有规律的
1.常规属性
键为字符串的属性
特点: 根据创建时的顺序排序
2.排序属性
属性键值为数字(包含数字字符串)的属性
特点: 按照索引值大小升序排序
为什么设计常规属性和排序属性
- 提升属性的访问速度
- 两种线性数据结构保存(elements和properties,先访问排序属性再访问常规属性)
对象内属性
被保存到对象自身的常规属性
内属性的数量限制: 大概是10个
V8为了性能做了优化,当数量不同时,在elements和properties中保存的数量不同,线性或非线性的数据结构也不同, 线性结构查找的时候快, 插入删除的时候慢,v8引擎会自动评估如何性能最优
隐藏类
什么是: 描述了对象的属性布局,包含属性名和属性偏移量
作用: 从时间上提升速度, 从空间上节约内存
动态添加和删除属性会破坏隐藏类,有性能开销
如何守护隐藏类
- 初始化时保持属性顺序一致
- 一次性初始化完毕
- 谨慎使用delete
属性来源, 属性访问控制, 属性冻结
- 静态属性, 例如: Object.assign
- 原型属性, 例如: Object.prototype.toString
- 实例属性, 例如: function Person(name){ this.name = name }
class Person {
constructor(name,age){
this.name = name
this.age = age
}
//这种写法是实例属性
getName = () =>{
return this.name
}
//这种写法是原型属性
getAge(){
return this.age
}
}
属性描述符
获取属性: Object.defineProperty Object.definePropertied
得到属性: Object.getOwnPropertyDescriptor Object.getOwnPropertyDescriptors
可配置 configurable
可枚举 enumerable
值 value
可更改 writable
访问器函数 get
访问器函数 set
- 数据属性 value + writable + configurable + enumerable
- 访问器属性 get + set + configurable + enumerable
const obj = {}
Object.defineProperty(obj,"name",{})
//此时obj.name不可改写
//{
//value:undefined,
//writable:false,
//enumerable:false,
//configurable:false
//}
obj.name=1
console.log(obj.name) //undefined
Object.defineProperty缺点
- 无法监听数组变化
- 只能劫持对象的属性, 因此我们需要对每个对象的每个属性进行遍历。如果属性也是对象, 还得进行递归
对象可扩展-Objetc.preventExtensions
- Object.preventExtensions: 对象变得不可扩展, 也就是永远不能再添加新的属性
- Object.isExtensible: 判断一个对象是否是可扩展
对象的封闭-Object.seal
- Object.seal: 阻止添加新属性+属性标记位不可配置
- Object.isSealed: 检查一个对象是否被密封
对象的冻结-Object.freeze
- Object.freeze: 不加新属性 + 不可配置 + 不能修改值
- Object.isFrozen: 检查一个对象是否被冻结
方法 | 新增属性 | 修改描述符 | 删除属性 | 更改属性值 |
---|---|---|---|---|
Object.preventExtensions | × | √ | √ | √ |
Object.seal | × | ×(writable有例外) | × | √ |
Object.freeze | × | ×(writable有例外) | × | × |
8+种姿势访问原型
1.prototype
- prototype是一个对象
- 原型会形成原型链, 原型链上查找属性比较耗时, 访问不存在的属性会访问整个原型链
class Person{
//私有属性
#canTalk = true
//静态属性
static isAnimal = true
constructor(name, age){
//实例属性
this.name = name
this.age = age
}
//原型属性
sayName(){
console.log(this.name)
}
}
2. __ proto__
- 构造函数的原型, null以外的对象均有__proto__属性
- Function, class的实例有prototype以及__proto__属性
- 普通函数, 祖上第三代上必为null
思考1: __proto__是实例对象的自身属性还是原型上属性
自身属性
思考2:普通对象祖上第几代__proto__为null
2代
3.instanceof
- 检测构造函数的prototype属性是否出现在某个实例对象的原型链上
思考1: 手写instanceOf
function instanceOf(instance, cclass){
let proto = instance.__proto__
let prototype = cclass.prototype
while(proto){
if( proto === prototype ) return true
proto = proto.__proto__
}
return false
}
思考2: Object instanceof Function, Function instanceof Object
本质上都是函数
4.getPrototypeOf
- 返回对象的原型
- Object.getPrototypeof, Reflect.getPrototypeOf
- 内部先 toObject转换, 注意null和undefined
5.setPrototypeOf
- 指定对象的原型
- Object.settPrototypeOf, Reflect.setPrototypeOf
- 原型的尽头是null
6.isPrototypeOf
- 一个对象是否存在于另一个对象的原型链上
- 左操作数应该是一个原型
其他
Object.create
方法 | 作用 | 重点和注意事项 |
---|---|---|
prototype | 获取对象原型 | 1.class ES6转ES5基于prototype 2.toString.call的怪异现象 |
__ proto __ | 构造函数的原型 | 1.函数祖上第三代__proto__是null 2.普通对象祖上第二代__proto__是null |
instanceof | 构造函数(右侧)的prototype属性是否出现在某个实例对象(左侧)的原型链上 | 1.Object instanceof Function 2.Function instanceof Function |
getPrototype | 获取对象的原型 | null和undefined没有原型 |
setPrototype | 设置对象的原型 | null可以作为第二个参数,原型的尽头是null |
isPrototype | 对象是否存在于另一个对象的原型链上 | 不会有prototype这一步运算 |
Object.create | 使用现有的对象来提供新创建的对象的__proto__ | 创建纯净对象 |
对象的属性遍历
属性的类型: 普通属性 不可枚举属性 原型属性 Symbol属性 静态属性
方法名 | 普通属性 | 不可枚举属性 | Symbol属性 | 原型属性 |
---|---|---|---|---|
for in | √ | × | × | √ |
Object.keys | √ | × | × | × |
Object.getOwnPropertyNames | √ | √ | × | × |
Object.getOwnPropertySymbols | × | √ | √ | × |
Reflect.ownKeys | √ | √ | √ | × |
获取对象的全部静态属性
- 不要被静态属性误导
- Reflect.ownKeys = Object.getOwnPropertyNames + Object.getOwnPropertySymbols
获取原型上的所有属性
注意: 是原型上的所有属性,注意所有两字!
思路: 递归并剔除内置属性
function loadAllProperties(instance){
if(instance == null) return;
let proto = instance.__proto__
while(proto){
result.push(...Reflect.ownKeys(proto))
proto = proto.__proto__
}
}
获取所有不可枚举的属性
如何知道某个属性不可枚举?
Object.getOwnPropertyDescriptors
考虑不考虑原型上的不可枚举属性?
//获取原型上所有不可枚举属性
function getNoEnumrable(_obj){
//获取原型对象
const keys = Reflect.ownKeys(_obj)
const result = keys.filter(key => {
return !Object.getOwnPropertyDescriptor(_obj,key).enumerable
})
return result
}
对象的隐式类型转换和注意事项
显式转换:主要通过JS定义的转换方法进行转换, 如String, Object, 显式toString,parseInt/parseFloat等
隐式转换: 编译器自动完成类型转换的方式就称为隐式转换, 总是期望返回基本类型值
何时发生隐式转换
- 二元 + 运算符
- 关系运算符> , < , >= , <= , ==
- 逻辑! , if/while , 三目条件
- 属性键遍历, for in等
- 模板字符串
对象隐式转换三大扛把子
- Symbol.toPrimitive
- Object.prototype.valueOf
- Object.prototype.toString
- 如果 Symbol.toPrimitive 方法存在, 优先调用, 无视valueOf 和 toString方法
- 否则, 如果期望是"string" --先调用obj.toString()如果返回不是原始值, 继续调用obj.valueOf()
- 否则, 如果期望值是"number"或"default" – 先调用obj.valueOf() 如果返回不是原始值, 继续调用obj.toString()
//如果未定义[Symbol.toPrimitive],期望值是string,toString和valueOf都没返回原始值,会抛出异常
const obj = {
value : 10,
valueOf(){
return this
},
toString(){
return this
}
}
console.log(10+obj) //报错
hint-string
- window.alert(obj)
- 模板字符串
${obj}
- test[obj] = 123
const obj = {
[Symbol.toPrimitive](hint){
if(hint == 'number'){
return 10
}
if(hint == 'string'){
return 'hello'
}
return true
}
}
console.log(`${obj}`) //hello
obj[obj] = 123
console.log(Object.keys(obj)) //['hello']
hint-number
- 一元+,位移
- -*/关系运算符
- Math.pow, String.prototype.slice等很多内部方法
吃透 JSON 和 toJSON
JSON格式
JSON是一种轻量级的、基于文本的、与语法无关的语法,用于定义数据交换格式
JSON键:只能是字符串,必须双引号包裹
最后一堆键值对后不能加逗号
值:number值必须是十进制
对象字面量
是创建对象的一种快捷方式, 字面量的性能优于使用new 构建, var obj = {}
JSON.parse()
注意:第二个参数函数,可以用来过滤数据或者调整数据
遍历顺序:先遍历子属性再返回父属性
const jsonStr = `{"name":"帅哥", "age":18, "IDCard":"xxxxx" }`
var obj = JSON.parse(jsonStr,function(key , value){
if(key == 'IDCard'){
return undefined
} else {
return value;
}
})
JSON.stringify
- 语法:JSON.stringify(value,[,replacer,[,space]])
- 第二个参数replacer:过滤属性或者处理值(如果是一个函数,则在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理;如果是一个数组,则只有包含在这个数组中的属性名才会被序列化到最终的JSOn字符串中;如果改参数为null或者未提供,则所有的属性都会被序列化)
- 第三个参数space:美化输出格式(如果参数是个数字,它代表有多少的空格,上限为10,该值若小于1,则意味着没有空格;如果参数为字符串,当字符串长度超过10个字母,取前10个字母,该字符串被作为空格,如果改参数没有提供或者为null,将没有空格)
const person = {
name: '帅哥',
age: 45,
birth: '1990-01-01'
}
// 第二个参数为函数
var jsonString = JSON.stringify(person, function(key,value){
if(typeof(value) === 'string'){
return undefined;
}
return value;
}) // {"age":45}
// 第二个参数为数组
JSON.stringify(person, ['name','age']);
// {"name":"帅哥","age":45}
// 第三个参数
JSON.stringify(person, null, '\t');
//{
// "name":"帅哥",
// "age":45,
// "birthday":"1990-01-01"
//}
规则-undefined、任意的函数、symbol
- 作为对象属性值,自动忽略
- 作为数组,序列化返回 null
- 单独序列化时,返回 undefined
JSON.stringify其他规则
- Date返回 ISO 字符串
- 循环饮用报错
- NaN,infinity,null都会作为null
- BigInt 报错
- Map/Set/WeakMap等对象,仅序列化可枚举属性
toJson
对象拥有toJSON方法,toJSON会覆盖对象默认的序列化行为
var project = {
"name":"牙膏",
"count":10,
"orderDetail":{
"orderId": 7263721832
},
toJSON(){
return{
name:'牙膏'
}
}
}
JSON.stringify(product); //{"name":"牙膏"}
对象的多种克隆方式及注意事项
- 意义:保证原数据的完整性和独立性
- 常见场景:复制数据,函数如参,class构造函数等
浅克隆
- 只克隆对象的第一层级
- 如果属性值是原始数据类型,拷贝其值,也就是我们常说的值拷贝
- 如果属性值是引用类型,拷贝其内存地址,也就我们常说的引用拷贝
常见的浅克隆:ES6扩展运算符… Object.assign. for in 和其他的一层遍历复制
数组常见的浅克隆:ES6扩展运算符… slice [].concat
深度克隆
- 克隆对象的每个层级
- 如果属性值是原始数据类型,拷贝其值,也就是我们常说的值拷贝
- 如果属性值是引用类型,递归克隆
方法方式 | 优点 | 缺点 | 备注 |
---|---|---|---|
浅克隆 | 性能高 | 数据可能不完全独立 | 一层属性全是值类型,等同于深度克隆 |
深度克隆 | 不影响原对象 | 性能低,时间和空间消耗更大 | 保持数据独立性,让函数无副作用 |
深度克隆方法1 JSON.stringify+JSON.parse
function clone(obj){
return JSON.parse(JSON.stringify(obj))
}
- 优点:纯天然,无污染
- 只能复制普通键的属性,Symbol类型的无能为力
- 循环引用对象,比如Window不能复制
- 函数,Date,Rege,Blob等类型不能复制
- 性能差
深度克隆方法2 消息通讯-BroadcastChannel等等
- window.postMessage
- Broadcast Channel
- Shared Worker
- Message Channel
特点
- 循环引用对象不能复制,如:Windows
- 函数不能复制
- 同步变成异步
深度克隆方法3 手写深度克隆