本文深入解析JavaScript中delete操作符的工作原理,重点阐述ECMAScript定义的内部属性Configurable如何控制属性的可删除性。通过对比数据属性与访问器属性的特征,结合Object.defineProperty和Object.getOwnPropertyDescriptor的使用示例,说明为何部分变量无法被删除,并展示如何正确获取和修改属性内部特征。
在深入探讨Configurable之前,我们先通过一道常见的面试题来引入话题:
a = 1;
console.log( window.a ); // 1
console.log( delete window.a ); // true
console.log( window.a ); // undefined
var b = 2;
console.log( window.b ); // 2
console.log( delete window.b ); // false
console.log( window.b ); // 2
上述示例揭示了两种声明方式的区别:当变量未使用var关键字声明时,使用delete命令可以成功删除该变量,再次访问时返回undefined;而若使用var声明变量,则delete命令无法将其删除,再次访问时仍能获取到原始值2。
1. delete操作符
执行delete删除变量或对象属性时,若删除成功则返回true,否则返回false。例如前文示例中,delete尝试删除变量a失败返回false,而成功删除变量b则返回true。
除了上述基本变量,其他常见类型的变量在delete操作下的表现各异,有的可被删除,有的则不能。此处暂不深究其背后的原理,仅关注delete操作的返回值结果:
尝试删除数组中的单个元素:
// 使用for~in是循环不到的,直接忽略到该元素
// 使用for()可以得到该元素,但是值是undefined
var arr = [1, 2, 3, 4];
console.log( arr ); // [1, 2, 3, 4]
console.log( delete arr[2] ); // true,删除成功
console.log( arr ); // [1, 2, undefined, 4]
尝试删除函数类型的变量:
// chrome 不能删除;火狐可以删除
function func(){
}
console.log( func );
console.log( delete func );
console.log( func );
尝试删除函数的length属性,该属性用于获取形参个数:
function func1(a, b){
}
console.log( func1.length ); // 2
console.log( delete func1.length ); // true,删除成功
console.log( func1.length ); // 0
尝试删除各类内置变量:
console.log( delete NaN ); // false,删除失败
console.log( delete undefined );// false
console.log( delete Infinity ); // false
console.log( delete null ); // true,删除成功
尝试删除prototype属性本身,而非其上的具体属性:
function Person(){
}
Person.prototype.name = "蚊子";
console.log( delete Person.prototype ); // false,无法删除
console.log( delete Object.prototype ); // false
尝试删除数组和字符串的length属性:
var arr = [1, 2, 3, 4];
console.log( arr.length ); // 4
console.log( delete arr.length ); // false,删除失败
console.log( arr.length ); // 4
var str = 'abcdefg';
console.log( str.length ); // 7
console.log( delete str.length ); // false,删除失败
console.log( str.length ); // 7
尝试删除普通对象中的属性:
var obj = {name:'wenzi', age:25};
console.log( obj.name ); // wenzi
console.log( delete obj.name ); // true,删除成功
console.log( obj.name ); // undefined
console.log( obj ); // { age:25 }
从实例对象的输出结果可见,delete仅能删除实例对象自身的属性,无法删除prototype链上的属性,即使多次执行delete也无济于事。若要删除prototype上的属性或方法,必须采用如下方式:delete Person.prototype.name:
function Person(){
this.name = 'wenzi';
}
Person.prototype.name = '蚊子';
var student = new Person();
console.log( student.name ); // wenzi
console.log( delete student.name ); // true,删除成功
console.log( student.name ); // 蚊子
console.log( delete student.name ); // true
console.log( student.name ); // 蚊子
console.log( delete Person.prototype.name );// true,删除成功
console.log( student.name ); // undefined
2. js的内部属性
在前述示例中,部分变量或属性可被成功删除,而另一部分则不能。决定这一差异的关键因素是什么?
ECMA-262第5版定义了JS对象属性的特征(供JS引擎内部使用,外部不可直接访问)。ECMAScript将属性分为两类:数据属性和访问器属性。
2.1 数据属性
数据属性包含一个存储数据值的位置,可在此读取或写入值。该属性具备四个描述其行为特性的特征:
- [[configurable]]:表示能否使用delete操作符删除该属性从而重新定义,或能否将其修改为访问器属性。默认值为true;
- [[Enumberable]]:表示是否可通过for-in循环返回该属性。默认值为true;
- [[Writable]]:表示是否可修改属性的值。默认值为true;
- [[Value]]:包含该属性的实际数据值。读取或写入操作均针对此值。默认值为undefined;例如前文实例对象Person中定义的name属性,其值为'wenzi',对该值的修改均反映在此位置。
若要修改对象属性的默认特征(默认均为true),可调用Object.defineProperty()方法。该方法接收三个参数:属性所在的对象、属性名以及一个描述符对象(必须包含configurable、enumberable、writable和value中的一个或多个)。
示例如下:
var person = {};
Object.defineProperty(person, 'name', {
configurable: false, // 不可删除,且不能修改为访问器属性
writable: false, // 不可修改
value: 'wenzi' // name的值为wenzi
});
console.log( person.name); // wenzi
console.log( delete person.name ); // false,无法删除
person.name = 'lily';
console.log( person.name ); // wenzi
可以看出,此时delete及重置person.name的值均失效,原因在于调用defineProperty函数修改了对象属性的特征。值得注意的是,一旦将configurable设置为false,便无法再使用defineProperty将其改回true(执行时会报错:Uncaught TypeError: Cannot redefine property: name);
2.2 访问器属性
访问器属性主要包含一对getter和setter函数。读取访问器属性时,会调用getter返回有效值;写入访问器属性时,则调用setter写入新值。该属性具备以下四个特征:
- [[Configurable]]:是否可通过delete操作符删除并重新定义属性;
- [[Numberable]]:是否可通过for-in循环查找该属性;
- [[Get]]:读取属性时自动调用,默认值为undefined;
- [[Set]]:写入属性时自动调用,默认值为undefined;
访问器属性无法直接定义,必须使用defineProperty()来定义,示例如下:
var person = {
_age: 18
};
Object.defineProperty(person, 'isAdult', {
Configurable : false,
get: function () {
if (this._age >= 18) {
return true;
} else {
return false;
}
}
});
console.log( person.isAdult ); // true
此外需注意一点,Object.defineProperty()方法在设置属性时,不能同时声明访问器属性(set和get)和数据属性(writable或者value)。即若某属性已设置writable或value,则不能再声明get和set,反之亦然。
若尝试以下列方式定义,使访问器属性和数据属性同时存在:
var o = {};
Object.defineProperty(o, 'name', {
value: 'wenzi',
set: function(name) {
myName = name;
},
get: function() {
return myName;
}
});
上述代码看似无误,但实际执行时会报错,错误信息如下:
Uncaught TypeError: Invalid property. A property cannot both have accessors and be writable or have a value
对于数据属性,可获取的特征包括:configurable、enumberable、writable和value;
对于访问器属性,可获取的特征包括:configurable、enumberable、get和set。
由此可知:一个变量或属性是否可被删除,由其内部属性Configurable控制。若Configurable为true,则该变量或属性可被删除,否则不可删除。
那么,我们该如何获取这个Configurable的值呢?总不能仅靠delete尝试删除来测试吧。其实是有办法的!!
2.3 获取内部属性
ES5为我们提供了Object.getOwnPropertyDescriptor(object, property)来获取内部属性。
例如:
var person = {name:'wenzi'};
var desp = Object.getOwnPropertyDescriptor(person, 'name'); // person中的name属性
console.log( desp ); // {value: "wenzi", writable: true, enumerable: true, configurable: true}
通过Object.getOwnPropertyDescriptor(object, property)我们能够获取到4个内部属性,其中configurable控制着变量或属性是否可被删除。在此例中,person.name的configurable为true,说明该属性是可以被删除的:
console.log( person.name ); // wenzi
console.log( delete person.name ); // true,删除成功
console.log( person.name ); // undefined
我们再回到最开始的那个面试题:
a = 1;
var desp = Object.getOwnPropertyDescriptor(window, 'a');
console.log( desp.configurable ); // true,可以删除
var b = 2;
var desp = Object.getOwnPropertyDescriptor(window, 'b');
console.log( desp.configurable ); // false,不能删除
其结果与我们使用delete操作删除变量时产生的结果完全一致。
3. 总结
看似简单的delete操作,背后实则蕴含了丰富的原理!