1

前言

初级前端开发工程师一般很少会关注对象的属性类型,因为简单的任务几乎不需要使用到,但是随着业务的复杂度提高或者需要自己造轮子的时候,了解对象的属性类型很有必要。

下面列举几种使用场景:

1.不想被遍历的属性

大多数的情况,在循环遍历的时候,我们只想关心自身的属性,而在对象和数组中会有一些自带的内部属性,如果这些属性都能够遍历出来,不仅对编码造成麻烦,对性能也是一种浪费。
所以在数据属性中出现了 enumerable,它可以控制在使用 for-inObject.keys() 等方法时是否返回该属性。

enumerable: 能否通过 for-in/Object.key()等方法返回属性,默认为true。

  • 注意:for-in能循环继承属性,最好使用Object.key()替代

默认不能被遍历的内部属性

如数组中自带length属性无法被遍历

var arr = ['a', 'b']
Object.getOwnPropertyDescriptor(arr, 'length')
// {
//     configurable: false
//     enumerable: false  // 可枚举性为false
//     value: 2
//     writable: true
// }

Object.keys(arr) // 仅输出下标属性[0, 1],未输出length属性

定义一个不想被遍历的属性

Object.defineProperty(arr, '2', {
  enumerable: true,
  value: 'c'
}); 
Object.getOwnPropertyDescriptor(arr, '2')
// {
//     configurable: false
//     enumerable: false  // 可枚举性为false
//     value: 2
//     writable: true
// }

Object.keys(arr) // 仅输出下标属性[0, 1],未输出下标2

2. 不想被修改的属性

一般设置的属性值都是能被外界修改的,但如果是开发插件或者组件的情况下,为了避免影响功能运行,有些属性是不希望被外界修改和删除,于是就出现了configurable属性。

不许动我的属性

var config = {
    host: 'http://shuairuoyang.cn'
}
Object.defineProperty(config, 'author', {
    value: '衰弱羊',
    configurable: false // 默认是false
})

delete config.author; // false 删除失败
delete config.host; // true 删除成功
console.log(config) // {author: '衰弱羊'} 

我不动你的属性

以vue源码举个栗子。

  • vue版本:2.6.11

defineReactive是vue响应式的核心方法,它对obj的key属性定义了get和set 操作,从而实现了数据的监听和响应。如果被监听的对象obj的key属性不可被配置,则函数直接返回,数据无法被监听。

// vue/src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return // 属性不可配置,直接返回,后面就不执行了
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true, // 如果上面不对property.configurable === false的情况 进行判断,这里就会报错
    get: function reactiveGetter () {
      // ...
    },
    set: function reactiveSetter (newVal) {
      // ...
    }
  })
}

举个栗子:在 data 中设置一个不响应的属性。

<template>
  <div>
    <p>响应的属性-age:{{responsizeAge}}</p>
    <p>不响应的属性-name:{{responsizeName}}</p>
    <button type="button" @click="chagne">点击改变</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      person: (function() {
        const obj = {
          age: 18 // 响应属性
        }
        Object.defineProperty(obj, 'name', {
          configurable: false, // 不想响应了
          writable: true,
          value: 'tony'
        })
        return obj
      }())
    }
  },
  watch: {
    'person.age': function(newVal, oldVal) {
      console.log('age watch>>>>', newVal, oldVal)
    },
    'person.name': function(newVal, oldVal) {
      console.log('name watch>>>', newVal, oldVal) // 没有响应,这里不会输出
    }
  },
  computed: {
    responsizeAge() {
      return this.person.age + 100
    },
    responsizeName() {
      return 'china--' + this.person.name
    }
  },
  methods: {
    chagne() {
      this.person.name = 'jack-' + Math.random()
      this.person.age++
      console.log('this.person>>>', this.person) // 直接查看对象数据有更新,但界面和其他地方并没有响应
      console.log(Object.getOwnPropertyDescriptors(this.person))
    }
  }
}
</script>
  • age:响应属性,改变后界面会同步修改,也能被watch和computed监听到
  • name:configurable为false,非响应属性,修改后其他地方并不会同步修改

看看person对象属性的描述对象

总结:如果以后遇到很大的对象,而有部分内容不想被监听,可是使用这种方式处理,减少被监听元素的个数,从而提高vue的运行性能。

3. 动态的属性值

一个属性值可以是一个简单的值,也可以是一个或者两个方法替代,它们就是getter和setter。

=赋值同时也可以影响其他属性

var p = {
  x: 1.0,
  y: 1.0,

  get r() {
    return Math.sqrt(this.x * this.x + this.y * this.y)
  },
  set r(newValue) {
    var oldValue = Math.sqrt(this.x * this.x + this.y * this.y)
    var ratio = newValue/oldValue;
    this.x = ratio
    this.y = ratio
  }
}

console.log(p) // {r: 1.4142135623730951, x:1, y:1}

p.r = 100 // 调用r的set方法

console.log(p) // 三个属性值都变了
 // {r: 99.99999999999999, x:70.71067811865474, y:70.71067811865474}

举个栗子

再以vue源码里的defineReactive()方法举个栗子

  • vue版本:2.6.11

在data设置属性的时候,可以给属性定义setter方法,如果有则调用,没有则直接赋值。
如果被重新赋值,则调用dep.notify()方法去通知所有的watcher(视图、computed、watch等)

// vue/src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  // ...

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // ...
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal 
      }
      childOb = !shallow && observe(newVal)
      dep.notify() // 消息订阅器:如果数据被重新赋值,由它通知所有的watcher
    }
  })
}

从上面的源码中可以看到,如果属性值自己设置了get和set方法,会优先调用自定义的set和get,这样就可以实现一些特定场景。

<script>
export default {
    data() {
        return {
            person: {
                realAge: 18, // 真正的年龄值
                get age() {
                    return 18
                },
                set age(newValue) {
                    this.realAge = newValue + 18
                }
            }
        };
    },
    mounted() {
        this.person.age = 30 // 无论设置成多少,都只会返回18
        console.log(this.person) // {age: 18, realAge: 48}
    },
};
</script>

4. 高级方法

1. 创建一个对象常量

结合writeble: false 和 configurable:false 就可以创建一个真正的常量属性(不可修改,重定义或删除)

var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
 value: 42,
 writable: false,
 configurable: false
} );

2. 禁止拓展

如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(..)

var myObject = { 
  a:2
};
Object.preventExtensions( myObject );
myObject.b = 3; 
myObject.b; // undefined

3. 密封

Object.seal(..) 会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..) 并把所有现有属性标记为 configurable:false。所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。

4. 冻结

Object.freeze(..) 会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..) 并把所有“数据访问”属性标记为writable:false,这样就无法修改它们的值。这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意
直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。

你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(..),然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(..)。但是一定要小心,因为这样做有可能会在无意中冻结其他(共享)对象。


YanniLi
56 声望4 粉丝