likes
comments
collection
share

通过Object.defineProperty()来分析Vue2中的数据代理

作者站长头像
站长
· 阅读数 21

Object.defineProperty

Object :对象 / define:定义/ Property:属性

Object.defineProperty 作用是什么?

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

语法:

 // obj: 要定义属性的对象
 // prop:要定义或修改的属性的名称
 // descriptor:要定义或修改的属性描述符
 Object.defineProperty(obj, prop, descriptor)

注意:该方法只能是在Object上调用 而不是在Object的实例上调用

Object.defineProperty()  ✔
a.defineProperty() ×

1.增加和修改

 1.修改属性
 let a = {
     name:"小明",
     age:18
 }
 let b = {
     name:"小红",
     age:12
 }
 Object.defineProperty(a,"name",{
     value:"小芳"
 })
 console.log(a);

通过Object.defineProperty()来分析Vue2中的数据代理

 2.增加属性
 let a = {
     name:"小明",
     age:18
 }
 let b = {
     name:"小红",
     age:12
 }
 Object.defineProperty(a,"hobby",{
     value:"上网"
 })
 console.log(a);

通过Object.defineProperty()来分析Vue2中的数据代理

2.属性描述符(decriptor)

对象里目前存在的属性描述符(descriptor)有两种主要形式:数据描述符存取描述符一个描述符只能是这两者其中之一;不能同时是两者。

