js-你不知道的对象

-## 对象属性

对象属性访问

对象的属性有两种方式访问
1.使用点符号(.)
这是最常见和直观的方式,适用于已知属性名且属性名符合标识符规则的情况(即不能包含空格、特殊字符或保留字,并且不能以数字开头)。

let person = {
    name: 'Alice',
    age: 25
};

console.log(person.name); // 输出: Alice
console.log(person.age);  // 输出: 25
  1. 使用方括号符号([])
    这种方式提供了更大的灵活性,允许你用变量或表达式作为键来访问属性,适用于动态属性名或包含特殊字符的属性名。
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引擎内部管理。
属性描述符有两种类型:

  1. 数据描述符:是那些拥有值的描述符,它们可以被读取和写入。
  2. 访问器描述符:是那些拥有至少一个获取函数(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,则不会出现在属性枚举中。
访问器描述符

当给一个属性定义 gettersetter 或者两者都有时,这个属性会被定义为访问器属性,他所拥有的描述符是访问描述符,对于访问描述符来说,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的原因是因为可以对数据格式进行一些检验。

  1. 对象常量在 结合writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、 重定义或者删除):
 var myObject = {}; 
 Object.defineProperty( 
 myObject, "FAVORITE_NUMBER", { 
 value: 42, writable: false, 
 configurable: false 
 } );
  1. 禁止扩展如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.prevent Extensions(…):
var myObject = { a:2 }; 
Object.preventExtensions( myObject ); 
myObject.b = 3; myObject.b; // undefined 在非严格模式下,创建属性b会静默失败。在严格模式下,将会抛出TypeError错误。
  1. 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()

这个方法返回的对象键列表包括所有属性(无论是可枚举还是不可枚举)。他不会遍历原型链上的属性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值