【JavaScript】构造函数与 new 运算符
什么是构造函数
构造函数 (Constructor Function) 是用于创建和初始化对象的特殊函数.
通过 new 关键字调用构造函数, 可以创建具有相同属性和方法的多个对象实例.
构造函数的类型
JavaScript 中的构造函数分为两类:
1. 内置构造函数
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. 自定义构造函数
开发者定义的构造函数, 用于创建自定义类型的对象:
// 构造函数使用大驼峰命名 (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 引擎会执行以下步骤:
执行步骤
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 关键字, 构造函数会作为普通函数执行:
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' }
严格模式下的保护:
"use strict";function Person(name) {// 严格模式下, this 为 undefinedthis.name = name; // TypeError: Cannot set property 'name' of undefined
}const person = Person("Eve"); // 抛出错误
构造函数的基本使用
定义与调用
// 定义构造函数
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 参数默认值:
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 }
对象参数解构:
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 的指向
function Person(name) {this.name = name;// this 指向新创建的实例对象console.log("Inside constructor:", this);this.regularMethod = function () {// 普通函数中的 this 指向调用者console.log("Regular method:", this);};this.arrowMethod = () => {// 箭头函数中的 this 继承自构造函数的 thisconsole.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 语句.
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) - 被忽略
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) - 覆盖默认返回值
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); // falseconsole.log(p3); // [Function (anonymous)]
console.log(p3()); // I am a function
console.log(p3 instanceof PersonWithFunction); // false
实际应用场景
场景 1: 单例模式
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: 条件性返回不同对象
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 调用构造函数时, 都会创建一个新的独立对象:
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: 2counter2.increment(); // Count: 1
counter2.increment(); // Count: 2console.log(counter1.count); // 2
console.log(counter2.count); // 2// 两个实例互不影响
console.log(counter1 === counter2); // false
console.log(counter1.increment === counter2.increment); // false (每个实例都有自己的方法)
方法定义的性能问题
⚠️ 性能警告: 在构造函数中直接定义方法会导致每个实例都创建新的函数对象, 浪费内存.
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 (代码相同)
// 但它们是不同的对象, 占用不同的内存
✅ 解决方案: 使用原型
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 运算符调用.
基本使用
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
}
自动修正
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 调用的子类构造函数:
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: Personconst employee = new Employee("Bob", "Engineer");
// new.target: Employee
手动实现 new 运算符
通过手动实现 new 运算符的逻辑, 可以深入理解其工作原理.
完整实现
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
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: 构造函数返回对象覆盖实例
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: 箭头函数不能作为构造函数
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: 在构造函数中定义方法导致内存浪费
// ❌ 不推荐: 每个实例都创建新函数
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 (浪费内存)
✅ 解决方案: 使用原型方法
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 (共享方法)