这两种描述符都是对象。它们共享以下可选键值(默认值是指在使用 Object.defineProperty() 定义属性时的默认值:

configurable

当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变,同时该属性也能从对应的对象上被删除。 默认为 false。(configurable 特性表示对象的属性是否可以被删除,以及除 valuewritable 特性外的其他特性是否可以被修改。)

 先回忆一下如何删除对象上的一个属性:
 let a = {
     name:"小明",
     age:18
 }
 delete a.name;
 console.log(a); //只剩下 age:18

通过Object.defineProperty()来分析Vue2中的数据代理

 1.没有加configurable 去删除属性 发现删除失败
 let a = {
     name:"小明",
     age:18
 }
 Object.defineProperty(a,"hobby",{
   value:"上网"
 })
 delete a.hobby;
 console.log(a); 

通过Object.defineProperty()来分析Vue2中的数据代理

 2.加configurable后 去删除属性 发现删除成功
 let a = {
     name:"小明",
     age:18
 }
 Object.defineProperty(a,"hobby",{
   value:"上网",
   configurable:true
 })
 delete a.hobby;
 console.log(a); 

通过Object.defineProperty()来分析Vue2中的数据代理

enumerable

当且仅当该属性的 enumerable 键值为 true 时,该属性才会出现在对象的枚举属性中。 默认为 false。(enumerable 定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。)

 1.没有加enumerable 用for...in 去遍历对象 发现hobby属性遍历失败
 let a = {
     name:"小明",
     age:18
 }
 Object.defineProperty(a,"hobby",{
   value:"上网",
 })
 for(let key in a){
     console.log(a[key]);
 }
 console.log(a);

通过Object.defineProperty()来分析Vue2中的数据代理

 1.加enumerable后 用for...in 去遍历对象 发现hobby属性遍历成功
 let a = {
     name:"小明",
     age:18
 }
 Object.defineProperty(a,"hobby",{
   value:"上网",
   enumerable:true,
 })
 for(let key in a){
     console.log(a[key]);
 }
 console.log(a);

通过Object.defineProperty()来分析Vue2中的数据代理

数据描述符还具有以下可选键值:

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。 默认为 undefined

writable

当且仅当该属性的 writable 键值为 true 时,属性的值,也就是上面的 value,才能被赋值运算符 改变。 默认为 false (当 writable 属性设置为 false 时,该属性被称为“不可写的”。它不能被重新赋值。)

```
 1.当没有配置 writable时 改写属性失败
 let a = {
     name:"小明",
     age:18
 }
 Object.defineProperty(a,"hobby",{
     value:"上网"
 })
 a.hobby = "吃饭";
 console.log(a);

通过Object.defineProperty()来分析Vue2中的数据代理

 2.配置 writable后 改写属性成功
 let a = {
     name:"小明",
     age:18
 }
 Object.defineProperty(a,"hobby",{
     value:"上网",
     writable:true 
 })
 a.hobby = "吃饭";
 console.log(a);

通过Object.defineProperty()来分析Vue2中的数据代理

存取描述符还具有以下可选键值:

get

属性的 getter 函数(getter是啥意思 就是get 加后面函数体里面的内容 总称 getter),如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。 执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。 该函数的返回值会被用作属性的值。 默认为 undefined

```
 let a = {
     name:"小明",
     age:18
 }
 const aihao = "上网"
 Object.defineProperty(a, "hobby", {
     get(){
    //get 函数(getter)何时执行?—— 有人读取 a 对象的 hobby 属性时执行
        // getter是啥意思 就是get 加后面函数体里面的内容 总称 getter 
         //点击打印后的hobby后的 ... 就调用了get  输出了this 
    //get 函数(getter)中的this是谁? —— 当前对象(a)
     console.log("getter函数中的this==>"this);
     return aihao
     }
 })
 console.log(a);
 console.log(a.hobby);
```

通过Object.defineProperty()来分析Vue2中的数据代理

set

属性的 setter 函数(setter是啥意思 就是set 加后面函数体里面的内容 总称 setter),如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。 该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为 undefined

```
 let a = {
     name:"小明",
     age:18
 }
 let online = "上网"
 Object.defineProperty(a, "hobby", {
     get(){
     return aihao
     },
     // set函数(setter)何时会被调用? —— 有人修改 a 对象的 hobby 属性时执行
    // set函数(setter)中的 this 是谁? —— 当前对象(a)
     set(value){
     console.log('有人修改了person的age属性,值为:', value, this);
     online = value
     }
 })
 a.hobby = "吃饭";
 console.log(a);
 ​
```

通过Object.defineProperty()来分析Vue2中的数据代理

扩展知识一

有细心的小伙伴应该有发现 (descriptor属性描述符)中的writable,configurable,enumerable这些描述符中的可选键对Object实例上原本的属性 好像不奏效

 例如:删除a对象上的name属性 此时在Object.defineProperty中描述符
 configurable应该是默认值false 也就是无法删除 但结果却删除了 
 let a = {
     name:"小明",
     age:18
 }
 Object.defineProperty(a,"name",{
     value:"小芳"
 })
 delete a.name;
 console.log(a);

通过Object.defineProperty()来分析Vue2中的数据代理

 //原因是 object={属性:属性值} object.属性 = 属性值 object["属性"] = 属性值
 这种方式写出来的就相当于
 let a = {};
 a.age = 1;
 Object.defineProperty(a,"age",{
     value:1,
     writale:true,
     configurable:true,
     enumerable:true
 })
 ​
 //而Object.defineProperty(a, "age", { value : 1 });
 这种在a对象上新增age属性的方式就等同于
 Object.defineProperty(a, "age", {
   value: 1,
   writable: false,
   configurable: false,
   enumerable: false
 });

现在我们改下代码:

 let a = {
     name:"小明",
     age:18
 }
 Object.defineProperty(a,"name",{
     value:"小芳",
     configurable:false,
 })
 delete a.name;//name属性 删除失败
 console.log(a);

通过Object.defineProperty()来分析Vue2中的数据代理

扩展知识二

在最开始写描述符时有这么一句话 对象里目前存在的属性描述符(descriptor)有两种主要形式:数据描述符存取描述符一个描述符只能是这两者其中之一;不能同时是两者。

描述符的可选键值:

  • 共享可选键值:configurable ,enumerable
  • 数据描述符可选键值:value ,writable
  • 存取描述符可选键值:get ,set

如果一个描述符同时拥有 valuewritablegetset 键,则会产生一个异常。

 Object.defineProperty(a, "hobby", {
     enumerable:true,
     writable:true,
     get(){
     return aihao
     },
 })
 //报错 TypeError  类型错误:无效的属性描述符。
 //不能同时指定访问器和值或可写属性,在函数.defineProperty

通过Object.defineProperty()来分析Vue2中的数据代理

无用的小知识

我们在用Object.defineProperty()去添加一个新属性 发现属性打印出来的颜色跟原本的属性颜色不同

通过Object.defineProperty()来分析Vue2中的数据代理

当我们在描述项中加入 enumerable:true(可以被枚举) 发现新属性和原本属性的颜色变为一样了

通过Object.defineProperty()来分析Vue2中的数据代理

数据代理

什么是数据代理?

通过一个对象(a) 代理 对另一个对象(b) 中属性 (age) 的操作(读/写)

请看代码:

let a = { num: 10 }
let b = { age: 100 }

    Object.defineProperty(a, 'age', {
        get() {
            return b.age
        },

        set(newVal) {
            b.age = newVal
        }
    })
    console.log(a);
    console.log(b);

更详细的来说就是:

  1. a对象通过Object.defineProperty()增加了一个属性age,这个age属性是b对象身上的。
  2. 通过get来读取b.age的值,return返回给a.age,这样读取a.age就相当于读取b.age。
  3. 设置a.age的值时调用set方法,去改变b.age的值,这样b.age的值被修改。因为get,导致a.age的值也被"修改"。

有'印象'的理解:你可以把他想象成ntr(本人纯爱党),b身上的属性只能b来修改和读取,但b的朋友a现在也可以对b的属性进行修改和读取。

注意: a增加的属性age只是为了语义化,你可以给添加的属性起名叫a,b,c.因为get返回的值是b.age也就是你只要读取a.age 返回的是b对象身上age属性的值.

通过Object.defineProperty()来分析Vue2中的数据代理

Vue上的数据代理

就是通过vm实例对象来对_data中属性的操作(读/写)。 老规矩先上代码:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue数据代理</title>
</head>

<body>

  <div id="app">
    <p>动物类:{{ animal }}</p>
    <p>名称:{{ name }}</p>

    <p>{{ _data.animal }}</p>
    <p>{{ _data.name }}</p>
  </div>

  <script src="../vue.js"></script>
  <script>
    const vm = new Vue({
        el: '#app',
        data: {
            animal: '动物',
            name: '小猫'
        }
    })
    console.log(Vue);
    console.log(vm)
</script>
</body>
</html>

打印一下运行结果:发现 _data.name也可以访问到

通过Object.defineProperty()来分析Vue2中的数据代理

再打印一下vm这个实例对象 :

通过Object.defineProperty()来分析Vue2中的数据代理

发现vm身上有animal和name属性,_data身上也有animal和name属性,这又是为什么呢?

以下代码均为模拟 不是源码!!!

1.我们先要知道:在我们`new Vue(options)`时传入的那个`data`,被`Vue`放在了`vm`上,
名为`_data`。
  const vm = new Vue({//构造函数和普通函数的区别 就是看调用的时候有没有new
      el: '#app',
      data: {
        val: 100
      }
    })
 2.我们new Vue({})传入了一个对象 这个对象就是options,我们把data这个属性赋值给了
this._data 也就相当于 vm._data = options.data
    function Vue (options) {
    // 如何把 data 放到 vm 实例身上
    //在构造函数中,this 指实例
       this._data = options.data
     }
3.问题来了 我们在vm身上只加了_data属性,(_data属性也是个对象身上有val属性)没有在
vm身上加val属性 那么vm身上的val属性是怎么来的?
通过Object.defineProperty()

Object.defineProperty(vm, 'val', {
       get () {
       console.log('我被调用了~~~~')
       return _data.val
  },

set (newVal) {
       console.log('我也被调用了~~~~')
       _data.val = newVal
   }
 })
我们在vm身上通过Object.defineProperty()来添加val属性,这个value属性读取的值是
_data.val设置value属性 设置的是_data.val

相信大家看到这里 稍微有点感觉,那趁热打铁再来:

也不是源码,更深的模拟
 function Vue(options) {
      // 构造函数里面的 this 代表的是 实例
      this._data = options.data || {}
      // 该方法的作用:将 _data 中的成员转换成 getter 和 setter,并注入到 Vue 实例中
      this._proxyData(this._data)//我们把 Object.defineProperty封装在一个方法里
    }

    // 将 data 中的数据转换成 getter 和 setter 注入到 vue 实例 vm 身上
    Vue.prototype._proxyData = function (_data) {//方法都写在原型对象上
    //Object.keys和for...in差不多 只不过返回的是对象身上的每个属性名组成的数组
      // 遍历 data 中的所有的属性,将 data 中每个属性,都放到 vm 上
      Object.keys(_data).forEach(key => {//属性名组成的数组用forEach数组方法进行遍历
        // 把 data 中的数据都注入到实例中
        Object.defineProperty(this, key, {//key就是_data对象中的每一个属性名
          enumerable: true,//可枚举
          configurable: true,//可删除
          get() {
            return _data[key]//返回_data对象属性名为[key]的值
          },
          set(newValue) {//修改传入的值
            if (newValue === _data[key]) return//如果修改的值和原本的值一样直接返回,提高性能
            _data[key] = newValue//修改_data对象属性名为[key]的值
          }
        })
      })
    }


    const vm = new Vue({
      data: {
        name: '小明',
        age: 10
      }
    })

    console.log(vm)

Vue中数据代理的好处

那么我们为什么要用数据代理,绕来绕去直接_data不好么?

还真不好 我们要用那么多数据每个数据都得_data.属性名来读取/修改,直接属性名的方式可以更加方便的操作data中的数据。

{{_data.name}} {{name}} 你会选择使用哪种?