Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript对象的的创建及属性状态维护详解 #5

Open
SimonZhangITer opened this issue Feb 20, 2017 · 0 comments
Open

JavaScript对象的的创建及属性状态维护详解 #5

SimonZhangITer opened this issue Feb 20, 2017 · 0 comments

Comments

@SimonZhangITer
Copy link
Owner

在说属性之前,我们先来了解一下ES5的新方法,Object.create()函数。
#新的对象创建方法
在旧的“原型继承”观念中,它的本质上是“复制原型”,即:以原型为模板复制一个新的对象。然而我们应该注意到一点事实:在这个思路上,“构造器函数”本身是无意义的。更确切的说,构造器函数对实例的修饰作用可有可无,例如:

//在构造器中修饰对象实例
function MyObject(){
	this.yyy = ...;
}

当意识到这一点后,ES5实现Object.cerate()这样一种简单的方法,通过这一方法将“构造器函数”从对象创建过程中赶了出去。在新的机制中,对象变成了简单的“原型继承+属性定义”,而不再需要“构造器”这样一层语义,例如:

//新的对象创建方法
newObj = Object.create(prototypeObj,PropertyDescriptors);

这里的PropertyDescriptors是一组属性描述符,用于声明基于prototypeObj这个原型之上的一些新的属性添加或修改,它与defineProperties()方法中的props参数是一样的,并在事实上也将调用后者。它的用法如下例所示:

var aPrototypeObject = {name1:"value1"};
var aNewInstance = Object.create(aPrototypeObject,{
	name2:{value:'value2'},
	name3:{get:function(){ return 'value3' }}
})

很显然,在这种新方案中我们看不到类似MyObject()那样的构造器了。事实上在引擎实现Object.create()时也并不特别地声明某个构造器。

所以,所有由Object.create()创建的对象实例具有各自不同的原型(这取决于调用create()方法时传入的参数),但它们的constractor值指向相同的引用——引擎内建的Object构造器。

#属性状态维护
ES5中在Object()上声明了三组方法,用于维护对象本身在属性方面的信息,如下表(Markdown不会使用分组列表,大家凑合看看。。如果有知道的也告诉我一下哈~)

分类 方法 说明
取属性列表 getOwnPropertyNames(obj) 取对象自有的属性名数组
取属性列表 keys(obj) 取对象自由的、可见的属性名数组
状态维护 preventExtensions(obj) 使实例obj不能添加新属性
状态维护 seal(obj) 使实例obj不能添加新属性,也不能删除既有属性
状态维护 freeze(obj) 使实例obj所有属性只读,且不能再添加、删除属性
状态检查 isExtensible(obj) 返回preventExtensions状态
状态检查 isSealed(obj) 返回seal状态
状态检查 isFrozen(obj) 返回freeze状态
其中,preventExtensions、seal和freeze三种状态都是针对对象来操作的,会影响到所有属性的性质的设置。需要强调的有两点:
  • 由原型继承来的性质同样会受到影响
  • 以当前对象作为原型时,子类可以通过重新定义同名属性来覆盖这些状态

更进一步的说,这三种状态是无法影响子类使用defineProperty()和defineProperties()来“重新定义(覆盖)”同名属性的。

本质上说,delete运算是用于删除运算对象属性的属性描述符,而非某个属性。

##取属性列表

取属性列表的传统方法是使用for...in语句。为方便后续讨论,我们先为该语句封装一个与Object.keys()类似的方法:

Object.forIn = function(obj){
	var Result = [];
	for(var n in obj) Result.push(n);
	return Result;
}

forIn()得到的总是该对象全部可见的属性列表。而keys()将是其中的一个子集,即"自有的(不包括继承而来的)"可见属性列表。下面的例子将显示二者的不同:

var obj1 = {n1:100};
var obj2 = Object.create(obj1,{n2 : {value :200,enumerable:true}});

//显示'n1' , 'n2'
//  - 其中n1继承自obj1
alert(Object.forIn(obj2));

//显示'n2'
alert(Object.keys(obj2));

getOwnPropertyNames()得到的与上述两种情况都不相同。它列举全部自有的属性,但无论它是否可见。也就是说,它是keys()所列举内容的超集,包括全部可见和不可见的、自有的属性。仍以上述为例:

// (续上例)

//定义属性名n3,其enumerable性质默认为false
Object.defineProperty(obj2,'n3',{value:300})

//仍然显示'n1','n2'
// - 新定义的n3不可见
alert(Object.forIn(obj2));

//显示'n2'
alert(Object.keys(obj2));

//显示n2,n3
alert(Object.getOwnPropertyNames(obj2));

##使用defineProperty来维护属性的性质
在defineProperty()或defineProperties()中操作某个属性时,如果该名字的属性未声明则新建它;如果已经存在,则使用描述符中的新的性质来覆盖旧的性质值。

