一、可变对象
js有7种基本数据类型:number、string、boolean、null、undefine、object、symbol(ES6新增),除了object为引用类型,其他均为基本类型
基本类型将值存在栈中
引用类型将地址存在栈中,值存在栈中地址指向的堆中
因此,我们如果copy基本类型,拷贝的是它的值,如果再单独赋值也不会影响被拷贝的值。如果我们copy一个引用类型,其实拷贝的是它在堆中的地址,如果改变其中一方的值,就是改变了堆中的值,另一方因为指向同一个堆,所以它的值也被同步修改了。
所以对于引用类型(array、function、obj等)在拷贝时为了使两个引用类型互不影响,一般我们不直接将旧的array赋值给新的array,而是生成新的引用类型变量,将旧的数组中的值一个一个的分别拷贝到新的array中,这就是所谓的深拷贝,但是如果数组中的值又是引用类型的话,还要把这个引用类型中的值再分别取出付给又新生成的引用对象中,如果嵌套层次太多的话,处理起来就容易出错。
二、不可变对象
用来引用类型深拷贝问题,有弱到强主要分为三种:
Object.preventExtensions(不可扩展)、Object.seal(密封)、Object.freeze(冻结)
1.Object.preventExtensions()
Object.preventExtensions()可以使一个对象不可再添加新的属性,参数为目标对象,返回修改后的对象。
var obj=object.preventExtensions({});
//直接定义新的属性会报错
object.defineProperty(obj,'content',{
value:'hello'
});//TypeError: Cannot define property:p, object is not extensible.
//非严格模式下通过点符号添加不会报错,但会静默失败,原对象仍然没有content属性
object.content='Hello';
object.content://undefine
Object.isExtensible()可以判断一个对象是否可扩展,即是否可以添加新的属性。参数是目标对象,返回布尔值,true代表可扩展,false不可扩展。
var obj=new object();
object.isExtensible(obj);//true
object.preventExtensions(obj)
object.isExtensible(obj);//false
2.Object.seal()
Object.seal()可以使一个对象无法添加新属性的同时也无法删除旧属性,参数是目标对象,返回修改后的对象。
其本质是通过修改对象的configurable(可设置)为false来实现的。在属性描述对象里讲到,configurable为false时,其他配置不可改变,writable只能true变false,且属性无法被删除。只要writable或configurable其中之一为true时,value是可变的,所以密封之后的对象还是可以改属性值的。
var obj={content:'hello'};
object.getOwnPropertyDescriptor(obj,'content');
//obj{
//configurable: true
//enumerable: true
//value: "hello"
//writable: true
//}
object.seal(obj);
object.getOwnPropertyDescriptor(obj,'content');//seal后configurable变为false
//obj{
//configurable: false
//enumerable: true
//value: "hello"
//writable: true
//}
对应的Object.isSealed()可以检测一个对象是否密封,参数是目标对象,返回布尔值,true代表被密封,不可增删属性,false代表没被密封可增删属性。
var obj=new object();
object.isExtensible(obj);//true
object.isSealed(obj)//false
object.seal(obj);
object.isExtensible(obj)//false,seal后对象isExtensible()也随之改变
object.isSealed(obj);//true
3.Object.freeze()
Object.freeze()可以使一个对象不能再添加新属性,也不可以删除旧属性,且不能修改属性的值。参数是目标对象,返回修改后的对象。
var obj=object.freeze({name:'example'});
//直接定义新的属性会报错
object.defineProperty(obj,'content',{
value:'hello'
})//TypeError:cannot define property:p,object is not extensible.
//非严格模式下通过点符号添加不会报错,但会静默失败,原对象仍然没有content属性
obj.content='hello';
obj.content;//undefined;
delect obj.name;//删除失败,返回false
obj.name='hello';
obj.name;//仍然是‘example’
对应的Object.isFrozen()可以检测一个对象是否冻结,即是否可以增删改。参数是目标对象,返回布尔值,true表示已经冻结不可再增删改,false表示未冻结。
var obj=new object()
object.isExtensible(obj);//true
object.isSealed(obj);//false
object.isFrozen(obj);//false
object.freeze(obj);
object.isExtensible(obj);//false,freeze后对象的isExtensible()也随之改变
object.isSealed(obj);//true,freeze后对象的isSealed()也随之改变
object.isFrozen(obj);//true
注意
无论是不可扩展,密封,还是冻结,都是浅层控制的,即只对对象本身属性的增删改。如果对象属性是一个引用类型,比如数组subArr或对象subObj等,虽然subArr、subObj的不可被删改,但他们的属性可以增删改。
var obj=object.freeze({
content:{name:'example'}
});
obj.content=new object();
obj.content;//{name:'example'},content本身不可修改
obj.content.name='test';
obj.content;//{name:'test'}但content的属性仍可改,因为冻结的是obj不是它的属性
由于每个对象都有一个proto该属性的值是该对象的原型对象,也是引用类型,由于冻结是浅层的,所以原型对象并不会被连着冻结,仍然可以通过给对象的原型对象加属性达到给当前对象新增属性的效果。所以如果想进一步冻结还需要把原型对象也冻结上。
var obj=object.freeze({});
obj.content='hello';
obj.content;//undefine,增加失败
var proto=object.getPrototypeOf(obj);
proto.content='hello'//obj的圆形属性为hello
obj.content;//obj本身没有content这个属性的话,向它的原型找这个属性,‘hello’增加成功
object.freeze(proto);
proto.name="example";
obj.name//undefine,冻结原型之后增加失败
object.defineProperty()
object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
语法
object.definePoperty(obj,prop,descriptor)
参数
obj 要在其上定义属性的对象
prop 要定义或修改的属性的名称
descriptor 将被定义或修改的属性描述符
返回值
被传递给函数的对象
在ES6中,由于Symbol类型的特殊性,用Symbol类型的值来做对象的key与常规的定义或修改不同,而object.defineProperty是定义key为Symbol的属性的方法之一。
描述
该方法允许添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举的,能够在属性枚举间呈现出来(for...in 或 object.keys方法),这些属性的值可以被改变,也可以被删除。这个方法允许修改默认的额外选项(或配置)。默认情况下,使用 Object.defineProperty() 添加的属性值是不可修改的。
属性描述符
对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。存取描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。
数据描述符和存取描述符均具有以下可选键值:
configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
enumerable
当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:
value
该属性对相应的值。可以是有效的JavaScript值(数值、对象、函数)默认undefine
writable
当且仅当属性的writable为true时,value才能被赋值运算符改变,默认为false
存取描述符同时具有以下可选键值:
get(默认为undefined)
一个属性提供getter方法,如果没有getter则为undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
set(默认为undefined)
一个属性提供setter的方法,如果没有setter则为undefined。当属性值修改时,触发执行该方法。该方法将接收唯一参数,即该属性新的参数值
描述符可同时具有的键值
如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常。
记住,这些选项不一定是自身属性,如果是继承来的也要考虑。为了确认保留这些默认值,你可能要在这之前冻结
Object.prototype
,明确指定所有的选项,或者通过 Object.create(null)
将__proto__
属性指向null
。
浏览器兼容问题
数组的length属性重定义是可能的,但会受到一般的重定义限制。(length属性初始值为non-configurable,non-enumerable 以及 writable。对于内容不变的数组改变其length属性的值或者改变它为non-writable是可能的。但是改变其可枚举性和可配置性或者当它是non-writable时尝试改变它的值或是可写性,这两者是不允许的。)然而并不是所有的浏览器都允许Array.length的重定义。
在 Firefox 4 至 22 版本中尝试去重定义数组的 length 属性都会抛出一个 TypeError
异常。
有些版本的Chrome中,Object.defineProperty()
在某些情况下会忽略不同于数组当前length
属性的length值。有些情况下改变可写性并不起作用(也不抛出异常)。同时,比如Array.prototype.push
的一些数组操作方法也不会考虑不可读的length属性。
有些版本的Safari中,Object.defineProperty()
在某些情况下会忽略不同于数组当前length
属性的length值。尝试改变可写性的操作会正常执行而不抛出错误,但事实上并未改变属性的可写性。
只在Internet Explorer 9及以后版本和Firefox 23及以后版本中,才完整地正确地支持数组length
属性的重新定义。目前不要依赖于重定义数组length
属性能够起作用,或在特定情形下起作用。与此同时,即使你能够依赖于它,你也没有合适的理由这样做。
Internet Explorer 8具体案例
Internet Explorer 8 实现了 Object.defineProperty()
方法,但 只能在 DOM 对象上使用。 需要注意的一些事情:
- 尝试在原生对象上使用
Object.defineProperty()会
报错。 - 属性特性必须设置一些特定的值。对于数据属性描述符,
configurable
,enumerable
和writable
特性必须全部设置为true;
对于访问器属性描述符,configurable
必须设置为true
,enumerable
必须设置为false
。(?) 任何试图提供其他值(?)将导致一个错误抛出。 - 重新配置一个属性首先需要删除该属性。如果属性没有删除,就如同重新配置前的尝试。
链接:github