一、对象的基本定义

JavaScript对象是一种复合数据类型,用于存储键值对(属性和方法)的集合。在JavaScript中,对象是引用类型的数据结构,几乎所有JavaScript值(除了原始类型)都是对象。

根据JavaScript语言规范,对象是:无序的属性集合,每个属性可以包含原始值、对象或函数。

对象的核心特性
  • 键值对集合:对象由零个或多个键值对组成。

  • 动态性:可以随时添加、修改或删除属性。

  • 引用类型:对象赋值时传递的是引用而非副本。

  • 原型继承:对象可以从其他对象继承属性和方法。

  • 属性的多样性:属性可以是数据属性或访问器属性

对象的内部表示

在JavaScript引擎中,对象通常以哈希表或类似结构实现,包含:

  • 属性名(字符串或Symbol)。

  • 属性值的引用。

  • 指向原型对象的内部链接[[Prototype]])。

二、对象的创建方式

对象字面量语法(最常用)
const person = {
  name: "雷龙",
  age: 28,
  greet() { // ES6方法简写
    return `你好,我是${this.name}`;
  }
};
工厂函数模式
function createPerson(name, age) {
  // 创建并返回一个新对象
  const person = {};
  person.name = name;
  person.age = age;
  person.greet = function() {
    return `你好,我是${this.name}`;
  };
  return person; // 显式返回对象
}

const person1 = createPerson("追风", 28);
const person2 = createPerson("破军", 30);
工厂函数方式特征
  • 函数名通常为动词形式(createXXX)。

  • 在函数内部创建一个新对象。

  • 为对象添加属性和方法。

  • 显式返回创建的对象。

  • 创建的对象无法识别其类型(person1 instanceof createPerson 为 false)。

构造函数方式
// Object构造函数
const person = new Object();
person.name = "追风";
person.age = 25;
person.greet = function() {
  return `你好,我是${this.name}`;
};

// 自定义构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function() {
    return `你好,我是${this.name}`;
  };
}

const person1 = new Person("追风", 28);
const person2 = new Person("破军", 30);
构造函数方式特征
  • 使用函数定义对象类型(通常首字母大写)。

  • 通过 new 关键字调用函数。

  • 在函数内部使用 this 关键字指向新创建的对象。

  • 不需要显示返回对象。

  • 创建的对象是该构造函数的实例(person1 instanceof Person 为 true)。

原型模式
function Person() {}

// 方法定义在原型上,所有实例共享
Person.prototype.name = "默认名";
Person.prototype.greet = function() {
  return `你好,我是${this.name}`;
};

const person1 = new Person();
person1.name = "烈阳";
组合模式(构造函数+原型)
function Person(name, age) {
  // 实例属性
  this.name = name;
  this.age = age;
  this.friends = ["黄九"];
}

// 共享方法
Person.prototype.greet = function() {
  return `你好,我是${this.name}`;
};
ES6类语法
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `你好,我是${this.name}`;
  }
  
  static createAdult(name) {
    return new Person(name, 18);
  }
}
 Object.create()方法
// 创建以指定对象为原型的新对象
const personProto = {
  greet() {
    return `你好,我是${this.name}`;
  }
};

const person = Object.create(personProto, {
  name: { value: "陆通", writable: true }
});

三、原型与继承机制

原型链(Prototype Chain)
  • 每个对象都有一个内部的 [[Prototype]] 属性,指向它的原型对象。

  • 当访问对象的属性时,如果对象本身没有该属性,会沿着原型链向上查找。

  • 原型链的顶端是 Object.prototype ,其 [[Prototype]] 为 null 

原型继承的实现方式
// 原型继承示例
function SuperType() {
  this.superValue = true;
}

SuperType.prototype.getSuperValue = function() {
  return this.superValue;
};

function SubType() {
  this.subValue = false;
}

// 关键:将子类型的原型设为父类型的实例
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function() {
  return this.subValue;
};

