# 5.对象小谈

可以将本文理解为《你不知道的javascript》(上卷)观后感,尽管在我读这本书之前看到很多人将此书奉上神坛,但还是不影响我每次翻阅此书都想直呼牛x!!

# 1. 属性描述符

前言:在ES5之前,JS没有提供可以直接检测属性特性的方法,比如判断属性是否是只读;从ES5开始,所有的属性都具备了属性描述符。

# 属性描述符的定义和配置:

如果要为一个属性定义它的属性描述符,可以通过Object.defineProperty:

let obj = {};
obj.a = 1;
Obeject.defineProperty(obj, 'a', {
  value: 2, // 相当于 obj.a = 2; 会覆盖上方赋值的 1
  writable: ture, // 默认为true,
  enumerable: true, // 默认为true
  configurable: true, // 默认为true
})
  • writable:该属性是否能被重新赋值;
  • enumerable:该属性能否被枚举;
  • configurable:该属性的属性描述符能否被重新配置和删除;

有了这几个描述符,我们就能对对象的属性进行更丰富的定义,例如对象常量

只需将属性描述符的writableconfigurable都设置为false,那么该属性就会变成一个无法更改且无法删除的常量。

# 属性描述符的细节和其他定义对象的方法:

除此之外,ES5还提供了一些直接定义整个对象的方法,例如Object.preventExtensions(objName),调用改方法能禁止一个对象添加新属性并且保留已有属性:

let obj = {
  a: 1,
}
Object.preventExtensions(obj);
obj.b = 1;
console.log(obj.b); // undefined

并且如果是在严格模式下,强行创建属性b会提示TypeError;

在此基础上,如果想将obj里的所有属性都更改为不可配置性,(不可配置表示的是该属性的属性描述符不可被修改(writable例外),并且也无法被删除。)那么就可以调用Object.seal(objName)。该方法的底层原理就是在Object.preventExtension(objName)的基础上,再将存在于obj上的所有属性的configurable都修改为false,不过与直接修改configurable不同的是,一个所有属性都为configurable:false的对象调用改方法不会报错:

let obj = {}
obj.a = 1;
Object.defineProperty(obj,'a',{
    value:2,
    configurable:false,
})

Object.seal(obj)
console.log(obj.a); // 2

而如果一个属性的configurable为false的话,再次配置该属性的属性配置符会提示TypeError:

let obj = {};
Object.defineProperty(obj, 'a', {
  value: 1,
  configurable: false,
  enumerable: true
})
Object.defineProperty(obj, 'a', {
  configurable: true,
  enumerable: false,
})
// TypeError: Cannot redefine property: a

不过一个属性例外,那就是writable。如果某个属性的configurable为false,那么它的writable仍可以修改为false,并且切可以将false赋值给writable(虽然赋值多次的操作没什么用)

let obj = {};
Object.defineProperty(obj, 'a', {
    value: 1,
    configurable: false,
})
Object.defineProperty(obj, 'a', {
    writable:false
})
Object.defineProperty(obj, 'a', {
    writable:false
})
Object.defineProperty(obj, 'a', {
    writable:false
})
console.log(obj.a); // 1

但如果将writable的值由false改为true,就会提示TypeError:

let obj = {};
Object.defineProperty(obj, 'a', {
    value: 1,
    configurable: false,
})
Object.defineProperty(obj, 'a', {
    writable:false
})
Object.defineProperty(obj, 'a', {
    writable:true
})
// TypeError: Cannot redefine property: a

绕了一圈,继续聊回对象,还有一个比Object.seal(objName)更高级别的方法,那就是Object.freeze(objName),该方法是在seal的基础上将存在于对象上的所有属性的writable都配置为false。

说完这么多,在这简单总结一下ES5三个定义对象的方法:

  • 禁止扩展——Object.preventExtensions(objName):保留对象的原有属性,禁止对象再新增属性。在普通模式下新增属性会被js无视,严格模式下会抛出TypeError。
  • 密封——Object.seal(objName):在Object.preventExtensions(objName)的基础上,将所有存在于对象上的属性配置为configurable:false。
  • 冻结——Object.freeze(objName) :在Object.seal(objName)的基础上,将所有存在于对象上的属性配置为writable:false

