引言
在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}
关键点:
- 构造函数首字母大写(约定,非强制)。
- 若忘记使用
new
,this
会指向全局对象(如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 原型优势
- 方法定义在原型:减少内存占用。
- 属性定义在构造函数:每个实例独立。
- 避免动态修改内置对象原型(如
Array.prototype
),防止命名冲突。
六、总结
- 构造函数:通过
new
初始化对象属性。 - 原型:存储共享方法,支持动态扩展。
- 实例:继承原型方法,通过
__proto__
访问原型链。
理解这三者的关系是掌握JavaScript面向对象编程的核心。无论是ES5的构造函数还是ES6的class
,本质都基于原型机制。通过灵活运用原型链,可以构建高效、可复用的代码结构。