在JavaScript中,对象是属性的无序集合,通过对JS属性的理解可以更好地了解JS对象。
属性的构成与分类
在JS中,对像的属性是由名字(key)和一组特性(attribute)构成,其中:
- key是一个字符串(包括空字符串),
- attribute是一些与之相关的值,主要是这4个:
- 值(value),可以是任意的JS值,也可以是一个getter或setter函数(或两者都有)。
当value为getter或setter时,那么我们称这个属性为“存取器属性(accessor property)”,其他时候则称作“数据属性(data property)” - 可写性(writable attribute),表时属性的值是否可以设置
- 可枚举性(enumerable attribute),表时属性是否可能通过for/in循环返回
- 可配置性(configurable attribute),表明属性是否可以删除或者修改
- 值(value),可以是任意的JS值,也可以是一个getter或setter函数(或两者都有)。
实际上,当属性是存取器属性时,是没有可写属性的,只有可枚举和可配置这两个属性。因此,可以这么说,对于一个数据属性,那么它有4个特性:值、可写性、可枚举性和可配置性,对于一个存取器属性,也有4个特性:get、set、可写性和可配置性。
对于存取器属性的值是否可以设置,通过getter和setter来决定的:当只设置了getter函数时,那么这个属性中是只读的;当设置了setter时,那么属性是可写的;两个都设置那么属性是既可读也能写的。
下面是一个具有存取器属性对象的定义:
var person = {
name: "Kaidi Yang",
get familyName() {
return this.name.split(" ")[1];
},
set familyName(val) {
this.name = this.name.split(" ")[0] + " " + val;
}
};
console.log(person.familyName); //输出: Yang
person.familyName = "XXXX";
console.log(person.name); //输出: Kaidi XXXX
var person2 = {
name: "Kaidi Yang",
get familyName() {
return this.name.split(" ")[1];
}
};
console.log(person2.familyName); //输出: Yang
person2.familyName = "Wang";
console.log(person2.name); //输出: Kaidi Yang</pre>
对于person这个对象中的familyName,它是同时具有getter和setter两个特性的,因此可以设置与改变。而person2这个对象中的familyName,它不具有setter,因此它是一个只读属性,给他设置新的值的话则会失败。不过值得注意的是,给这样的只读属性赋值的操作不会引发错误,这是JS的一个bug,在严格模式中已得到修复。
ES5中也提供一个新的方法Object.defineProperty用来定义对象的属性:
var xxx = {};
Object.defineProperty(xxx, "k", {
value: 1,
writable: true,
enumerable: true,
configurable: true
});
console.log(xxx.k); // 1
var zzz = {};
Object.defineProperty(zzz, "t", {
get: function() { return 2 },
set: function(val) {
console.log("get new val: " + val);
},
enumerable: true,
configurable: true
});
console.log(zzz.t); // 2
属性的访问
JS是一门基于原型继承的语言,当在访问对对象的属性时,会从自身开始一直检索整个原型链。
首先,一个JS对象具有“自有属性(own property)”,同时也有一些属性是从原型对象中继承来的。当我们访问一个对象xxx的属性k时,如果xxx中有k这个自有属性,那么,就会返回这个属性;如果对象xxx中没有属性k,那么就会继续在他的原型对象中查找属性k。如果原型对象中也没有,那么就会在这个原型对象的原型对象中查找,直到找到属性k或者查找到一个原型是null。
同样,当我们给对象xxx的属性k赋值时,如果xxx中已经有属性x(自有的)了,那么这个赋值操作就只改变这个自有属性的值;如果xxx中没有这个的自有属性k,那么赋值操作会给xxx加上一个新的自有属性k(如果xxx的的原型对象中有k,那么这个新加上的自有属性会覆盖原来的继承属性)。
上面是两是JS对象属性访问基本的规则,下面指出一些特殊的情况:
- 不能给只读属性赋值,除非用defineProperty方法把可配置属性变成可写的
- 在覆盖原型对象中的同名属性时,如果此属性是只读的,那么覆盖会失败。比如下面的代码
console.log(Object.getOwnPropertyNames(navigator)) // []
navigator.userAgent = "hello";
console.log(navigator.userAgent); //Mozilla/5.0 (Windows NT 10.0; WO....
这里我们先输出了navigator的所有自有属性,可以看出是一个空数组,因此可以知道navigator的所有属性值都是从原型对象中继承来的;于是我们就试图在navigator中加入一个userAgent,以期望它能覆盖原型对象中的userAgent;然而从后面的输出中可以看到userAgent没有发生变化,即覆盖失败,这就是因为userAgent中原型对象中是只读的。
- 在覆盖原型对象中的同名属性时,如果此属性是存取属性且定义了setter方法,那么不会发生覆盖而是当前对象会执行这个setter方法,下面是一个例子
var z = Object.create(zzz);
z.t = 13; // get new val: 13
console.log(z.t); // 2
属性的特性(attribute)
一个属性从构成来看有4个特性,这一点我们可以用过ES5提供的Object.getOwnPropertyDescriptor()方法来查看。比如下面的:
var xxx = {"k": 1};
console.log(Object.getOwnPropertyDescriptor(xxx, "k"));
console.log(Object.getOwnPropertyDescriptor(person, "familyName"));
//下面是输出
{ value: 1, writable: true, enumerable: true, configurable: true }
{
get: [Function: familyName],
set: [Function: familyName],
enumerable: true,
configurable: true
}
从上面的输出来看,对于一个新创建的对象,它默认的writable、enumerable和configurable都是true。如果我们想要改变的话,那么通过Object.defineProperty()方法:
Object.defineProperty(person2, "familyName", {
writable: true,
enumerable: false,
configurable: false
});
person2.familyName = "XXXX";
console.log(person2.familyName);
console.log(Object.getOwnPropertyDescriptor(person2, "familyName"));
//下面是输出
XXXX
{
value: 'XXXX',
writable: true,
enumerable: false,
configurable: false
}
//试着再定义这个属性
Object.defineProperty(person2, "familyName", {
writable: true,
enumerable: true,
configurable: false
}); //这里报错了
上面的代码,把person这个对象的familyName属性从存取器变成了数据性,enumerable和configurable变成了false。这样,familyName这个属性就变成了一个可写的、不可枚举的、不可配置的数据属性。我们可以设置它为新的值,也能正常访问,不过,却不能用for/in来遍历得到。
但是,当我们再次执行defineProperty,试图改变其枚举性时,JS执行却报错了,这是因为它的configurable已经设置了false。也就是说,一个属性的configurable一旦被设置成false,那么:
- 它的枚举性和可配性就不能发生变化,
- 也不能从数据性变成存取性,或从存取性变成数据性,或者修改setter和getter
- 还不能将要可写性变从false变成true
- 只能把可写性从true变成false
再回到前面试图修改浏览器userAgent的情况,我们先用getOwnPropertyDescriptor输出navigator.__proto__的中userAgent的情况,可以看到它是一个没有setter函数但是configurable为true的只读属性,因此完全是可以重写getter方法让这返回我们想的数据
Object.defineProperty(navigator.__proto__, "userAgent", {
get: function() {
return "hello"
}
});
console.log(navigator.userAgent); //hello
//或者直接给navigator定义一个数据属性的
Object.defineProperty(navigator, "userAgent", {
value: "hello"
});
console.log(navigator.userAgent); //hello
网友评论