玩命加载中 . . .

defineProperty与数据劫持


初识defineProperty

Object.defineProperty(),是一个Object对象上的方法,用于创建对象上的属性。

基本语法

Object.defineProperty(object, prop, desp);
其中,object指明要添加属性的对象名,prop指出属性名,而desp则是一些属性描述符。

var phone = {name: 'nava7'};
Object.defineProperty(car, 'price', {
    value: 7000,
    configurable: false,
    writable: true,
    enumerable: true,
})

下面对属性描述符做一番简单介绍。

configurable

指定属性是否可以在第一次设置后被修改、是否可以被删除。
比如,如下代码在严格模式下,会报错;在一般模式下,修改、删除无效。

var person = {name: 'Yank'};
Object.defineProperty(person, 'age', {
    value: 20,
    configurable: false,
    writable: true,
    enumerable: false,
})
console.log(person);
delete person.age
console.log(person);
//删除无效,结果仍为 {name: 'Yank', age: 20}

enumerable

enumerable配置项用于描述属性是否可以被for in 或者Object.keys()的遍历(枚举)中。
当enumerable为false时,它不可以被上述两个语句遍历到。

var school = {name: 'XiWang Primary School'};
Object.defineProperty(school, 'age', {
    value: 55,
    configurable: true,
    enumerable: false,
})
Object.defineProperty(school, 'address', {
    value: 'Hubei Province',
    configurable: true,
    enumerable: true,
})
// 打印结果,只有address属性,而没有age属性
console.log(Object.keys(school));
//  ['name', 'address']

writable

比较好理解,writable为false时,表示该属性是只读的,不可以被写,且在严格模式下,写操作会报错;一般模式下,写操作失效。

get/set

get方法用于获取属性,而set方法用于设置属性。注意,设置了getter和setter之后,不可以再设置value和writable了,否则会报错。

一般地,使用第三方变量避免getter和setter反复无限调用:

// 错误写法
    var province = {name: 'Hubei'};
    Object.defineProperty(province, 'centerCity', {
        get: function() {
            console.log('调用了getter');
            return province.centerCity
        },
        set: function(val) {
            console.log('调用了setter');
            province.centerCity = val
        }
    })
    province.centerCity = 'Wuhan'
// 正确写法,另设了一个变量str
var province = {name: 'Hubei'};
var str = '';
Object.defineProperty(province, 'centerCity', {
    get: function() {
        console.log('调用了getter');
        return str
    },
    set: function(val) {
        console.log('调用了setter');
        console.log(val);   // Wuhan
        str = val
    }
})
province.centerCity = 'Wuhan'

使用Object.defineProperty()可以为对象新增属性,并且还可以细化地指定属性描述符,默认三个属性描述符(configurable、writable、enumerable)均为false;
而另一种直接的方式Object.prop即点运算符新增属性,则是默认三个属性描述符均为true。

属性描述符的分类

  • 数据描述符:value和writable
  • 存取描述符:get和set

注意,两种描述符不可以同时使用。

与Vue中的数据代理

通过一个对象代理对另一个对象中属性的操作(读/写),叫做数据代理。
下面举一个很简单的栗子:

var obj1 = {
    x: 1,
}
var obj2 = {
    y: 2,
}
Object.defineProperty(obj2, 'x', {
    configurable: true,
    enumerable: true,
    get() {
        console.log("getter of obj2 is working!");
        return obj1.x;
    },
    set(val) {
        console.log("setter of obj2 is working!");
        obj1.x = val;
    }
})

在这个栗子中,我们就实现了obj1和obj2之间的数据代理。我们要想读取/修改obj1.x,只需要读取/修改obj2.x就好,因为会触发其getter/setter。

下面进入正题:Vue中的数据代理究竟是什么?

在vue2中,我们在构建vue实例对象时传入各项配置参数:

<!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>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
    <div id="root">
        <h2>姓名:{{ name }}</h2>
        <h2>住址:{{ address }}</h2>
    </div>
