解密 JavaScript 面向对象:构造函数、原型与实例的三角关系

50 阅读3分钟

引言

在JavaScript中,对象是代码组织的核心单元。不同于传统面向对象语言(如Java),JavaScript通过构造函数原型的独特机制实现对象的创建与继承。本文将通过理论解析与代码示例,带你全面理解这三者的关系与运作原理。


一、对象的创建方式

1.1 对象字面量

对象字面量是最简单的对象创建方式,适合快速定义单个对象:

const person = {
    name: "张三",
    sayHello: function() {
        console.log(`你好,我是${this.name}`);
    }
};

缺点:缺乏灵活性,无法批量创建结构相似的对象。

1.2 ES6的class关键字

ES6引入了class语法糖,形式上更接近传统面向对象语言:

class Person {
    constructor(name) {
        this.name = name;
    }
    sayHello() {
        console.log(`你好,我是${this.name}`);
    }
}

const person1 = new Person("李四");
person1.sayHello(); // 输出:你好,我是李四

本质class底层仍基于构造函数和原型实现,方法定义在原型上,所有实例共享。


二、构造函数:对象的初始化逻辑

2.1 构造函数的定义与调用

构造函数通过new操作符调用,用于初始化对象属性:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

const person = new Person("王五", 25);
console.log(person); // {name: "王五", age: 25}

关键点

  • 构造函数首字母大写(约定,非强制)。
  • 若忘记使用newthis会指向全局对象(如window),导致属性泄漏。

2.2 构造函数与普通函数的区别

函数是否为构造函数取决于调用方式:

// 普通调用(错误用法)
Person("赵六", 30); 
console.log(window.name); // "赵六"(浏览器环境)

// 正确调用
const person = new Person("赵六", 30);

三、原型(Prototype):共享方法与属性

3.1 为什么需要原型?

若在构造函数内定义方法,每个实例会创建独立的方法副本,浪费内存:

function Person(name) {
    this.name = name;
    this.sayHello = function() { 
        console.log(`你好,我是${this.name}`);
    };
}

const p1 = new Person("小明");
const p2 = new Person("小红");
console.log(p1.sayHello === p2.sayHello); // false

解决方案:将方法定义在原型上,所有实例共享同一方法。

3.2 原型的使用与动态性

每个构造函数都有一个prototype属性,指向原型对象:

function Person(name) {
    this.name = name;
}

// 添加方法到原型
Person.prototype.sayHello = function() {
    console.log(`你好,我是${this.name}`);
};

const person = new Person("小李");
person.sayHello(); // 输出:你好,我是小李

动态性:修改原型后,所有实例(包括已存在的)都能访问新增方法:

Person.prototype.sayAge = function() {
    console.log(`我今年${this.age}岁`);
};

person.age = 20;
person.sayAge(); // 输出:我今年20岁

四、原型链与继承机制

4.1 原型链的基本概念

实例通过__proto__访问原型,形成链式结构:

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

4.2 原型切换的实用技巧

原型可以动态替换,但需谨慎操作:

const basketballPlayer = {
    play: function() {
        console.log(`${this.name}正在打篮球`);
    }
};

function Student(name) {
    this.name = name;
}

// 保存旧原型
const oldPrototype = Student.prototype;

// 切换原型
Student.prototype = basketballPlayer;
const student = new Student("小王");
student.play(); // 输出:小王正在打篮球

// 恢复旧原型
Student.prototype = oldPrototype;

注意事项

  • 切换原型后,新实例使用新原型,旧实例不受影响。
  • 若需恢复旧原型,必须提前保存。

五、常见错误与原型优势

5.1 错误示例:原型循环引用

将构造函数原型指向自身会导致逻辑混乱:

function Person(name) {
    this.name = name;
}

Person.prototype = Person; // ❌ 错误操作
const p = new Person();
console.log(p.name); // undefined(未传递参数)
console.log(p.__proto__ === Person); // true

解决方法:原型应指向一个普通对象。

5.2 原型优势

  1. 方法定义在原型:减少内存占用。
  2. 属性定义在构造函数:每个实例独立。
  3. 避免动态修改内置对象原型(如Array.prototype),防止命名冲突。

六、总结

  • 构造函数:通过new初始化对象属性。
  • 原型:存储共享方法,支持动态扩展。
  • 实例:继承原型方法,通过__proto__访问原型链。

理解这三者的关系是掌握JavaScript面向对象编程的核心。无论是ES5的构造函数还是ES6的class,本质都基于原型机制。通过灵活运用原型链,可以构建高效、可复用的代码结构。