-## 对象属性
对象属性访问
对象的属性有两种方式访问
1.使用点符号(.)
这是最常见和直观的方式,适用于已知属性名且属性名符合标识符规则的情况(即不能包含空格、特殊字符或保留字,并且不能以数字开头)。
let person = {
name: 'Alice',
age: 25
};
console.log(person.name); // 输出: Alice
console.log(person.age); // 输出: 25
- 使用方括号符号([])
这种方式提供了更大的灵活性,允许你用变量或表达式作为键来访问属性,适用于动态属性名或包含特殊字符的属性名。
let key = 'name';
let person = {
'first name': 'Alice',
age: 25
};
console.log(person[key]); // 输出: Alice
console.log(person['first name']);// 输出: Alice
console.log(person['age']); // 输出: 25
在对象中,属性名永远都是字符串。如果你使用string(字面量)以外的其他值作为属性名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的确是数字,但是在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中数字的用法:
var myObject = { };
myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";
myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
数组
数组本质上是特殊的对象,它们具有数字索引(下标)来存储元素,并且这些索引确实可以被视为对象的键名。
稀疏数组:js中当你创建一个数组并只给某些索引赋值时,而其他索引没有被显式地赋值,这些未赋值的索引就形成了所谓的“空洞”。他存储使用哈希表做到O1的取出时间复杂度。
let arr = [];
arr[0] = 1;
arr[2] = 3;
console.log(arr); // 输出: [1, <1 empty item>, 3]
console.log(arr[1]); // 输出: undefined
遍历行为:
- 某些遍历方法(如 `for...in` 和 `Object.keys()`)会跳过这些空洞,不会枚举未定义的索引。
- 但是,像 `forEach`、`map` 或者 `for...of` 这样的迭代器会遍历所有索引,包括那些未定义的元素,尽管它们可能会返回 `undefined`。
复制对象
在 JavaScript 中,当你使用 ==
或 ===
比较两个对象时,无论是普通对象、数组还是函数(它们都是对象),比较的结果取决于引用而不是内容。这意味着只有当两个操作数指向内存中的同一个对象实例时,才会返回 true
。换句话说,JavaScript 不会进行深度比较来检查对象的内容是否相同。
let obj1 = { key: 'value' };
let obj2 = { key: 'value' };
let obj3 = obj1;
console.log(obj1 == obj2); // false (不同的对象实例)
console.log(obj1 === obj2); // false (不同的对象实例)
console.log(obj1 == obj3); // true (相同的对象实例)
console.log(obj1 === obj3); // true (相同的对象实例)
浅拷贝 基本属性会复制值,但是对象属性是引用。
对于浅拷贝来说,复制出的新对象中a的值会 复制旧对象中a的值,也就是2,但是新对象中b、c、d三个属性其实只是三个引用,它们 和旧对象中b、c、d引用的对象是一样的。
相比深复制,浅复制非常易懂并且问题要少得多,所以ES6定义了Object.assign(…)方法来实现浅复制。Object.assign(…)方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。注意: 源对象属性的一些特性(比如writable)不会被复制到目标对象。
let obj1 = { a: { b: 1 } };
let obj2 = Object.assign({}, obj1);
obj1.a.b = 2;
console.log(obj2.a.b); // 输出: 2 (因为是浅拷贝,obj2.a 和 obj1.a 指向同一个对象)
深拷贝
我们来看下面代码,假如我要完全拷贝另一个对象跟obj1属性相同,且不是引用,那可能会由于循环引用导致死循环。
obj1 ={
}
obj2 ={
obj:obj1
}
obj1.obj = obj2
console.log(obj1);
console.log(obj1.obj);
console.log(obj1.obj.obj);
console.log(obj1.obj.obj.obj);
我们是应该检测循环引用并终止循环(不复制深层元素)?还是应当直接报错或者是选择其他方法呢?JavaScript 应当采用哪种方法作为标准呢?在很长一段时间里,这个问题都没有明确的答案。
属性描述符
ES5开始,所有的属性都具备了属性描述符。对象的属性描述符并不是直接存储在对象本身的数据结构中,而是由JavaScript引擎内部管理。
属性描述符有两种类型:
- 数据描述符:是那些拥有值的描述符,它们可以被读取和写入。
- 访问器描述符:是那些拥有至少一个获取函数(getter)或设置函数(setter)的描述符。
数据描述符
我们可以通过可以通过Object.getOwnPropertyDescriptor()
方法来查看现有属性的描述符。可以通过Object.defineProperty()
或Object.defineProperties()
方法Object.defineProperty(…) 来添加一个新属性或者修改一个已有属性(如果它是configurable)并对特性进行设置。
例子:
const obj = {};
// 添加一个具有自定义描述符的新属性
Object.defineProperty(obj, 'myProperty', {
value: 42,
writable: false,
enumerable: true,
configurable: false
});
// 获取属性描述符以检查其当前状态
console.log(Object.getOwnPropertyDescriptor(obj, 'myProperty'));
// { value: 42,
// writable: false,
// enumerable: true,
// configurable: false }
我们可以通过可以通过Object.defineProperty()
或Object.defineProperties()
方法来设置这些描述符。
每种描述符类型都有一组特定的可配置选项。以下是完整的属性描述符键列表:
-
value
: 属性的值。可以是任何有效的 JavaScript 值(数字、字符串、对象等)。仅适用于数据描述符。 -
writable
: 如果为true
,则属性的值可以通过赋值操作改变;如果为false
,则不能。仅适用于数据描述符。
var obj = {};
Object.defineProperty(obj,
'a',{
value: 1,
writable: false,
enumerable: true,
configurable: false
});
obj.a = 2;
console.log(obj.a); //1
如你所见,我们对于属性值的修改静默失败(silently failed)了。如果在严格模式下,这种方法会报错。
configurable
: 如果为true
,则该属性描述符能够被改变,同时该属性也能从对应的对象上被删除。如果为false
,描述符不可修改,属性也不能删除!!!这也意味着我们无法把configurable改回true,把configurable修改成 false是单向操作,无法撤销。
修改属性配置:
defineProperty(…)会产生一个TypeError错误,不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。
要注意有一个小小的例外:即便属性是configurable:false, 我们还是可以 把writable的状态由true改为false,但是无法由false改为true。这是js的语言考虑的,用户能向内部收缩控制权限。
obj = {
a:'1'
}
Object.defineProperty(obj,
'a',{
configurable: false
});
obj.a = 2;
Object.defineProperty(obj, // 报错
'a',{
enumerable: false
});
删除属性:默认会失败,严格模式下会报错。
Object.defineProperty( myObject, "a", {
value: 2,
writable: true,
configurable: false,
enumerable: true } );
console.log(myObject.a); // 2
delete myObject.a;
console.log(myObject.a); // 2
enumerable
: 如果为true
,则该属性会在遍历对象属性时出现(例如,在for...in
循环或者Object.keys()
方法中)。如果为false
,则不会出现在属性枚举中。
访问器描述符
当给一个属性定义 getter
、setter
或者两者都有时,这个属性会被定义为访问器属性,他所拥有的描述符是访问描述符,对于访问描述符来说,JavaScript会忽略它们的value和 writable 特性,取而代之的是关心set和get(还有configurable和enumerable)特性.
通过直接在对象中定义和 defineProperty(…)中的显式定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当作属性访问的返回值
obj = {
get a(){
return 1;
}
}
console.log(obj.a); // 1
console.log(Object.getOwnPropertyDescriptors(obj));
//{
// get: [Function: get a],
// set: undefined,
// enumerable: true,
// configurable: true
//}
Object.defineProperty( myObject, // 目标对象
"b", // 属性名
{ // 描述符
get: function(){ return this.a * 2 }, // 给b设置一个getter
enumerable: true }// 确保b会出现在对象的属性列表中
出现set和get的原因是因为可以对数据格式进行一些检验。
属性的获取与访问:[[Get]] and [[Put]]
[[Get]]
和 [[Put]]
并不是你可以直接调用的方法,而是 JavaScript 规范中定义的内部操作
[[Get]]
[[Get]]
内部方法用于获取对象属性的值。当尝试访问一个对象的属性时,JavaScript 引擎会调用这个内部方法来决定返回什么值。如果该属性是一个数据属性,则直接返回它的 value
;如果该属性是一个访问器属性,并且它有 get
函数,则调用 get
函数并返回其结果。如果没找到,则会在原型链上查找。
无论如何都没找到,则会返回undefind。
var myObject = { a:2 };
myObject.b; // undefined
但是假如属性的值为undefind呢?我们如何区分是属性不存在 还是值为undefind呢?稍后会区分这两种情况。
var myObject = { a: undefined };
myObject.a; // undefined
myObject.b; // undefined
[[Put]]
[[Put]]
内部方法用于设置对象属性的值。当你给对象的一个属性赋值时,JavaScript 引擎会调用 [[Put]]
来处理这个赋值操作。
1.对于数据属性,它会将提供的值写入 value
特性中(前提是 writable
特性允许)。对于访问器属性,如果有 set
函数,则调用 set
函数并将提供的值传递给它。
2.属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,在 严格模式下抛出TypeError异常。
3. 如果都不是,将该值设置为属性的值。
不变性
ES5中可以通过很多种方法来实现。但是它们只会影响目标对象和它的直接属性。如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的出现set和get的原因是因为可以对数据格式进行一些检验。
- 对象常量在 结合writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、 重定义或者删除):
var myObject = {};
Object.defineProperty(
myObject, "FAVORITE_NUMBER", {
value: 42, writable: false,
configurable: false
} );
- 禁止扩展如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.prevent Extensions(…):
var myObject = { a:2 };
Object.preventExtensions( myObject );
myObject.b = 3; myObject.b; // undefined 在非严格模式下,创建属性b会静默失败。在严格模式下,将会抛出TypeError错误。
- Object.seal(…) 这个方法实际上会在一个现有对象上调用 Object.preventExtensions(…) 并把所有现有属性标记为configurable:false。 所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以 修改属性的值)。
4.Object.freeze(…) 这个方法实际上会在一个现有对象上调用 Object.seal(…) 并把所有“数据访问”属性标记为writable:false,这样就无法修改它们的值。(不过引用的对象的内容还是能修改)
存在性
如myObject.a的属性访问返回值可能是undefined,但是这个值有可能 是属性中存储的undefined,也可能是因为属性不存在所以返回undefined。我们可以在不访问属性值的情况下判断对象中是否存在这个属性
in 操作符会检查属性是否在对象及其[[Prototype]]原型链中(参见第5章)。相比之下, hasOwnProperty(…) 只会检查属性是否在myObject 对象中,不会检查[[Prototype]] 链。
var myObject = { a:2 }; ("a" in myObject); // true
("b" in myObject); // false
myObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "bmyObject" ); // false
console.log('toString' in myObject);//true
console.log(myObject.hasOwnProperty('toString'));false
看起来in操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。对于数组来说这个区别非常重要,4 in [2, 4, 6]的结 果并不是你期待的True,因为[2, 4, 6]这个数组中包含的属性名是0、1、 2,没有4。
枚举
var myObject = { };
Object.defineProperty(
myObject,
"a",
// 让a像普通属性一样可以枚举
{ enumerable: true, value: 2 }
);
Object.defineProperty(
myObject,
"b",
// 让b不可枚举
{ enumerable: false, value: 3 }
);
myObject.b; // 3
("b" in myObject); // true
myObject.hasOwnProperty( "b" ); // true
// .......
for (var k in myObject) {
console.log( k, myObject[k] );
}
// "a" 2
for...in
循环:
可枚举属性会出现在 for...in
循环的结果中。 注意:for...in
也会遍历原型链上的可枚举属性。因此通常不使用他来遍历数组。
Object.keys()
:
返回一个包含所有可枚举自身属性名的数组。他不会遍历原型链上的属性。
Object.getOwnPropertyNames()
:
这个方法返回的对象键列表包括所有属性(无论是可枚举还是不可枚举)。他不会遍历原型链上的属性