1、复制对象
JavaScript初学者最常见的问题之一就是如何复制一个对象。
思考一下这个对象:
function anotherFunction(){
/*..*/
}
var anotherObject ={
c : true
};
var anotherArray = [];
var myObject ={
a : 2,
b : anotherObject, // 引用, 不是复本!
c : anotherArray, // 另一个引用!
d : anotherFunction
};
anotherArray.push(anotherObject, myObject);
如何准确的表示myObject的复制呢?
首先,应该判断它是浅复制还是深复制。对于浅复制来说,复制出的新对象中a的值会复制旧对象中a的值,但是新对象中b、c、d三个属性其实只是三个引用,它们和旧对象中b、c、d引用的对象是一样的。对于深复制来说,除了复制myObject以外还会复制anotherObject和anotherArray。这时问题就来了, anotherArray 引用了 anotherObject 和myObject, 所以又需要复制 myObject, 这样就会由于循环引用导致死循环。
对于JSON安全(也就是说可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:
var newObj = JSON.parse(JSON.stringify(someObj));
相比深复制,浅复制非常易懂并且问题要少得多,ES6定义了Object.assign()方法来实现浅复制。Objext.assign()方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。它会遍历一个或多个源对象的所有可枚举(enumerable)的自有键(owned key)并把它们复制到目标对象,最后返回目标对象,就像这样:
var newObj = Object.assign( {}, myObject );
newObj.a; // 2
newObj.b === anotherObject; // true
newObj.c === anotherArray; // true
newObj.d === anotherFunction; // true
2、属性描述符
从ES5开始,所有的属性都具备了属性描述符
var myObject ={
a : 2
};
Object.getOwnPropertyDescriptor(myObject, "a");
// {
// value: 2,
// writable: true, 可写
// enumerable: true, 可枚举
// confgurable: true,可配置
// }
在创建普通属性时属性描述符会使用默认值,也可以是哦那个Object.defineProperty()来添加一个新属性或者修改一个已有属性(如果它是configurable)并对特性进行设置。举例来说:
var myObject = {};
Object.defineProperty(myObject, "a",{
value : 2,
writable : true,
configurable : true,
enumerable : true
});
myObject.a; // 2
然而,除非你想修改属性描述符,一般不会用这种方式。
属性访问[[Get]]在实现时有一个微妙却非常重要的细节,思考下列代码
var myObject = {
a: 2
};
myObject.a; //2
myObject.a是一次属性访问,在语言规范中,myObject.a在myObject上实际上是实现了[[Get]]操作。对象默认的内置[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。否则就会遍历可能存在的[[Prototype]]链,如果无论如何都没有找到名称相同的属性,那[[Get]]操作会返回undefined。
myObject.a的属性访问返回值可能是undefined。有两种可能,1是属性不存在所以返回undefined,2是属性中存储的undefined。
我们可以在不访问属性值的情况下判断对象中是否存在这个属性:
var myObject ={
a : 2
};
("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false
[[Put]]操作:[[Put]]被触发时,实际的行为取决于许多因素,包括对象中是否已经存在这个属性(这是最重要的因素)。如果存在:
- 属性是否是访问描述符?如果是并且存在setter就调用setter。
- 属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在严格模式下抛出TypeError异常。
- 如果都不是,将该值设置为属性的值。
Getter和Setter
对象默认的[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取。在ES5中可以使用getter和setter部分改写默认操作,但是只能应该在单个属性上,无法应用在整个对象上。getter是一个隐藏函数,会在获取属性值时调用。setter也是一个隐藏函数,会在设置属性值时调用。
当给一个属性定义getter、setter或者两个都有时,这个属性会被定义为“访问描述符”。对于访问描述符来说,JavaScript会忽略它们的value和writeable特性,取而代之的是关心set和get特性。思考下列代码:
var myObject = {
// 给 a 定义一个 getter
get a()
{
return 2;
}
};
Object.defineProperty(
myObject, // 目标对象
"b", // 属性名
{ // 描述符
// 给 b 设置一个 getter
get : function (){
return this.a * 2
},
// 确保 b 会出现在对象的属性列表中
enumerable : true
});
myObject.a; // 2
myObject.b; // 4
不管是对象文字语法中的get a(){},还是defineProperty()中的显示定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值:
var myObject ={
// 给 a 定义一个 gett
get a(){
return 2;
}
};
myObject.a = 3;
myObject.a; // 2
由于之定义了a的getter,所以对a的值进行设置时set操作会忽略赋值操作,不会抛出错误。而且即使有合法的setter,由于自定义的getter只会返回2,多以set操作是没有意义的。
为了让属性更合理,还应当定义setter,setter会覆盖单个属性默认的[[Put]]操作。通常来说getter和setter是成对出现。
3、遍历
for...in循环可以用来遍历对象的可枚举属性列表(包括[[Prototype]]链。但如何遍历属性的值呢?
对于数值索引的数组来说,可以使用标准的for循环来遍历值,这实际上并不是在遍历值,而是在遍历下标来指向值。
使用for..in遍历对象是无法直接获取属性值的。需要手动获取属性值。ES6增加了一种用来遍历数组的for..of循环语法
var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
console.log( v );
}
// 1
// 2
// 3
for..of循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值。
数组有内置的@@iterator,因此for..of可以直接应用在数组上。
和数组不同,普通的对象没有内置的@@iterator,所以无法自动完成for..of遍历。
需要自行定义@@iterator:
var myObject = {
a : 2,
b : 3
};
Object.defineProperty(myObject, Symbol.iterator,{
enumerable : false,
writable : false,
configurable : true,
value : function () {
var o = this;
var idx = 0;
var ks = Object.keys(o);
return{
next : function (){
return{
value : o[ks[idx++]],
done : (idx > ks.length)
};
}
};
}
}
);
// 手动遍历 myObject
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefned, done:true }
// 用 for..of 遍历 myObject
for (var v of myObject){
console.log(v);
}
//2
// 3
网友评论