这也意味着一个使用"数据属性描述符"的属性,也可以重新使用"存取属性描述符"——但总的来说只能存在其中一个。例如:

var pOld,pNew;
var obj = { data : 'oldValue'}

//显示'value,writable,enumerable,configuable'
pOld = Object.getOwnPropertyDescriptor(obj,'data');
alert(Object.keys(pOld));

//步骤一:通过一个闭包来保存旧的obj.data的值
Object.defineProperty(obj,'data',function(oldValue){
	return {
		get:function(){ return oldValue},
		configurable:false
	}
}(obj.data))

//显示'get,set,enumerable,configurable'
pNew = Object.getOwnPropertyDescriptor(obj,'data');
alert(pNew);

//步骤二:测试使用重定义的getter来取obj.data的值
// - 显示 'oldValue'
alert(obj.data);

//步骤三:(测试)尝试再次声明data属性
// - 由于在步骤一中已经设置configurable为false,因此导致异常(can't redefine)。
Object.defineProperty(obj,'data',{value:100});

##对于继承自原型的属性,修改其值的效果

如果某个从原型继承来的属性是可写的,并且它使用的是"数据属性描述符",那么在子类中修改该值,将隐式地创建一个属性描述符。这个新属性描述符将按照"向对象添加一个属性"的规格来初始化。即:必然是数据属性描述符,且Writable,Enumerable和Configurable均为true值。例如:

var obj1 = {n1 : 100};
var obj2 = Object.create(obj1);

//显示为空
// - 重置n1的enumerable性质为false,因此在obj1中是不可见的
Object.defineProperty(obj1,'n1',{enumerable:false})
alert(Object.keys(obj1));

//显示为空
// - n1不是obj2的自有属性
alert(Object.getOwnPropertyNames(obj2));

//显示n1
// - 由于n1赋值导致新的属性描述符,因此n1成为了自有的属性
obj2.n1 = 'newValue';
alert(Object.getOwnPropertyNames(obj2));

//显示n1,表明n1是可见的
// - 由于新的属性描述符的enumerable重置为true,因此在obj2中它是可见的
alert(Object.keys(obj2));

如果一个属性使用的是"存取属性描述符",那么无论它的读写性为何,都不会新建属性描述符。对子类中该属性的读写,都只会忠诚地调用(继承而来的、原型中的)读写器。

##重写原型继承来的属性的描述符
使用defineProperty()或defineProperties()将重新定义该属性,会显式的创建一个属性描述符。在这种情况下,该属性也将变成自雷对象中"自有的"属性,它的可见性等性质就由新的描述符来决定。

与上一小节不同的是,这与原型中该属性是否"只读"或是否允许修改性质(configurable)无关。

这可能导致类似如下的情况:在父类中某个属性时只读的,并且不可修改其描述符性质的,但是在子类中,同一个名字的属性却可以读写并可以重新修改性质。更为严重的是,仅仅观察两个对象实例的外观,我们无法识别这种差异是如何导致的。下面的示例说明这种情况:

var obj1 = {n1 : 100};
var obj2 = Object.create(obj1);

//对于原型对象obj1,修改其属性n1的性质,使其不可列举、修改、且不能重设性质
Object.defineProperty(obj1,'n1',{writable:false,enumerable:false,configurable:false});

//显示为空,obj1.n1是不可列举的
alert(Object.keys(obj1));

//由于不可重设性质,因此对obj1.n1的下述调用将导致异常
//Object.defineProperty(obj1,'n1',{configurable:true});

接下来我们观察"重新定义属性"带来的效果:

//(续上例)

//重新定义obj2.n1
Object.defineProperty(obj2,'n1',{value:obj2.n1,writable:true,enumerable:true,configurable:true});

//显示newValue'
// - 结论:可以通过重定义属性,使该属性从"只读"变成"可读写"(以及其他性质的变化)
obj2.n1 = 'newValue';
alert(obj2.n1);

//列举obj2的自有性质,结果显示:n1
// - 现在n1是自有的属性了
alert(Object.getOwnpropertyNames(obj2));

从表面上看,一个父类中只读的属性在子类变成了可读写。而且,一旦我们用delete删除该属性,它又会恢复父类中的值和性质。例如:

//尝试删除该属性
// - 显示100,即它在原型中的值
delete obj2.n1;
alert(obj2.n1);

再次强调这一事实:在ES5中没有任何方法可以阻止上述过程。也就是说,我们无法阻止子类对父类同名属性的重定义,也无法避免这种重定义可能带来的业务逻辑问题。

@SimonZhangITer SimonZhangITer changed the title ES5对象的的创建及属性状态维护分析 JavaScript对象的的创建及属性状态维护详解 May 2, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant