前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
属性描述符
属性名 | 属性类型 | 值 | 默认值 | 描述 |
---|---|---|---|---|
value | 数据属性 | ECMAScript 语言值 | undefined | 属性值 |
writable | 数据属性 | true, false | false | true: 允许改写值 false: 不允许改写值 |
get | 访问器属性 | undefined, Function | undefined | 取值函数 |
set | 访问器属性 | undefined, Function | undefined | 设置值的函数 |
configurable | 数据属性 或者访问器属性 | true, false | false | true: 允许更改属性配置 false: 不允许更改属性配置则尝试删除该属性,将其从数据属性更改为访问属性,或从访问属性更改为数据属性,或对其属性进行任何更改(替换现有的[[ Value ]]或将[[ Writable ]]设置为 false 除外) 都将失败 |
enumerable | 数据属性 或者访问器属性 | true, false | false | true: 可以被枚举 false: 不可以被枚举 |
你眼中的属性设置
通过 Object.defineProperty 或者 Object.defineProperties 设置上面的属性给对象添加属性。
虽然工程师通过是通过 .
或者 []
的方式设置属性,其底层逻辑是一致的。
var obj = {};
obj.name = 'name';
// 等同于
Object.defineProperty(obj, {
value: 'name', // 值
writable: true, // 可以改写值
configurable: true, // 可以更改配置
enumerable: true // 可以被枚举
});
需要注意的是writable
, configurable
, enumerable
的默认值都是false。
var obj = {};
Object.defineProperty(obj, "name", {});
// 尝试修改 writeable,失败
obj.name = 10;
console.log("name", obj.name); // name undefined
// 尝试删除 configurable,失败
console.log(delete obj.name); // false
// 尝试遍历, 不会被枚举出来
console.log(Object.keys(obj)); // []
configurable
的真正含义
本意是不再允许再配置该属性, 下面两种情况除外
- 替换现有的value
- 将writeable设置为 false 除外
第一条 其实也还好理解, value 是由 writeable
控制的,如果其值为 true, 允许更改值合情合理。
第二条 writeable 支持降级。
const obj = {};
Object.defineProperty(obj, "name", {
writable: true
});
// 设置值,成功
obj.name = 'name';
// 尝试修改描述符writable信息, 成功
Object.defineProperty(obj, "name", {
writable: false
});
那反过来,哪些情况会被禁止或者失败
- 尝试删除该属性
- 将其从数据属性更改为访问属性
- 或从访问属性更改为数据属性
- 或对其属性进行任何更改
- configurable
- enumerable
- get
- set
属性描述符,真正需要额外注意的就是 这个 configurable,其他的,懂的都懂。
整体的属性访问控制
属性描述符,通过 Object.defineProperty 仅仅只能对某个属性进行设置,虽然可以通过Object.defineProperties 一次对多个属性进行设置,底层逻辑依然是一个一个属性就进行设置,本质没有发生任何变化。并不能做到对对象整体的访问控制。
有请 Object.preventExtensions, Object.seal, Object.freeze
方法 | 作用 |
---|---|
Object.preventExtensions | 阻止添加新属性 |
Object.seal | 阻止添加新属性 现有属性标记为不可配置 不可以删除属性 |
Object.freeze | 阻止添加新属性 现有属性标记为不可配置 不可以删除属性 不能修改属性值 |
换个姿势
方法 | 新增属性 | 修改描述符 | 删除属性 | 更改属性值 |
---|---|---|---|---|
Object.preventExtensions | ✘ | ✔ | ✔ | ✔ |
Object.seal | ✘ | ✘(writable有例外) | ✘ | ✔ |
Object.freeze | ✘ | ✘(writable有例外) | ✘ | ✘ |
通用方法
上面的三个方法,只能对对象的一级属性做到控制,
- 如果多级属性呢?
- 原型上的属性呢?
var obj = {
a: {
b: "b"
}
}
Object.freeze(obj);
// 二级属性赋值成功
obj.a.b = 'bb'
封装一个deepFreeze
function isObject(obj) {
if (obj == null) return false;
return typeof obj == 'object';
}
function deepFreeze(obj) {
Object.freeze(obj);
const keys = Reflect.ownKeys(obj);
keys.forEach(key => {
let val = obj[key];
if (isObject(val)) {
deepFreeze(val);
}
})
}
测试一下
var obj = {
a: {
b: "b"
}
}
deepFreeze(obj);
obj.a.b = 'bb'
// 二级属性赋值不成功
console.log(obj.a.b); // "b"
这样写三个方法,是不是逻辑有点浪费呢? 借用一下函数式编程的高阶函数的思想,抽象一下。
function isObject(obj) {
if (obj == null) return false;
return typeof obj == 'object';
}
function deepCommon(obj, method) {
method.call(Object, obj);
const keys = Reflect.ownKeys(obj);
keys.forEach(key => {
let val = obj[key];
if (isObject(val)) {
deepCommon(val, method);
}
})
}
const createDeep = method => obj => deepCommon(obj, method);
const deepPreventExtensions = createDeep(Object.deepPreventExtensions);
const deepSeal = createDeep(Object.seal);
const deepFreeze = createDeep(Object.freeze);
看似已经nice了,
- 如果属性的值是数组呢?
- 如果属性的值是 Map, Set等呢?
属性来自何处
属性的来源
- 静态属性
- 实例属性
- 原型属性
静态属性和另外两种本质还是有区别的,重点就是 实例属性和原型属性。 当你使用某个属性时, 两个问题?
- 有没有这个属性
- 这个属性来自哪里
获取所有的属性
方法名 | 普通属性 | 不可枚举属性 | Symbol属性 | 原型属性 |
---|---|---|---|---|
for in | ✔ | ✘ | ✘ | ✔ |
Object.keys | ✔ | ✘ | ✘ | ✘ |
Object.getOwnPropertyNames | ✔ | ✔ | ✘ | ✘ |
Object.getOwnPropertySymbols | ✘ | ✔(Symbol) | ✔ | ✘ |
Reflect.ownKeys | ✔ | ✔ | ✔ | ✘ |
要想获得全部属性
- 原型上的
- 不可枚举的
- Symbol
利用现有方法,是不能满足的,那么代码起。
- 采用 Reflect.ownKeys 获取所有属性
- 等效与 Object.getOwnPropertyNames + Object.getOwnPropertySymbols
- 遍历原型
function getAllProperties(obj) {
let result;
function walkPrototype(instance) {
if (instance == null) return;
result = Reflect.ownKeys(instance);
let proto = Object.getPrototypeOf(instance);
while (proto) {
result.push(...Reflect.ownKeys(proto));
proto = Object.getPrototypeOf(proto);
}
}
return (walkPrototype(obj), result)
}
执行 getAllProperties(()=>{})
可以获得全部的属性,但是可以看到很多重复的属性。
获取所有的属性去重
利用Set去重。
function getAllProperties(obj) {
let result;
function walkPrototype(instance) {
if (instance == null) return;
result = Reflect.ownKeys(instance);
let proto = Object.getPrototypeOf(instance);
while (proto) {
result.push(...Reflect.ownKeys(proto));
proto = Object.getPrototypeOf(proto);
}
}
walkPrototype(obj);
// Set 去重
return Array.from(new Set(result));
}
属性从24个成了20个。
获取所有属性树
function getAllProperties(obj) {
let result = {
properties: [],
};
let r = result;
function walkPrototype(instance) {
if (instance == null) return;
result.properties = Reflect.ownKeys(instance);
let proto = Object.getPrototypeOf(instance);
while (proto) {
result.prototype = { proto, properties: [] };
result = result.prototype;
result.properties = Reflect.ownKeys(proto);
proto = Object.getPrototypeOf(proto);
}
result.prototype = { proto }
}
return (walkPrototype(obj), r)
}
const result = getAllProperties(() => { });
可以清晰的看到不同的属性以及其来源。
获取属性以及其来源
获取一个对象的属性的值相关信息
- value: 属性值
- ownObject: 拥有该属性的对象
- ownHas: true 自身拥有该属性,false 原型链上
function getProperty(obj, property) {
let result, properties;
function walkPrototype(instance) {
if (instance == null) return;
properties = Reflect.ownKeys(instance);
if (properties.includes(property)) {
return result = {
value: instance[property],
ownObject: instance,
ownHas: true,
}
}
let proto = Object.getPrototypeOf(instance);
while (proto) {
properties = Reflect.ownKeys(proto);
if (proto && properties.includes(property)) {
return result = {
value: instance[property],
ownObject: instance,
ownHas: false,
}
}
proto = Object.getPrototypeOf(proto);
}
}
return (walkPrototype(obj), result)
}
const obj = {
toString() { },
}
const obj2 = {};
console.log(getProperty(obj, 'toString'));
console.log(getProperty(obj2, 'toString'));
迷惑行为一: 属性赋值
属性屏蔽, 这看起来似乎有些不合理。
var proto = {
name: "proto name"
};
var ins = Object.create(proto);
console.log(ins.name) // proto name
console.log(ins.__proto__.name); // proto name
ins.name = 'name';
console.log(ins.name) // name
console.log(ins.__proto__.name); // proto name
解释这个过程
下面是基本的调用链路:
O为对象,P为属性名, V为要设置的值
- Set ( O, P, V, Throw )
- 执行 O.[[Set]](P, V, O),所以下面的 Receiver 刚开始就是对象 O
- [[Set]] ( P, V, Receiver )
- 调用 OrdinarySet ( O, P, V, Receiver )
- OrdinarySet ( O, P, V, Receiver )
- 从对象O上取属性值P的描述符信息,然后调用 OrdinarySetWithOwnDescriptor
- OrdinarySetWithOwnDescriptor ( O, P, V, Receiver, ownDesc ) 核心
重点看中间部分加粗的注释。
小结
- 如果是数据属性,一定是在对象本身上进行操作,更改值或者新建属性。
- 如果是访问器属性,是可能在原型上操作的。
- 严格模式, 属性如果不可被更改,是会抛出错误的。
数据属性
var proto = {
name: "proto name"
};
var ins = Object.create(proto, {
name: {
value: "object name",
writable: true
}
});
// 更改的是ins对象name的值
ins.name = "name"
console.log(ins.name); // "name"
访问器属性
var proto = (function createProto() {
let name = 'proto name';
return Object.create(null, {
name: {
get() {
return name;
},
set(val) {
name = val;
}
}
})
})();
var ins = Object.create(proto);
ins.name = "new name";
console.log(ins.name) // "new name"
// ins 并未给自身新增属性 name
Object.hasOwnProperty.call(ins,"name") // false
严格模式
; (function init() {
"use strict"
var proto = {
name: "proto name"
};
var ins = Object.create(proto, {
name: {
value: "object name",
writable: false
}
});
// 严格模式更改只读属性
ins.name = "name"
// Uncaught TypeError: Cannot assign to read only property 'name' of object '#<Object>'
})();