什么是构造函数
构造函数 (Constructor Function) 是用于创建和初始化对象的特殊函数.
通过 new 关键字调用构造函数, 可以创建具有相同属性和方法的多个对象实例.
构造函数的类型
JavaScript 中的构造函数分为两类:
1. 内置构造函数
JavaScript 提供的系统构造函数, 用于创建基本类型的包装对象和内置对象:
javascript
// 基本类型包装对象
const num = new Number(42);
const str = new String("hello");
const bool = new Boolean(true);
// 内置对象
const obj = new Object();
const arr = new Array(1, 2, 3);
const fn = new Function("a", "b", "return a + b");
const date = new Date();
const regex = new RegExp("\\d+");
// 注意: Symbol 和 BigInt 不能用 new 调用
// const sym = new Symbol(); // TypeError: Symbol is not a constructor
// const bigInt = new BigInt(123); // TypeError: BigInt is not a constructor
2. 自定义构造函数
开发者定义的构造函数, 用于创建自定义类型的对象:
javascript
// 构造函数使用大驼峰命名 (PascalCase)
function Person(name, age) {
this.name = name;
this.age = age;
}
// 普通函数使用小驼峰命名 (camelCase)
function calculateAge(birthYear) {
return new Date().getFullYear() - birthYear;
}
命名规范
- 构造函数 : 使用大驼峰命名 (PascalCase), 如
Person,UserAccount,OrderManager - 普通函数 : 使用小驼峰命名 (camelCase), 如
getUserInfo,calculateTotal,handleClick
这种命名约定帮助开发者快速识别函数的用途.
new 运算符的工作原理
使用 new 关键字调用构造函数时, JavaScript 引擎会执行以下步骤:
执行步骤
javascript
function Person(name) {
// 步骤 1: 创建空对象 (引擎自动执行)
// const newObj = {};
// 步骤 2: 将空对象的 [[Prototype]] 指向构造函数的 prototype (引擎自动执行)
// Object.setPrototypeOf(newObj, Person.prototype);
// 步骤 3: 将 this 绑定到新对象 (引擎自动执行)
// 从这里开始, this 指向新创建的对象
// 步骤 4: 执行构造函数代码
this.name = name;
this.greet = function () {
console.log(`Hello, I'm ${this.name}`);
};
// 步骤 5: 返回新对象 (引擎自动执行, 除非显式返回对象)
// return newObj;
}
const person = new Person("Alice");
console.log(person.name); // Alice
person.greet(); // Hello, I'm Alice
不使用 new 的后果
如果不使用 new 关键字, 构造函数会作为普通函数执行:
javascript
function Person(name) {
this.name = name;
console.log("this", this); // 在浏览器中指向 window, 在严格模式下是 undefined
}
// ❌ 错误用法: 不使用 new
const person1 = Person("Charlie");
console.log(person1); // undefined (函数没有返回值)
console.log(window.name); // 'Charlie' (污染了全局对象!)
// ✅ 正确用法: 使用 new
const person2 = new Person("David");
console.log(person2); // Person { name: 'David' }
严格模式下的保护:
javascript
"use strict";
function Person(name) {
// 严格模式下, this 为 undefined
this.name = name; // TypeError: Cannot set property 'name' of undefined
}
const person = Person("Eve"); // 抛出错误
构造函数的基本使用
定义与调用
javascript
// 定义构造函数
function User(username, email) {
// 实例属性
this.username = username;
this.email = email;
this.createdAt = new Date();
}
// 使用 new 创建实例
const user1 = new User("alice", "alice@example.com");
const user2 = new User("bob", "bob@example.com");
console.log(user1.username); // alice
console.log(user2.username); // bob
// 验证实例类型
console.log(user1 instanceof User); // true
console.log(user1.constructor === User); // true
参数传递与默认值
ES6 参数默认值:
javascript
function Person(name = "Anonymous", age = 0) {
this.name = name;
this.age = age;
}
const p1 = new Person("Alice", 25);
const p2 = new Person("Bob");
const p3 = new Person();
console.log(p1); // Person { name: 'Alice', age: 25 }
console.log(p2); // Person { name: 'Bob', age: 0 }
console.log(p3); // Person { name: 'Anonymous', age: 0 }
对象参数解构:
javascript
function Person({ name = "Anonymous", age = 0, email = "no-email" } = {}) {
this.name = name;
this.age = age;
this.email = email;
}
// 灵活的调用方式
const p1 = new Person({ name: "Alice", age: 25 });
const p2 = new Person({ email: "bob@example.com" });
const p3 = new Person();
console.log(p1); // Person { name: 'Alice', age: 25, email: 'no-email' }
console.log(p2); // Person { name: 'Anonymous', age: 0, email: 'bob@example.com' }
console.log(p3); // Person { name: 'Anonymous', age: 0, email: 'no-email' }
this 的指向
javascript
function Person(name) {
this.name = name;
// this 指向新创建的实例对象
console.log("Inside constructor:", this);
this.regularMethod = function () {
// 普通函数中的 this 指向调用者
console.log("Regular method:", this);
};
this.arrowMethod = () => {
// 箭头函数中的 this 继承自构造函数的 this
console.log("Arrow method:", this);
};
}
const person = new Person("Alice");
// Inside constructor: Person { name: 'Alice' }
person.regularMethod();
// Regular method: Person { name: 'Alice', ... }
person.arrowMethod();
// Arrow method: Person { name: 'Alice', ... }
// 注意: 普通函数的 this 可以被改变
const borrowed = person.regularMethod;
borrowed(); // Regular method: undefined (严格模式) 或 window (非严格模式)
// 箭头函数的 this 不会改变
const borrowedArrow = person.arrowMethod;
borrowedArrow(); // Arrow method: Person { name: 'Alice', ... }
构造函数的返回值
默认返回行为
构造函数默认返回新创建的实例对象 (this), 即使没有显式写 return 语句.
javascript
function Person(name) {
this.name = name;
// 隐式返回 this
}
const person = new Person("Alice");
console.log(person); // Person { name: 'Alice' }
console.log(person instanceof Person); // true
显式返回的影响
构造函数的返回值行为遵循以下规则:
规则 1: 返回原始值 (Primitive) - 被忽略
javascript
function PersonWithNumber(name) {
this.name = name;
return 42; // 返回数字 (原始值)
}
function PersonWithString(name) {
this.name = name;
return "hello"; // 返回字符串 (原始值)
}
function PersonWithNull(name) {
this.name = name;
return null; // 返回 null (原始值)
}
// 所有原始值返回都会被忽略, 依然返回实例对象
const p1 = new PersonWithNumber("Alice");
const p2 = new PersonWithString("Bob");
const p3 = new PersonWithNull("David");
console.log(p1); // PersonWithNumber { name: 'Alice' }
console.log(p2); // PersonWithString { name: 'Bob' }
console.log(p3); // PersonWithNull { name: 'David' }
规则 2: 返回对象 (Object) - 覆盖默认返回值
javascript
function PersonWithArray(name) {
this.name = name;
return [1, 2, 3]; // 返回数组 (对象)
}
function PersonWithObject(name) {
this.name = name;
return { customName: "Custom", value: 100 }; // 返回对象
}
function PersonWithFunction(name) {
this.name = name;
return function () {
return "I am a function";
}; // 返回函数 (对象)
}
// 对象类型的返回值会覆盖默认的实例对象
const p1 = new PersonWithArray("Alice");
const p2 = new PersonWithObject("Bob");
const p3 = new PersonWithFunction("Charlie");
console.log(p1); // [1, 2, 3]
console.log(p1 instanceof PersonWithArray); // false (不再是实例)
console.log(p2); // { customName: 'Custom', value: 100 }
console.log(p2 instanceof PersonWithObject); // false
console.log(p3); // [Function (anonymous)]
console.log(p3()); // I am a function
console.log(p3 instanceof PersonWithFunction); // false
实际应用场景
场景 1: 单例模式
javascript
function Singleton(name) {
// 如果已经存在实例, 返回已有实例
if (Singleton.instance) {
return Singleton.instance;
}
this.name = name;
this.timestamp = Date.now();
// 保存实例引用
Singleton.instance = this;
}
const s1 = new Singleton("First");
const s2 = new Singleton("Second");
console.log(s1 === s2); // true (同一个实例)
console.log(s1.name); // First
console.log(s2.name); // First (不是 Second)
场景 2: 条件性返回不同对象
javascript
function UserFactory(type, name) {
this.name = name;
// 根据类型返回不同的对象结构
if (type === "admin") {
return {
name: name,
role: "admin",
permissions: ["read", "write", "delete"],
};
}
if (type === "guest") {
return {
name: name,
role: "guest",
permissions: ["read"],
};
}
// 默认返回普通用户实例
}
const admin = new UserFactory("admin", "Alice");
const guest = new UserFactory("guest", "Bob");
const user = new UserFactory("user", "Charlie");
console.log(admin); // { name: 'Alice', role: 'admin', permissions: [...] }
console.log(guest); // { name: 'Bob', role: 'guest', permissions: [...] }
console.log(user); // UserFactory { name: 'Charlie' }
实例属性与方法
实例独有属性
每次使用 new 调用构造函数时, 都会创建一个新的独立对象:
javascript
function Counter() {
this.count = 0;
this.increment = function () {
this.count++;
console.log(`Count: ${this.count}`);
};
}
const counter1 = new Counter();
const counter2 = new Counter();
counter1.increment(); // Count: 1
counter1.increment(); // Count: 2
counter2.increment(); // Count: 1
counter2.increment(); // Count: 2
console.log(counter1.count); // 2
console.log(counter2.count); // 2
// 两个实例互不影响
console.log(counter1 === counter2); // false
console.log(counter1.increment === counter2.increment); // false (每个实例都有自己的方法)
方法定义的性能问题
⚠️ 性能警告: 在构造函数中直接定义方法会导致每个实例都创建新的函数对象, 浪费内存.
javascript
function Person(name) {
this.name = name;
// ❌ 不推荐: 每次创建实例都会创建新的函数对象
this.greet = function () {
console.log(`Hello, I'm ${this.name}`);
};
}
const p1 = new Person("Alice");
const p2 = new Person("Bob");
// 两个实例的 greet 方法是不同的函数对象
console.log(p1.greet === p2.greet); // false
// 每个实例都占用独立的内存空间
console.log(p1.greet.toString() === p2.greet.toString()); // true (代码相同)
// 但它们是不同的对象, 占用不同的内存
✅ 解决方案: 使用原型
javascript
function Person(name) {
this.name = name;
}
// 将共享方法定义在原型上
Person.prototype.greet = function () {
console.log(`Hello, I'm ${this.name}`);
};
const p1 = new Person("Alice");
const p2 = new Person("Bob");
// 所有实例共享同一个方法
console.log(p1.greet === p2.greet); // true (节省内存)
p1.greet(); // Hello, I'm Alice
p2.greet(); // Hello, I'm Bob
new.target 元属性
new.target 是 ES2015 (ES6) 引入的元属性, 用于检测函数是否通过 new 运算符调用.
基本使用
javascript
function Person(name) {
// 检测是否使用 new 调用
if (!new.target) {
throw new Error("Person must be called with new");
}
this.name = name;
}
// ✅ 正确使用
const person1 = new Person("Alice");
console.log(person1); // Person { name: 'Alice' }
// ❌ 错误使用
try {
const person2 = Person("Bob"); // 抛出错误
} catch (e) {
console.error(e.message); // Person must be called with new
}
自动修正
javascript
function Person(name) {
// 如果忘记使用 new, 自动补救
if (!new.target) {
return new Person(name);
}
this.name = name;
}
const person1 = new Person("Alice"); // 使用 new
const person2 = Person("Bob"); // 忘记使用 new, 自动补救
console.log(person1); // Person { name: 'Alice' }
console.log(person2); // Person { name: 'Bob' }
console.log(person2 instanceof Person); // true
检测继承调用
new.target 在继承场景中指向实际被 new 调用的子类构造函数:
javascript
function Person(name) {
this.name = name;
console.log("new.target:", new.target.name);
}
function Employee(name, job) {
Person.call(this, name);
this.job = job;
}
const person = new Person("Alice");
// new.target: Person
const employee = new Employee("Bob", "Engineer");
// new.target: Employee
手动实现 new 运算符
通过手动实现 new 运算符的逻辑, 可以深入理解其工作原理.
完整实现
javascript
function myNew(constructor, ...args) {
// 参数验证: 确保 constructor 是函数
if (typeof constructor !== "function") {
throw new TypeError("First argument must be a constructor function");
}
// 步骤 1 & 2: 创建对象并设置原型
const obj = Object.create(constructor.prototype);
// 步骤 3 & 4: 绑定 this 并执行构造函数
const result = constructor.apply(obj, args);
// 步骤 5: 判断返回值
// 如果构造函数返回对象, 则返回该对象; 否则返回新创建的对象
const isObject = result !== null && typeof result === "object";
const isFunction = typeof result === "function";
return isObject || isFunction ? result : obj;
}
常见陷阱与注意事项
陷阱 1: 忘记使用 new
javascript
function Person(name) {
this.name = name;
}
// ❌ 错误: 忘记 new
const person = Person("Alice");
console.log(person); // undefined
console.log(window.name); // 'Alice' (污染全局对象)
解决方案:
- 使用
new.target检测 - 使用严格模式
- 使用 ES6 Class (强制使用 new)
陷阱 2: 构造函数返回对象覆盖实例
javascript
function Person(name) {
this.name = name;
return { custom: "object" }; // 覆盖默认返回值
}
const person = new Person("Alice");
console.log(person); // { custom: 'object' }
console.log(person.name); // undefined
console.log(person instanceof Person); // false
陷阱 3: 箭头函数不能作为构造函数
javascript
const Person = (name) => {
this.name = name;
};
try {
const person = new Person("Alice"); // TypeError: Person is not a constructor
} catch (e) {
console.error(e.message);
}
原因:
- 箭头函数没有自己的
this - 箭头函数没有
prototype属性 - 箭头函数没有
[[Construct]]内部方法
陷阱 4: 在构造函数中定义方法导致内存浪费
javascript
// ❌ 不推荐: 每个实例都创建新函数
function Person(name) {
this.name = name;
this.greet = function () {
console.log(`Hello, I'm ${this.name}`);
};
}
const p1 = new Person("Alice");
const p2 = new Person("Bob");
console.log(p1.greet === p2.greet); // false (浪费内存)
✅ 解决方案: 使用原型方法
javascript
function Person(name) {
this.name = name;
}
Person.prototype.greet = function () {
console.log(`Hello, I'm ${this.name}`);
};
const p1 = new Person("Alice");
const p2 = new Person("Bob");
console.log(p1.greet === p2.greet); // true (共享方法)