</body>
<script>
    Vue.config.productionTip = false

    const vm = new Vue({
        el: '#root',
        data: {
        	name: '胡图图',
        	address: '翻斗花园'
    	}
    })
</script>
</html>

在浏览器中查看执行结果,在控制台输出vm,可以发现name、address都已经变成了vm实例自身的属性了,并且都有自己的getter/setter。
但是数据代理在vue中是如何体现的呢?首先我们需要知道的是,vm在拿到我们配置的属性name、address后,一定会将其保留下来。事实上,它们存在于vm的 _ data属性中了。也就是说,接下来,对 _ data中属性的修改,就相当于是对vm上属性的修改了(数据代理)。

为了能够验证这一点,我们需要想方法拿到原始的配置对象(options)上的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>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
    <div id="root">
        <h2>姓名:{{ name }}</h2>
        <h2>住址:{{ address }}</h2>
    </div>
</body>
<script>
    Vue.config.productionTip = false
    // 小技巧,将data配置项单独全局声明,方便在控制台直接获取
    var myData = {
        name: '胡图图',
        address: '翻斗花园'
    }

    const vm = new Vue({
        el: '#root',
        data: myData,
    })
    console.log(vm);
    console.log(myData);
    console.log(vm._data === myData);	// 验证data配置项就是vm的_data属性,故下文中不对两者(配置项data属性与vm上的_data属性)作刻意区分
</script>
</html>

现在在浏览器控制台作出如下操作:
vm.name = 'hututu'
我们就会发现姓名变成了‘hututu’。从数据代理角度来说,我们修改的是vm上的name属性,也就调用了其setter,从而改变了与之关联的data( _ data)上的name。

现在对整个数据代理过程梳理一下:

  1. 首先,我们书写了如下代码:
    const vm = new Vue({
    	el: '#root',
    	data: {
    		name: '胡图图',
    		address: '翻斗花园'
    	}
    })
  2. 在创建好的vm实例对象上,就会有一个准备好的 _ data属性,里面存放了options配置项中的data,大致如下:
    // vm
    {
    	...
    	_data: {
    		name: '胡图图',
    		address: '翻斗花园',
    	},
    	...
    }
  3. 到这里,还没有出现数据代理。但是,如果Vue仅仅到此为止的话,模板中的语法就应该这样书写了:
    <!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>Document</title>
        <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    </head>
    <body>
        <div id="root">
            <h2>姓名:{{ _data.name }}</h2>
            <h2>住址:{{ _data.address }}</h2>
        </div>
    </body>
    <script>
        Vue.config.productionTip = false
        var myData = {
            name: '胡图图',
            address: '翻斗花园'
        }
    
        const vm = new Vue({
            el: '#root',
            data: myData,
        })
        console.log(vm);
        console.log(myData);
        console.log(vm._data === myData);
    </script>
    </html>
    原因很简单,此时vm实例对象上还没有name、address属性,只有 _ data属性,通过它获取到数据。
  4. 数据代理,也就是再vm身上添加了相应的属性:
    // vm
    {
    	...
    	_data: {
    		name: '胡图图',
    		address: '翻斗花园',
    	},
    	...
    	name: ...
    	address: ...
    	...
    }
    而这里两者之间就是通过Object.defineProperty()建立的数据代理关系:
    通过这个方法,将data对象中的所有属性添加到vm上,同时为每一个属性指定一个getter/setter,在getter/setter内部去操作data对象中对应的属性。

可见,数据代理,大大地简化了代码书写,也使得更加方便地操作data中的数据了。

与数据劫持

数据劫持,就是对某一项数据进行访问、设置时,会触发相应的函数,并返回最后想要的结果(或返回属性值getter,或修改属性值setter)。当然,我们就可以在函数中另外做一些我们自己想要的操作了,这就是数据劫持。

在vue2中,就是使用defineProperty()实现的数据劫持,当数据被修改时,就会通知该数据被修改了。通俗地理解,就是通过defineProperty()修改了属性的getter/setter,并且在其中增添了监视功能。


文章作者: 鹿卿
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 鹿卿 !
评论
  目录