JavaScript对象的属性,看起来是一个很简单的问题,但是往细了想有很多细节需要注意。比如,可枚举属性与不可枚举属性的区别是什么,当我们打印一个对象的时候,打印的是对象本身的属性,还是包含继承的属性?我们去遍历一个对象的时候,多种方法之间又有什么区别?在这里我们做一个全面的分析和总结。

属性的设置

在搞清楚对象的属性之前,先看看我们一般如何设置一个对象的属性。总的来说有两种方法:

  1. 直接访问对象属性进行设置,这是最常用的

    // 1.直接设置属性,无论通过点还是中括号访问,效果都是一样的
    var obj = {};
    obj.a = 1;
    obj['b'] = 2;
    // 2. 在构造函数中设置,本质上也是通过a访问。
    function Person (a) {
        this.age = a;
    }
    var a = new Person(11)
  2. 通过Object.defineProperty()方法。Object.defineProperty提供更强大的功能,他通过属性描述符来对属性进行更大的配置。属性描述符又分为数据描述符存取描述符两类:

    var obj = {};
    Object.defineProperty(obj, a, {
      /***数据描述符***/
      // 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
      configurable: true, 
      // 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
      enumerable: false,
      // 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined
      value: 1,
      // 当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false
      writable: true,
      /***存取描述符***/
      // 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行
      get: function() {
        console.log('获取属性a的值')
        return 1;
      },
      // 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。
      set: function(val) {
        console.log('设置属性a的值')
        a = val;// 这里直接写属性名就行
      }
    })

属性的分类

我们从3个维度来对JavaScript属性进行分类,这是后面进行分析的基础。

可枚举与不可枚举

js对象的属性分为可枚举属性和不可枚举属性。通过上面说的第一种方法设置的属性都是可枚举属性,在第二种方法中,如果将数据描述符enumerable设置为false,则属性时不可枚举。对于大多数情况,我们使用的都是可枚举属性。

如果你去搜索可枚举和不可枚举属性的区别是什么,很多博客都是说会影响for...in等方法的行为。其实说白了,这些都是可枚举属性和不可枚举属性在表现上的区别,是果,不是因。关于具体的区别,我们将在下面介绍API时具体分析,在这里我们只用记住,可枚举与不可枚举是怎么来的就行。

继承与自身

js对象是一大特点就是可继承性,所以我们常常提到原型链。如果属性设置在对象本身,我们就说这是自身属性,如果来自于原型链,我们就说这是继承属性

function Parent() {
  this.p1 = 'p1';
}
var parent = new Parent();

function Son () {
  this.s1 = 's1';
}
Son.prototype = parent;

var son = new Son();
console.log(son.p1) // 继承属性
console.log(son.s1) // 自身属性

Symbol属性与非Symbol属性

ES6引入了Symbol,于是又给属性分类多添了一种。

var skey = Symbol.for('symbolKey');
function Son() {
    this.s1 = 's1';// s1是非Symbol属性
    this[skey] = 's3';// skey是Symbol属性
}

Symbol属性与非Symbol属性只是key值的不同而已。设置Symbol属性往往是为了更加安全,不轻易暴露出来。所以在访问上,一般来说Symbol属性隐藏更深。

8种分类与4种分类

从3个维度分类,每个维度有2种分类,所以排列组合一下,我们一共将js属性分为8类:

  • 自身可枚举Symbol属性
  • 自身可枚举非Symbol属性
  • 自身不可枚举Symbol属性
  • 自身不可枚举非Symbol属性
  • 继承可枚举Symbol属性
  • 继承可枚举非Symbol属性
  • 继承不可枚举Symbol属性
  • 继承不可枚举非Symbol属性

很复杂,有点多是不是。其实,Symbol属性只是key值的类型为Symbol而已,在一般情况下时作为私有属性,隐藏的更深,Symbol属性一般不会访问到。所以,为了简单起见,我们不单独考虑这种,默认都是访问不到的,只有在能访问到的时候才单独说明。所以,我们还是按照4种分类:

  • 自身可枚举属性
  • 自身不可枚举属性
  • 继承可枚举属性
  • 继承不可枚举属性

