作为一名前端开发人员,我们或许都听说过Vue 2.0中实现双向数据绑定采用了Object.defineProperty
,我相信有很多小伙伴们和我一样有疑问,这个神奇的东西是怎么做到的呢?在介绍Object.defineProperty
之前,我们先来认识一下什么是属性类型。
ECMA-262描述道:
ECMA-262 第5 版在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特征。
ECMA-262 定义这些特性是为了实现JavaScript 引擎用的,因此在JavaScript 中不能直接访问它们。
也就是说,属性类型是属性内部的特性,不能直接访问,但是我们可以一些手段操作属性类型来改变他们的属性特征,呈现出一个定制化的对象。属性类型只有两种:数据属性和访问器属性,下面我们来具体看看它们的用法和区别吧。
数据属性包含一个数据值的位置。在这个位置可以读取和写入值
数据属性有四个描述其行为的特性,放在了两对方括号中,表示是不可直接访问的。
[[Configurable]]
:可以通过delete删除的属性,默认为true
[[Enumerable]]
:可枚举的属性,也就是可以通过for-in循环遍历出来的属性,默认true
[[Writable]]
:可写的属性,也就是可以修改属性,默认true
[[Value]]
:属性的值。默认为undefined
那怎么来验证呢,一般来说我们创建对象是没法看到这些属性的,这个时候我们的主角Object.defineProperty
就可以上场了,它是ES5中提供的一个很强大的方法,下面是MDN的描述:
Object.defineProperty
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
方法接受三个参数:属性所在的对象、属性名和一个描述符对象(descriptor
)。
下面是一个简单的例子:
var obj = {}
Object.defineProperty(obj, 'name', {
configurable: false,
writable: true,
enumerable: true,
value: 'Tom'
})
delete obj.name // false, 查找 configurable
console.log(obj.name) // Tome, 查找 value
obj.name = 'Amy' // 查找 writable
console.log(obj.name) // Amy
for (var key in obj) {
console.log(key + '================' + obj[key]) // name================Amy, 查找enumerable
}
注意:
Object.defineProperty()
方法时,如果不指定,configurable
、 enumerable
和writable
特性的默认值都是 false 。configurable
一旦被指定为false
,调用Object.defineProperty()
方法修改除 writable
之外的特性,都会导致错误。访问器属性不包含数据值;它们包含一对
getter
和setter
函数(不过,这两个函数都不是必需的,除非是严格模式)。在读取访问器属性时,会调用getter
函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter 函数并传入新值,这个函数负责决定如何处理数据。
访问器属性也有4个特性:
[[Configurable]]
:可以通过delete删除的属性,默认为true
[[Enumerable]]
:可枚举的属性,也就是可以通过for-in循环遍历出来的属性,默认true
[[Set]]
:写入属性调用的函数,默认undefined
[[Get]]
:读取属性调用的函数,默认为undefined
对于数据属性,访问器属性没有了writable
、value
,相反被替换成了set
和get
,虽然都有获取和读取数据的作用,但后者把读取数据和写入数据的放到了一个方法内,这样可以定制化数据读取和写入的行为了,这也是实现数据劫持一个很重要的特征。
var obj = {
_name: 'Tom'
}
Object.defineProperty(obj, 'name', {
get () {
return 'I am ' + this._name;
},
set (newValue) {
this._name = newValue;
}
});
console.log(obj.name); // I am Tom, 调用get方法
obj.name = 'Amy' // 调用set方法
console.log(obj.name); // I am Amy
注意:
set
和get
缺一不可,否则会报错set
和get
的浏览器中(比如IE8
)可以使用非标准的__defineGetter__
or__defineSetter__
来替代。当我们需要定制一个对象中多个属性的行为的时候,Object.defineProperty
的孪生兄弟Object.defineProperties
就派上用场了,用法很简单:
var obj = {}
Object.defineProperties(obj, {
_name: {
value: 'Tom',
writable: true
},
age: {
value: 18,
enumerable: false,
},
name: {
get() {
return 'I am ' + this._name
},
set(newValue) {
this._name = newValue
},
},
})
我们如果想要查看对象某个属性的特性描述,那么使用Object.getOwnPropertyDescriptor
方法,可以取得给定属性的描述符。
这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get 和set;如果是数据属性,这个对象的属性有configurable、enumerable、writable 和value。
同样地,它也有一个孪生兄弟Object.getOwnPropertyDescriptors
可以某个对象所有属性的描述符。下面是一个例子:
// 获取某个属性的描述符
var result1 = Object.getOwnPropertyDescriptor(obj, 'name')
// 获取所有属性的描述符
var result1 = Object.getOwnPropertyDescriptors(obj)
有了前面知识的铺垫,下面我们来利用Object.defineProperty
实现简单的Vue双向数据绑定吧。
首先我们定义一个DOM模板:
<div id="app">
<input type="text" v-model="username"/>
<p>{{ username }}p>
div>
然后我们按照Vue里面的结构来写JS代码:
let el = document.getElementById('app') // 模拟el
let data = { _username: 'Tom' }; // 模拟data对象
let template = el.innerHTML // 获取el里面的字符串模板,方便以后替换
接下来我们需要一个render
函数来渲染模板:
function render() {
// 初始渲染先替换DOM里面的模板
el.innerHTML = template.replace(/{{\s+[\w.]+\s+}}/g, (str) => {
str = str.slice(2, str.length - 2).trim()
return data[str]
})
//双向绑定
// 获取所有带`v-model`属性的input元素
Array.from(el.getElementsByTagName('input'))
.filter(element => element.getAttribute('v-model'))
.forEach(input => {
// 为每个input元素进行双向绑定
let bindData = input.getAttribute('v-model');
// 监听input时间,动态把input数据传入data
input.addEventListener('input', function () {
data[bindData] = this.value;
}, false)
// 将data的数据绑定到DOM
input.value = data[bindData];
});
}
有了render函数就很好说了,接下来就是使用Object.defineProperty
来定义数据的行为了:
Object.defineProperty(data, 'username', {
enumerable: true,
configurable: true,
get() {
return this._username;
},
set(value) {
this._username = value;
render()
}
});
最后实现的效果如下:
这篇文章前面讲述了数据属性的种类和区别,以及它们的用法。后面利用访问器属性的特性来实现了一个简单版的Vue双向绑定的效果。虽然我们平时很少用这个属性,但是对于我们来理解这门语言有很大的帮助。最后,这篇文章难免会有一些错误,欢迎大家指正和批评。
【1】《JavaScript高级程序设计》(第3版)