JS OBJECT 3 构造函数

83 阅读3分钟

在 ES6 的 class 语法普及之前,构造函数是 JavaScript 中创建特定类型对象的主要方式。即使有了 class,理解构造函数对于深入理解 JavaScript 的对象模型和原型链仍然至关重要,因为 class 本质上是构造函数和原型继承的语法糖。

什么是构造函数?

  1. 本质: 构造函数本质上就是一个普通的 JavaScript 函数。
  2. 目的: 它的主要目的是初始化(创建)新对象。
  3. 调用方式: 它通过 new 关键字来调用。

new 关键字做了什么?

当使用 new 关键字调用一个函数(即构造函数)时,会发生以下四个步骤:

  1. 创建新对象: 一个新的、空的对象被创建。这个对象的内部 [[Prototype]](或 proto)会指向构造函数的 prototype 属性。

  2. 绑定 this: 构造函数内部的 this 关键字会被绑定到这个新创建的对象上。

  3. 执行函数体: 构造函数的代码被执行。通常,这部分代码会使用 this 给新对象添加属性和方法。

  4. 返回对象:

    • 如果构造函数没有显式地 return 一个对象,那么 new 表达式的结果就是第一步创建并经过第三步初始化的那个新对象(即 this)。
    • 如果构造函数显式地 return 了一个非原始值(例如,另一个对象、数组、函数等),那么 new 表达式的结果就是这个被 return 的值,而不是新创建的对象。
    • 如果构造函数显式地 return 了一个原始值(字符串、数字、布尔值、null、undefined、Symbol、BigInt),这个 return 语句会被忽略,new 表达式的结果仍然是第一步创建的新对象。

基本示例

 // 1. 定义构造函数 (约定俗成,首字母大写)
function Person(name, age) {
  // 3. 执行函数体: `this` 指向新创建的对象
  console.log('Inside constructor, this:', this); // {} (initially)
  this.name = name;
  this.age = age;

  // 不推荐:方法最好放在 prototype 上共享
  // this.sayHello = function() {
  //   console.log(`Hello, my name is ${this.name}`);
  // }

  // 4. 隐式返回 this (因为没有显式 return 对象)
}

// 2. 使用 new 调用构造函数
const person1 = new Person('Alice', 30);
const person2 = new Person('Bob', 25);

// 访问属性
console.log(person1.name); // Output: Alice
console.log(person2.age);  // Output: 25

// person1 和 person2 是 Person 的实例
console.log(person1 instanceof Person); // Output: true
console.log(person2 instanceof Person); // Output: true

// 实例的 constructor 属性指向构造函数本身
console.log(person1.constructor === Person); // Output: true
    

使用构造函数创建对象的缺点:

每创建一个都会开辟一个内存空间,但是创建的对象有很多属性是一样的,没有必要浪费、 ,我们需要使用prototype属性了

好的,我们来深入探讨一下 JavaScript 中 Object 的原型,也就是 Object.prototype。

Object.prototype 在 JavaScript 的原型链中扮演着至关重要的角色。可以把它理解为绝大多数 JavaScript 对象(除了一些特殊情况)原型链的“终点”或“根基”。

核心概念:

  1. 原型链的顶端: 当你创建一个普通的对象(例如,使用对象字面量 {}、new Object(),或者通过大多数构造函数创建的实例最终都会继承自它)时,如果你试图访问该对象上不存在的属性或方法,JavaScript 引擎会沿着原型链向上查找。这个查找过程最终会到达 Object.prototype。
  2. 提供基础方法: Object.prototype 对象本身包含了一系列所有(或绝大多数)对象都可以使用的基础方法。这些方法是 JavaScript 对象模型的基础。
  3. [[Prototype]] 为 null: Object.prototype 对象自身的内部 [[Prototype]](或 proto)属性是 null。这意味着原型链到此结束,如果在 Object.prototype 上也找不到某个属性或方法,查找就会停止,并通常返回 undefined。

Object.prototype 上常见的内置方法:

这些方法默认情况下会被几乎所有 JavaScript 对象继承:

  • Object.prototype.toString(): 返回表示该对象的字符串。默认情况下,对于普通对象,它返回 "[object Object]"。很多内置类型(如 Array, Date, Function)会重写此方法以提供更有意义的字符串表示(例如,数组返回 "[object Array]",可以通过 Object.prototype.toString.call(yourArray) 来验证)。这个方法经常被用来检测对象的具体类型。
  • Object.prototype.hasOwnProperty(propertyName): 返回一个布尔值,指示对象自身(而不是其原型链)是否具有指定名称的属性。这是检查属性是否为对象“自有”的关键方法,常用于 for...in 循环中过滤掉继承来的属性。
  • Object.prototype.isPrototypeOf(object): 返回一个布尔值,指示调用它的对象(例如 Object.prototype)是否存在于指定对象 (object) 的原型链上。
  • Object.prototype.valueOf(): 返回对象的原始值。对于普通对象,默认返回对象本身。像 Number, String, Boolean 包装对象会重写此方法以返回它们包装的原始值。
  • Object.prototype.propertyIsEnumerable(propertyName): 返回一个布尔值,指示指定的自身属性是否是可枚举的(即是否能被 for...in 循环或 Object.keys() 遍历到)。
  • Object.prototype.constructor: 指向创建此对象的构造函数。对于 Object.prototype 来说,它指向 Object 构造函数本身。普通对象会继承这个属性。

示例:

 // 1. 对象字面量
const obj = { name: 'Alice' };

// obj 自身没有 toString 方法,它会从 Object.prototype 继承
console.log(obj.toString()); // Output: "[object Object]"

// obj 自身有 name 属性
console.log(obj.hasOwnProperty('name')); // Output: true

// obj 自身没有 hasOwnProperty 方法,它从 Object.prototype 继承
// 但 hasOwnProperty 是用来检查 *obj* 的属性,而不是检查 Object.prototype 的
console.log(obj.hasOwnProperty('toString')); // Output: false (toString 是继承的)

// 验证 obj 的原型是 Object.prototype
console.log(Object.getPrototypeOf(obj) === Object.prototype); // Output: true

// Object.prototype 是 obj 的原型
console.log(Object.prototype.isPrototypeOf(obj)); // Output: true

// Object.prototype 的原型是 null
console.log(Object.getPrototypeOf(Object.prototype)); // Output: null


// 2. 数组
const arr = [1, 2, 3];

// Array.prototype 重写了 toString
console.log(arr.toString()); // Output: "1,2,3"

// 使用 Object.prototype.toString 来检测类型
console.log(Object.prototype.toString.call(arr)); // Output: "[object Array]"

// arr 继承了 hasOwnProperty
console.log(arr.hasOwnProperty('length')); // Output: true (length 是数组自身的属性)
console.log(arr.hasOwnProperty('slice'));  // Output: false (slice 是从 Array.prototype 继承的)

// 数组的原型链: arr -> Array.prototype -> Object.prototype -> null
console.log(Object.getPrototypeOf(arr) === Array.prototype);       // true
console.log(Object.getPrototypeOf(Array.prototype) === Object.prototype); // true


// 3. 自定义构造函数
function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function() { console.log('Hello'); };

const person = new Person('Bob');

// person -> Person.prototype -> Object.prototype -> null
console.log(person.hasOwnProperty('name'));     // true (自身属性)
console.log(person.hasOwnProperty('sayHello')); // false (继承自 Person.prototype)
console.log(person.hasOwnProperty('toString')); // false (继承自 Object.prototype)

// 可以调用继承自 Object.prototype 的 toString
console.log(person.toString()); // Output: "[object Object]" (除非 Person.prototype.toString 被重写)

console.log(Object.getPrototypeOf(person) === Person.prototype);             // true
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype); // true
    

重要警告:不要修改 Object.prototype!