值得一提的是,上述的三个方法对对象的限制只局限于浅层属性:

let obj = {
    a: 1,
    b: {
        c: 2,
    }
}
Object.preventExtensions(obj);
obj.d = 3;
obj.b.e = 4;
console.log(obj.d, obj.b.e); // undefined 4   可以看到obj无法添加新的对象,而obj里的对象b可以继续添加新的对象
let b = {
    c: 2,
    e: 3
}
let obj = {
    a: 1,
    b
}
Object.seal(obj);
for (const objKey in b) {
    console.log(b[objKey]); // 2, 3
}
Object.defineProperty(b, 'c', {
    enumerable: false
})
for (const objKey in b) {
    console.log(b[objKey]); // 3
}
let b = {
    c: 2,
}
let obj = {
    a: 1,
    b
}
Object.freeze(obj);
console.log(b.c); // 2
b.c = 3;
console.log(b.c); // 3

# 2. 访问描述符

当我们对一个属性进行赋值和查找时,调用的都是对象内置默认的[[Put]] 和[[Get]]。接下来我们要介绍的是属性中的getter和setter,如果一个属性中至少有一个getter或setter,那么该属性就从属性描述符转变为访问描述符

访问描述符不同于属性描述符,value和writable都会被访问描述符忽视,其关心的只有getter、setter、enumerable和configurable:

let obj = {
    get a (){
        return 2
    }
}
obj.a = 3;
console.log(obj.a); // 2
console.log(Object.getOwnPropertyDescriptor(obj, 'a'));  // 访问描述符
// {
// get: [Function: get a],
// set: undefined,
//     enumerable: true,
//     configurable: true
// }
// 可以看到Object.getOwnPropertyDescriptor返回的对象中不再有value和writable,取而代之的是get和set。

如果说getter对应的是获取某个属性的值(类似于变量的RHS查询),那么setter对应的就是给某个属性赋值(类似变量的LHS查询):

let obj = {
    get a(){
        return this.val
    },
    set a(param){
        this.val = param * 2
    }
}
obj.a = 1;
console.log(obj.a); // 2  可以看到setter还可以自定义属性的赋值方法。

# 3. 存在性

如果访问对象的某个属性,返回一个undefined的值,那么要如何判断这个属性是否真实地存在于该对象上?

答案是可以通过in、obj.hasOwnProperty(objName)和Object.getOwnPropertyNames(obj):

let obj = {
    a: undefined
}
console.log('a' in obj); // ture
console.log('b' in obj); // false
console.log(obj.hasOwnProperty('a')); // true
console.log(obj.hasOwnProperty('b')); // false
console.log(Object.getOwnPropertyNames(obj));; // ['a']

先简单讲述一下这三个方法的异同:

  • objName in obj:返回一个布尔值,查找范围是整个对象及其原型链
  • obj.hasOwnProperty(objName):返回一个布尔值,查找范围仅限于对象上;
  • Object.getOwnPropertyNames(obj):返回一个数组,数组里每一项为对象的属性名。

# 4.枚举性

如果一个属性的enumerable为false,那我们能通过什么方法判断?

答案是obj.propertyIsEnumberable(objKey)和Object.keys(obj)

let obj = {
    a: 1
}
Object.defineProperty(obj,'b',{
    value:2,
    enumerable:false,
})
console.log(obj.propertyIsEnumerable('a')); // true
console.log(obj.propertyIsEnumerable('b')); // false
console.log(Object.keys(obj)); // ['a']

老规矩,看完例子我们在继续讲述一下这两个方法的异同:

  • obj.propertyIsEnumerable(objKey):传入一个obj对象上的属性名,返回一个布尔值表示这个属性是否为可枚举属性;
  • Object.keys(obj):返回一个数组,数组中的每一项为obj对象上可枚举的属性的属性名。

这两个方法只会查询存在于对象上的属性,不会涉及到原型链。

function Father() {
    this.a = 1;
}
Son.prototype = new Father();
function Son() {
    this.b = 2;
}
let son = new Son();
console.log('a' in son); // true
console.log(son.propertyIsEnumerable('a')); // false
console.log(Object.keys(son)); // ['b']