【JavaScript】构造函数与 new 运算符

什么是构造函数

构造函数 (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 (共享方法)
相关推荐
lqj_本人6 小时前
【Rust编程:从小白入坑】Rust所有权系统
开发语言·jvm·rust
疏狂难除6 小时前
【Tauri2】050——加载html和rust爬虫
开发语言·爬虫·rust·spiderdemo
艾小码6 小时前
2025年组件化开发这样做,效率提升300%
前端·javascript
Zhangzy@7 小时前
仓颉的空安全基石:Option类型的设计与实践
java·开发语言·安全
oioihoii7 小时前
Rust中WebSocket支持的实现
开发语言·websocket·rust
明道源码9 小时前
Kotlin Multiplatform 跨平台方案解析以及热门框架对比
开发语言·kotlin·cocoa
fie88899 小时前
C#实现连续语音转文字
开发语言·c#
一念&11 小时前
每日一个C语言知识:C 头文件
c语言·开发语言·算法
DARLING Zero two♡12 小时前
仓颉GC调优参数:垃圾回收的精密控制艺术
开发语言·仓颉