虽然技术上可以向 Object.prototype 添加新的属性或方法,但这被认为是非常糟糕的做法(称为 "monkey patching" 全局原型),原因如下:

  1. 全局污染: 你添加的属性/方法会出现在几乎所有的对象上(通过 for...in 循环等),包括你可能不期望的地方,比如库或框架内部的对象。
  2. 命名冲突: 你添加的属性名可能会与对象自身的属性名、其他库添加的属性名或未来 JavaScript 版本中引入的标准方法名冲突。
  3. 代码不可预测性: 其他开发者(或未来的你)可能不会预料到 Object.prototype 被修改过,导致难以调试的问题。
  4. 性能问题: 修改内置原型可能会破坏 JavaScript 引擎的优化。

如果需要为对象添加通用功能,应该使用继承(通过 class extends 或原型链)、组合或者创建独立的工具函数。

特例:Object.create(null)

可以使用 Object.create(null) 来创建一个完全没有原型的对象。这个对象的 [[Prototype]] 是 null,它不继承自 Object.prototype,因此它没有任何内置的对象方法(如 toString, hasOwnProperty 等)。

const pureObj = Object.create(null);
pureObj.data = 'some data';

console.log(Object.getPrototypeOf(pureObj)); // Output: null
// console.log(pureObj.toString()); // TypeError: pureObj.toString is not a function
// console.log(pureObj.hasOwnProperty('data')); // TypeError: pureObj.hasOwnProperty is not a function

// 需要使用 Object 类的静态方法来操作这种对象
console.log(Object.prototype.hasOwnProperty.call(pureObj, 'data')); // Output: true
    

这种“纯净”对象有时被用作字典或哈希映射,以避免与原型链上的属性名发生意外冲突。

每个 JavaScript 函数(除了箭头函数)都有一个特殊的 prototype 属性,它是一个对象。这个 prototype 对象对于构造函数尤其重要:

  • 共享方法/属性: 你可以把希望所有实例共享的方法或属性添加到构造函数的 prototype 对象上。这样做比在构造函数内部使用 this 添加方法更节省内存,因为方法只需要在内存中存在一份(在 prototype 对象上),而不是每个实例都复制一份。
  • 原型链: 当你试图访问一个实例的属性或方法时,如果实例本身没有这个属性/方法,JavaScript 引擎会沿着原型链向上查找,也就是查找实例的 [[Prototype]](它指向构造函数的 prototype 对象)。

示例:使用 prototype 添加共享方法

function Car(make, model) {
  this.make = make;
  this.model = model;
}

// 将方法添加到 Car 的 prototype 对象上
Car.prototype.displayInfo = function() {
  console.log(`Car: ${this.make} ${this.model}`);
};

Car.prototype.startEngine = function() {
  console.log(`${this.make} ${this.model} engine started.`);
};

const myCar = new Car('Toyota', 'Camry');
const yourCar = new Car('Honda', 'Civic');

myCar.displayInfo();    // Output: Car: Toyota Camry
yourCar.startEngine(); // Output: Honda Civic engine started.

// displayInfo 和 startEngine 方法并没有直接在 myCar 或 yourCar 对象上
console.log(myCar.hasOwnProperty('displayInfo')); // Output: false
console.log(yourCar.hasOwnProperty('startEngine')); // Output: false

// 它们存在于 myCar 和 yourCar 的原型(即 Car.prototype)上
console.log(Car.prototype.hasOwnProperty('displayInfo')); // Output: true
console.log(Object.getPrototypeOf(myCar) === Car.prototype); // Output: true
    

总结

  1. 命名约定: 构造函数名通常以大写字母开头(PascalCase)。
  2. new 关键字: 必须使用 new 来调用构造函数以创建实例。如果忘记 new,this 的指向会不符合预期(在非严格模式下可能指向全局对象 window,在严格模式下是 undefined),并且函数可能不会返回期望的对象。
  3. this: 在构造函数内部,this 指向正在创建的新实例。
  4. prototype: 用于定义所有实例共享的方法和属性,是实现继承的基础。
  5. instanceof: 用于检查一个对象是否是某个构造函数的实例。
  6. constructor 属性: 实例通常有一个 constructor 属性,指向创建它的构造函数(实际上是继承自 prototype.constructor)。
  7. ES6 class: 是构造函数/原型模式的语法糖,提供了更现代、更简洁的语法,但底层机制是相同的。理解构造函数有助于理解 class 的工作原理。