我们构造一个例子,包含上面这8种属性。下面的所有分析都在这个例子的基础上:

var skeyEnumerable = Symbol.for('skeyEnumerable');
var skeyNoEnumerable = Symbol.for('skeyNoEnumerable');
var pkeyEnumerable = Symbol.for('pkeyEnumerable');
var pkeyNoEnumerable = Symbol.for('pkeyNoEnumerable');
function Parent() {
    this.p1 = 'p1';
    this[pkeyEnumerable] = 'pSymbolEnumerable';
}
var parent = new Parent();

Object.defineProperty(parent, 'p2', {
    enumerable: false,
    value: 'p2'
});

Object.defineProperty(parent, pkeyNoEnumerable, {
    enumerable: false,
    value: 'pSymbolNoEnumerable'
});

function Son() {
    this.s1 = 's1';
    this[skeyEnumerable] = 'sSymbolEnumerable';
}

Son.prototype = parent;

var son = new Son();

Object.defineProperty(son, 's2', {
    enumerable: false,
    value: 's2'
});

Object.defineProperty(son, skeyNoEnumerable, {
    enumerable: false,
    value: 'sSymbolNoEnumerable'
});

在这个例子中:

  • s1是自身可枚举属性
  • s2是自身不可枚举属性
  • p1是继承可枚举属性
  • p2是继承不可枚举属性

除此之外,还有4个Symbol属性:

  • skeyEnumerable是自身可枚举Symbol属性
  • skeyNoEnumerable是自身不可枚举Symbol属性
  • pkeyEnumerable是继承可枚举Symbol属性
  • pSymbolNoEnumerable是继承不可枚举Symbole属性

为了简单起见,我们只考虑上面4种。下面的4中Symbol属性,只有在会展示的地方,才会特殊说明。

如何区分分类

根据上面说的两种分类,是解释来源,这里介绍如何鉴定区分。

区分是否可枚举

通过实例方法propertyIsEnumerable来区分是否可枚举:

console.log(son.propertyIsEnumerable('s1'));// true
console.log(son.propertyIsEnumerable('s2'));// false
console.log(son.propertyIsEnumerable('p1'));// false
console.log(son.propertyIsEnumerable('p2'));// false

根据上面例子中的设置,p1是继承的可枚举属性,但是这里输出false。有点小遗憾,这个propertyIsEnumerable方法只是对自身的属性才有用,对继承的属性统一输出为false,无论是可枚举还是不可枚举。

区分是否继承

通过实例方法hasOwnProperty来区分是否为继承属性。

console.log(son.hasOwnProperty('s1'));// true
console.log(son.hasOwnProperty('s2'));// true
console.log(son.hasOwnProperty('p1'));// false
console.log(son.hasOwnProperty('p2'));// false

打印对象

直接打印

如果我们直接将son对象打印出来,我们会得到什么?这4中属性里面,有哪些属性会被打印访问到呢?

console.log(son);
// 在Chrome中,打印的结果是Son {s1: "s1", s2: "s2"}
// 在node中,打印的结果是Parent { s1: 's1' }

可以看出打印出来的结果并不一致。这是因为,我们常用的console.log函数,并不是一个标准的方法,每个浏览器或引擎都有自己的一套实现方法,也并不是简单地调用对象的toString方法(如果是toString应该返回[object Object]),比如在Chrome中会打印可枚举和不可枚举,而在node中只会打印可枚举。需要提到的是,son.p1son.p2是可以访问,只是这里没有打印出来。

至于Symbol属性,由于个平台实现不一致,有的能打印出来,有的不能,不多纠结。

JSON.stringify方法

虽然console.log跨引擎实现是不一样的,但也不是很重要,毕竟这只是一个debug用的方法。换一种更通用的方法,通过JSON.stringfy序列化成字符换,最后的结果又是怎样呢

console.log(JSON.stringify(son)); // {"s1":"s1"}

可以看出,JSON.stringify会将所有的自身可枚举属性序列化成字符串

遍历对象属性

前面介绍这么多,都是基础。在实际应用中,我们最多的还是去遍历一个对象。接下来就把所有遍历对象属性的方法做一个分析比较。

