前言
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 设置上面的属性给对象添加属性。
虽然工程师通过是通过 .或者 []的方式设置属性,其底层逻辑是一致的。
            
            
              javascript
              
              
            
          
          var obj = {};
obj.name = 'name';
// 等同于
Object.defineProperty(obj, {
  value: 'name',           // 值
  writable: true,          // 可以改写值
  configurable: true,      // 可以更改配置
  enumerable: true         // 可以被枚举
});
        需要注意的是writable, configurable, enumerable的默认值都是false。
            
            
              javascript
              
              
            
          
          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 支持降级。
            
            
              javascript
              
              
            
          
          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有例外) | ✘ | ✘ | 
通用方法
上面的三个方法,只能对对象的一级属性做到控制,
- 如果多级属性呢?
 - 原型上的属性呢?
 
            
            
              javascript
              
              
            
          
          var obj = {
  a: {
    b: "b"
  }
}
Object.freeze(obj);
// 二级属性赋值成功
obj.a.b = 'bb'
        封装一个deepFreeze
            
            
              javascript
              
              
            
          
          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);
        }
    })
}
        测试一下
            
            
              javascript
              
              
            
          
          var obj = {
  a: {
    b: "b"
  }
}
deepFreeze(obj);
obj.a.b = 'bb'
// 二级属性赋值不成功
console.log(obj.a.b);   // "b"
        这样写三个方法,是不是逻辑有点浪费呢? 借用一下函数式编程的高阶函数的思想,抽象一下。
            
            
              javascript
              
              
            
          
          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
 
 - 遍历原型
 
            
            
              javascript
              
              
            
          
          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去重。
            
            
              javascript
              
              
            
          
          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个。

获取所有属性树
            
            
              javascript
              
              
            
          
          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)
}
        
            
            
              javascript
              
              
            
          
          const result = getAllProperties(() => { });
        可以清晰的看到不同的属性以及其来源。

获取属性以及其来源
获取一个对象的属性的值相关信息
- value: 属性值
 - ownObject: 拥有该属性的对象
 - ownHas: true 自身拥有该属性,false 原型链上
 
            
            
              javascript
              
              
            
          
          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)
}
        
            
            
              javascript
              
              
            
          
          const obj = {
    toString() { },
}
const obj2 = {};
console.log(getProperty(obj, 'toString'));
console.log(getProperty(obj2, 'toString'));
        
迷惑行为一: 属性赋值
属性屏蔽, 这看起来似乎有些不合理。
            
            
              javascript
              
              
            
          
          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 ) 核心
重点看中间部分加粗的注释。 

小结
- 如果是数据属性,一定是在对象本身上进行操作,更改值或者新建属性。
 - 如果是访问器属性,是可能在原型上操作的。
 - 严格模式, 属性如果不可被更改,是会抛出错误的。
 
数据属性
            
            
              javascript
              
              
            
          
          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"
        访问器属性
            
            
              javascript
              
              
            
          
          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
        严格模式
            
            
              javascript
              
              
            
          
          ; (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>'
})();