const instance = new SubType();
console.log(instance.getSuperValue()); // true
 ES6的类继承
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name}发出叫声`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 必须先调用super()
    this.breed = breed;
  }
  
  speak() {
    console.log(`${this.name}汪汪叫`); // 重写父类方法
  }
}
const shiba = new Dog('小柴', '柴犬');
shiba.speak(); // 小柴汪汪叫

四、对象的属性类型

1. 数据属性的四个特性
  • value:属性的值(赋值的值)。

  • writable:是否可修改(默认值:true)。

  • enumerable:是否可枚举(如在for...in循环中)(默认值:true,即可枚举)。

  • configurable:是否可配置(如删除属性,默认值:true,即可配置)。

// 创建一个空对象
const user = {};

// 添加一个具有自定义特性的属性
Object.defineProperty(user, 'username', {
  value: 'admin',
  writable: true,
  enumerable: true,
  configurable: true
});

console.log(user.username); // 'admin'
value特性示例
const obj = {};

// 设置value
Object.defineProperty(obj, 'test', {
  value: 'initial value'
});

console.log(obj.test); // 'initial value'

// 尝试修改value(如果writable为true)
Object.defineProperty(obj, 'test', {
  value: 'new value'
});

console.log(obj.test); // 'new value'
writable特性示例
const product = {};

// 创建不可写的属性
Object.defineProperty(product, 'price', {
  value: 100,
  writable: false  // 不可修改
});

console.log(product.price); // 100

// 尝试修改(在严格模式下会抛出错误,非严格模式下静默失败)
try {
  product.price = 150;
  console.log(product.price); // 仍然是100,因为writable为false
} catch (e) {
  console.log('错误:', e.message);
}
enumerable特性示例
const car = {};

// 添加可枚举属性
Object.defineProperty(car, 'brand', {
  value: 'Toyota',
  enumerable: true
});

// 添加不可枚举属性
Object.defineProperty(car, 'engine', {
  value: 'V6',
  enumerable: false
});

// 直接访问都可以
console.log(car.brand);  // 'Toyota'
console.log(car.engine); // 'V6'

// for...in循环只显示可枚举属性
console.log('\nfor...in循环结果:');
for (const prop in car) {
  console.log(prop + ': ' + car[prop]);
  // 只输出: brand: Toyota
}

// Object.keys()也只返回可枚举属性
console.log('\nObject.keys()结果:');
console.log(Object.keys(car)); // ['brand']

// Object.getOwnPropertyNames()返回所有自有属性,无论是否可枚举
console.log('\nObject.getOwnPropertyNames()结果:');
console.log(Object.getOwnPropertyNames(car)); // ['brand', 'engine']
configurable特性示例
const settings = {};

// 创建可配置的属性
Object.defineProperty(settings, 'theme', {
  value: 'dark',
  configurable: true
});

// 创建不可配置的属性
Object.defineProperty(settings, 'language', {
  value: 'zh-CN',
  configurable: false
});

// 尝试删除可配置属性
console.log(settings.theme); // 'dark'
delete settings.theme;
console.log(settings.theme); // undefined(已删除)

// 尝试删除不可配置属性
console.log(settings.language); // 'zh-CN'
try {
  delete settings.language;
  console.log(settings.language); // 仍然是'zh-CN',删除失败
} catch (e) {
  console.log('错误:', e.message);
}

// 尝试修改不可配置属性的特性
console.log('尝试修改language的writable特性:');
try {
  Object.defineProperty(settings, 'language', {
    writable: true
  });
  console.log('成功修改');
} catch (e) {
  console.log('错误:', e.message); // 在严格模式下会抛出错误
}
实际应用示例

下面是一个综合示例,展示如何使用这些特性创建一个简单的只读常量对象:

// 创建一个包含常量的对象
const CONFIG = {};

// 定义不可写、不可配置的常量属性
Object.defineProperty(CONFIG, 'API_URL', {
  value: 'https://api.example.com',
  writable: false,
  configurable: false,
  enumerable: true
});

Object.defineProperty(CONFIG, 'TIMEOUT', {
  value: 5000,
  writable: false,
  configurable: false,
  enumerable: true
});

// 或者使用Object.defineProperties一次定义多个属性
Object.defineProperties(CONFIG, {
  DEBUG: {
    value: true,
    writable: false,
    configurable: false,
    enumerable: true
  },
  VERSION: {
    value: '1.0.0',
    writable: false,
    configurable: false,
    enumerable: true
  }
});

// 尝试修改会失败
CONFIG.API_URL = 'https://new-api.example.com';
console.log(CONFIG.API_URL); // 仍然是 'https://api.example.com'

// 枚举所有配置
console.log('\n配置项:');
for (const key in CONFIG) {
  console.log(key + ': ' + CONFIG[key]);
}
数据属性之间的关系

当 configurable 为 false 时,有一些限制:

  1. 不能删除该属性。

  2. 不能将 configurable 从 false 改为 true。

  3. 不能修改 enumerable 特性。

  4. 可以修改 writable 从 true 改为 false(但不能反向)。

  5. 可以修改 value 特性的值(如果 writable 为 true)。

const limitedObj = {};

// 先创建一个可配置的属性
Object.defineProperty(limitedObj, 'test', {
  value: 1,
  writable: true,
  configurable: true
});

// 然后将其设为不可配置
Object.defineProperty(limitedObj, 'test', {
  configurable: false
});

// 现在可以将writable改为false
Object.defineProperty(limitedObj, 'test', {
  writable: false
});

// 但不能再改回true
console.log('尝试将writable从false改回true:');
try {
  Object.defineProperty(limitedObj, 'test', {
    writable: true
  });
} catch (e) {
  console.log('错误:', e.message);
}

通过这些示例,我们可以看到JavaScript对象的属性特性如何控制属性的行为,以及如何使用这些特性来实现更精细的对象属性管理。

2. 访问器属性的四个特性
  • get:获取属性值的函数。

  • set:设置属性值的函数。

  • enumerable:是否可枚举。

  • configurable:是否可配置。

基本示例
// 示例1: 使用Object.defineProperty()定义访问器属性
const person = {
  _name: "七杀" // 下划线通常表示内部属性
};

// 定义访问器属性name
Object.defineProperty(person, "name", {
  get() {
    console.log("获取name属性");
    return this._name;
  },
  set(newValue) {
    console.log("设置name属性为:", newValue);
    this._name = newValue;
  },
  enumerable: true,
  configurable: true
});

// 使用访问器属性
console.log(person.name); // 输出: 获取name属性 七杀
person.name = "柔柔";      // 输出: 设置name属性为: 柔柔
console.log(person.name); // 输出: 获取name属性 柔柔
在对象字面量中定义访问器属性
// 示例2: 在对象字面量中使用get/set语法糖
const person2 = {
  _name: "黄仙儿",
  _age: 25,
  
  // 定义getter和setter
  get name() {
    return this._name;
  },
  set name(value) {
    this._name = value;
  },
  
  // 使用访问器属性进行数据验证或计算
  get age() {
    return this._age;
  },
  set age(value) {
    if (value >= 0 && value <= 150) {
      this._age = value;
    } else {
      console.log("年龄必须在0-150之间");
    }
  }
};

console.log(person2.name); // 黄仙儿
person2.age = 160;        // 年龄必须在0-150之间
console.log(person2.age); // 25 (未被修改)

访问器属性通过 Object.defineProperty() 或 Object.defineProperties() 方法定义,也可以在对象字面量中使用get/set语法糖定义。

// 属性批量定义
const person = {};

Object.defineProperties(person, {
  firstName: {
    value: "建",
    writable: true
  },
  lastName: {
    value: "木",
    writable: true
  },
  fullName: {
    get() {
      return `${this.firstName}${this.lastName}`;
    }
  }
});

使用Object.getOwnPropertyDescriptor()方法查看属性的特性:

// 查看访问器属性的特性
const descriptor = Object.getOwnPropertyDescriptor(person, "name");
console.log(descriptor);
/* 输出:
{
  get: [Function: get],
  set: [Function: set],
  enumerable: true,
  configurable: true
}
*/

// 注意:访问器属性没有value和writable特性
console.log("value特性:", descriptor.value);         // undefined
console.log("writable特性:", descriptor.writable);   // undefined
3. 注意事项

访问器属性和数据属性的区别:

  • 数据属性有:value、writable、enumerable、configurable。

  • 访问器属性有:get、set、enumerable、configurable。

  • 两者不能同时拥有(会抛出错误)。

严格模式下的行为:

  • 在严格模式下,尝试写入一个只有getter没有setter的属性会抛出错误。

  • 尝试读取一个只有setter没有getter的属性会返回undefined(非严格模式)。

继承:访问器属性可以被继承,子类可以覆盖父类的getter/setter。

五、对象的高级特性

对象的可扩展性
  • Object.preventExtensions():阻止添加新属性。

  • Object.seal():阻止添加/删除属性(将所有属性的configurable设为false)。

  • Object.freeze():阻止添加/删除/修改属性(冻结对象)。

  • Object.isExtensible(), Object.isSealed(), Object.isFrozen():检查对象状态。

Object.preventExtensions():阻止添加新属性

// 创建一个普通对象
let obj1 = { name: 'John', age: 30 };

// 检查对象是否可扩展
console.log(Object.isExtensible(obj1)); // true

// 阻止对象扩展
Object.preventExtensions(obj1);

// 尝试添加新属性(在严格模式下会抛出错误)
try {
  obj1.city = 'New York';
  console.log(obj1.city); // undefined(非严格模式下静默失败)
} catch (e) {
  console.log(e.message); // 在严格模式下会显示错误信息
}

// 验证对象是否仍然可扩展
console.log(Object.isExtensible(obj1)); // false

// 仍然可以修改和删除现有属性
obj1.name = 'Alice';
console.log(obj1.name); // 'Alice'

delete obj1.age;
console.log(obj1.age); // undefined

Object.seal():阻止添加/删除属性(将所有属性的configurable设为false)

// 创建一个普通对象
let obj2 = { name: 'John', age: 30 };

// 检查对象是否被密封
console.log(Object.isSealed(obj2)); // false

// 密封对象
Object.seal(obj2);

// 尝试添加新属性(在严格模式下会抛出错误)
try {
  obj2.city = 'New York';
  console.log(obj2.city); // undefined(非严格模式下静默失败)
} catch (e) {
  console.log(e.message); // 在严格模式下会显示错误信息
}

// 尝试删除现有属性(在严格模式下会抛出错误)
try {
  delete obj2.age;
  console.log(obj2.age); // 30(非严格模式下静默失败,属性仍然存在)
} catch (e) {
  console.log(e.message); // 在严格模式下会显示错误信息
}

// 验证对象是否被密封
console.log(Object.isSealed(obj2)); // true

// 仍然可以修改现有属性的值
obj2.name = 'Bob';
console.log(obj2.name); // 'Bob'

// 尝试修改属性的描述符(会失败,因为configurable现在为false)
try {
  Object.defineProperty(obj2, 'name', { writable: false });
  console.log('Success'); // 在非严格模式下不会执行到这里
} catch (e) {
  console.log(e.message); // 在严格模式下会显示错误信息
}

Object.freeze():阻止添加/删除/修改属性(冻结对象)

// 创建一个普通对象
let obj3 = { name: 'John', age: 30 };

// 检查对象是否被冻结
console.log(Object.isFrozen(obj3)); // false

// 冻结对象
Object.freeze(obj3);

// 尝试添加新属性(在严格模式下会抛出错误)
try {
  obj3.city = 'New York';
  console.log(obj3.city); // undefined(非严格模式下静默失败)
} catch (e) {
  console.log(e.message); // 在严格模式下会显示错误信息
}

// 尝试删除现有属性(在严格模式下会抛出错误)
try {
  delete obj3.age;
  console.log(obj3.age); // 30(非严格模式下静默失败,属性仍然存在)
} catch (e) {
  console.log(e.message); // 在严格模式下会显示错误信息
}

// 尝试修改现有属性的值(在严格模式下会抛出错误)
try {
  obj3.name = 'Charlie';
  console.log(obj3.name); // 'John'(非严格模式下静默失败,值未改变)
} catch (e) {
  console.log(e.message); // 在严格模式下会显示错误信息
}

// 验证对象是否被冻结
console.log(Object.isFrozen(obj3)); // true

// 尝试修改属性的描述符(会失败)
try {
  Object.defineProperty(obj3, 'name', { writable: true });
  console.log('Success'); // 在非严格模式下不会执行到这里
} catch (e) {
  console.log(e.message); // 在严格模式下会显示错误信息
}

Object.isExtensible(), Object.isSealed(), Object.isFrozen():检查对象状态

// 创建普通对象
let normalObj = { prop: 'value' };
console.log('普通对象:');
console.log('可扩展:', Object.isExtensible(normalObj));  // true
console.log('已密封:', Object.isSealed(normalObj));     // false
console.log('已冻结:', Object.isFrozen(normalObj));     // false

// 创建不可扩展对象
let preventExtObj = { prop: 'value' };
Object.preventExtensions(preventExtObj);
console.log('\n不可扩展对象:');
console.log('可扩展:', Object.isExtensible(preventExtObj));  // false
console.log('已密封:', Object.isSealed(preventExtObj));     // false
console.log('已冻结:', Object.isFrozen(preventExtObj));     // false

// 创建密封对象
let sealedObj = { prop: 'value' };
Object.seal(sealedObj);
console.log('\n密封对象:');
console.log('可扩展:', Object.isExtensible(sealedObj));  // false
console.log('已密封:', Object.isSealed(sealedObj));     // true
console.log('已冻结:', Object.isFrozen(sealedObj));     // false

// 创建冻结对象
let frozenObj = { prop: 'value' };
Object.freeze(frozenObj);
console.log('\n冻结对象:');
console.log('可扩展:', Object.isExtensible(frozenObj));  // false
console.log('已密封:', Object.isSealed(frozenObj));     // true
console.log('已冻结:', Object.isFrozen(frozenObj));     // true
注意事项

严格模式 vs 非严格模式:

  • 在非严格模式下,违反这些限制的操作会静默失败。

  • 在严格模式下(使用 "use strict";),违反限制会抛出 TypeError 异常。

深度vs浅度操作:

  • 这些方法只对对象本身进行操作,不影响嵌套对象。

  • 要完全冻结嵌套对象,需要递归应用这些方法。

状态关系:

  • 冻结对象一定是密封的,密封对象一定是不可扩展的。

  • 因此 Object.isFrozen(obj) 为 true 时,Object.isSealed(obj) 和 Object.isExtensible(obj) 分别为 true 和 false。

性能优化:

  • 浏览器可能会对密封或冻结的对象进行性能优化,因为它们的结构不会改变。

对象的合并与复制
// 浅拷贝
const obj1 = { a: 1, b: { c: 2 } };
const obj2 = Object.assign({}, obj1);

// 或使用展开运算符
const obj3 = { ...obj1 };

// 深拷贝(简单实现)
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (obj instanceof Date) return new Date(obj.getTime());
  if (obj instanceof Array) return obj.map(item => deepClone(item));
  
  const clonedObj = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clonedObj[key] = deepClone(obj[key]);
    }
  }
  return clonedObj;
}

const person = { name: "小脚姑娘", age: 20, city: "北京" };

// 遍历键
for (const key in person) {
  if (person.hasOwnProperty(key)) {
    console.log(key);
  }
}

// 获取所有键
const keys = Object.keys(person);

// 获取所有值
const values = Object.values(person);

// 获取所有键值对
const entries = Object.entries(person);

// 使用for...of遍历
for (const [key, value] of Object.entries(person)) {
  console.log(`${key}: ${value}`);
}

六、对象的上下文与this

this的绑定规则
  • 默认绑定:独立函数调用时,this指向全局对象。

  • 隐式绑定:作为对象方法调用时,this指向该对象。

  • 显式绑定:使用call()/apply()/bind()时,this指向指定对象。

  • 构造函数绑定:使用new时,this指向新创建的对象。

const person = {
  name: "水月",
  greet() {
    console.log(`你好,我是${this.name}`);
  },
  delayedGreet() {
    // 这里this指向person
    setTimeout(function() {
      // 这里this指向全局对象(非严格模式)
      console.log(`你好,我是${this.name}`);
    }, 1000);
  },
  delayedGreetArrow() {
    // 箭头函数继承外部作用域的this
    setTimeout(() => {
      console.log(`你好,我是${this.name}`); // this指向person
    }, 1000);
  }
};

person.greet(); // 你好,我是水月
person.delayedGreet(); // 你好,我是
person.delayedGreetArrow(); // 你好,我是水月

七、JavaScript对象的特殊性质

一切皆对象?

JavaScript中,除了原始类型(string、number、boolean、null、undefined、symbol、bigint),其他值都是对象。原始类型有对应的包装对象。

内置对象与宿主对象
  • 内置对象:Object、Array、Function、Date、RegExp等。

  • 宿主对象:由执行环境提供,如浏览器中的 Window、Document 等。

对象的相等性比较
// 引用比较
const obj1 = { a: 1 };
const obj2 = { a: 1 };
const obj3 = obj1;

console.log(obj1 == obj2); // false
console.log(obj1 === obj2); // false
console.log(obj1 === obj3); // true
// 深度比较需要手动实现或使用库
小知识

BigInt 是在 ECMAScript 2020(ES11)中正式引入的原始数据类型,用于表示任意精度的整数,可以表示超出 Number 类型范围的整数。

BigInt 的特点:

  • 使用后缀 n 来表示,例如 123n。

  • 可以表示任意大的整数,不受 Number 类型 2^53 - 1 限制。

  • 不能与 Number 类型直接进行混合运算。

  • typeof 操作符对 BigInt 返回 'bigint'。

示例:

const bigNum = 123456789012345678901234567890n;
console.log(typeof bigNum); // 输出: "bigint"

// 超出 Number 安全整数范围的计算
const maxSafeInt = Number.MAX_SAFE_INTEGER; // 9007199254740991
console.log(maxSafeInt + 1 === maxSafeInt + 2); // 输出: true (因为溢出了)

const bigIntValue = BigInt(maxSafeInt);
console.log(bigIntValue + 1n === bigIntValue + 2n); // 输出: false (BigInt 正确处理)

八、性能优化考虑

对象属性访问优化
  • 避免频繁修改对象结构。

  • 使用对象字面量一次性创建对象。

  • 对于频繁访问的属性,考虑使用局部变量缓存。

对象内存管理
  • 避免不必要的闭包引用导致的内存泄漏。

  • 不再使用的大型对象设置为null。

  • 注意循环引用问题。

总结

JavaScript对象是语言的核心构建块,其灵活性和强大功能支持了复杂的应用开发。通过深入理解对象的内部结构、原型继承机制、属性特性和各种高级特性,我们可以更高效地设计和实现JavaScript应用,避免常见陷阱,编写出更加健壮和可维护的代码。