ECMAScript 杂谈:对象属性照妖镜

49 阅读6分钟

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

属性描述符

属性名属性类型默认值描述
value数据属性ECMAScript 语言值undefined属性值
writable数据属性true, falsefalsetrue: 允许改写值
false: 不允许改写值
get访问器属性undefined, Functionundefined取值函数
set访问器属性undefined, Functionundefined设置值的函数
configurable数据属性 或者访问器属性true, falsefalsetrue: 允许更改属性配置
false: 不允许更改属性配置则尝试删除该属性,将其从数据属性更改为访问属性,或从访问属性更改为数据属性,或对其属性进行任何更改(替换现有的[[ Value ]]或将[[ Writable ]]设置为 false 除外) 都将失败
enumerable数据属性 或者访问器属性true, falsefalsetrue: 可以被枚举
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的真正含义

本意是不再允许再配置该属性, 下面两种情况除外

  1. 替换现有的value
  2. 将writeable设置为 false 除外

第一条 其实也还好理解, value 是由 writeable控制的,如果其值为 true, 允许更改值合情合理。

第二条 writeable 支持降级。

const obj = {};

Object.defineProperty(obj, "name", {
  writable: true
});

// 设置值,成功
obj.name = 'name';

// 尝试修改描述符writable信息, 成功
Object.defineProperty(obj, "name", {
  writable: false
});

那反过来,哪些情况会被禁止或者失败

  1. 尝试删除该属性
  2. 将其从数据属性更改为访问属性
  3. 或从访问属性更改为数据属性
  4. 或对其属性进行任何更改
    1. configurable
    2. enumerable
    3. get
    4. 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等呢?

属性来自何处

属性的来源

  1. 静态属性
  2. 实例属性
  3. 原型属性

静态属性和另外两种本质还是有区别的,重点就是 实例属性和原型属性。 当你使用某个属性时, 两个问题?

  1. 有没有这个属性
  2. 这个属性来自哪里

获取所有的属性

方法名普通属性不可枚举属性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为要设置的值

小结

  1. 如果是数据属性,一定是在对象本身上进行操作,更改值或者新建属性。
  2. 如果是访问器属性,是可能在原型上操作的。
  3. 严格模式, 属性如果不可被更改,是会抛出错误的。

数据属性

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>'
})();