遍历对象的所有属性,就要先得到该对象的所有属性名。只有先得到所有的属性名,才能遍历每个属性的值。JS提供了很多API供我们得到所有的属性名,往往是放在一个数组中,然后遍历数组中的每一个key,访问对象对应的key值。

1. Object.keys()

Object.keys(son).forEach(key => {
    console.log(son[key]);// s1
});

该方法返回的是对象的所有自身可枚举属性。这跟JSON.stringify其实是一样的。

2. Object.getOwnPropertyNames()

Object.getOwnPropertyNames(son).forEach(key => {
    console.log(son[key]);// s1 s2
});

该方法返回的是对象自身的所有属性,包含可枚举属性和不可枚举属性。

3. Object.getOwnPropertySymbols()

前面提到,一般Symbol属性都是私有的,隐藏的。而通过该方法可以访问到对象自身的所有Symbol属性,包括自身可枚举的Symbol属性和自身不可枚举Symbol属性。

Object.getOwnPropertySymbols(son).forEach(key => {
    console.log(son[key]); //sSymbolEnumerable sSymbolNoEnumerable
})

4. Reflect.ownKeys()

Reflect.ownKeys(son).forEach(key => {
    console.log(son[key]); // sSymbolEnumerable sSymbolNoEnumerable s1 s2
})

这个方法就更强了,返回的是对象自身的所有属性,无论是否可枚举,也无论是否为Symbol属性,即包含:

  • 自身可枚举Symbol属性
  • 自身可枚举非Symbol属性
  • 自身不可枚举Symbol属性
  • 自身不可枚举非Symbol属性

到目前为止,介绍的几种方法都是访问对象自身的属性。Object.keys()是最弱的,只能访问自身可枚举属性,而Object.getOwnPropertyNames()就强一些,能访问自身可枚举和不可枚举属性;当然这些都不包含Symbol属性,需要使用Object.getOwnPropertySymbols来访问。可以说,Obejct.getOwnPropertySymbolsObject.getOwnPropertyNames是并列关系,而Reflec.ownKeys=Object.getOwnPropertyNames + Object.getOwnPropertySymbols。如下图所示:

5. for…in

for (var key in son) {
    console.log(son[key]);// s1 p1
}

该方法访问的是对象的自身和继承的可枚举属性。

如果想用for…in来访问对象自身的可枚举属性,应该怎么弄呢。结合前面说的hasOwnProperty方法,做一个区分即可:

for (var key in son) {
    if (son.hasOwnProperty(key)) {
        console.log(son[key]); // s1
    }
}

小结

为了方便区分记忆,我是按照从弱到强的方法来介绍各种方法的。除了Object.getOwnPropertySymbols和Reflect.ownKeys,其他方法都是访问不到Symbol属性的,这也是对于Symbole属性不多加区分的原因。大多数情况下的遍历,都是相对于对象自身的属性而言的。所以可以看到,这里提供的方法大多也都是针对对象自身属性的。如果像遍历继承属性,for...in的能力其实很弱。最简单的方法就是用前面的4种方法直接访问Son.prototype或者Object.getPrototypeOf(son)即可,比如:

Reflect.ownKeys(Object.getPrototypeOf(son)).forEach(key => {
    console.log(son[key]); // p1 p2 pSymbolEnumerable pSymbolNoEnumerable
})

复制对象时的属性

ES6提供的Object.assign方法,又是如何复制属性的呢。

扩展运算符

console.log(Reflect.ownKeys({...son}));// [ 's1', Symbol(skeyEnumerable) ]

复制的对象自身的可枚举属性,包含Symbol属性和非Symbol属性

Object.assign

console.log(Reflect.ownKeys(Object.assign({}, son)));// [ 's1', Symbol(skeyEnumerable) ]

复制的对象自身的可枚举属性,包含Symbol属性和非Symbol属性

小结

按照优先级,最高的应是自身可枚举属性,其次是自身不可枚举属性,再往后是Symbol属性。对于继承属性最不重要,因为继承属性就是prototype自身的属性而已。


程序员不止程序猿
177 声望7 粉丝