1. JavaScript中的面向对象基础
JavaScript作为一门多范式的编程语言,其面向对象编程(OOP)的实现方式与传统语言(如Java、C++)有着显著差异。在JS中,一切皆对象,但它的继承机制却是基于原型(prototype)而非类(class)的。这种独特的实现方式常常让初学者感到困惑。
1.1 原型链的本质
每个JavaScript对象都有一个内部属性[[Prototype]](可通过__proto__访问),它指向另一个对象或者null。当我们试图访问一个对象的属性时,如果该对象自身没有这个属性,JavaScript引擎就会沿着原型链向上查找,直到找到该属性或到达原型链末端(null)。
javascript复制let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // 设置rabbit的原型为animal
console.log(rabbit.eats); // true (从animal继承)
console.log(rabbit.jumps); // true (自身属性)
1.2 构造函数与new操作符
构造函数是创建对象的另一种方式,它本质上就是普通函数,但按照约定,构造函数名称以大写字母开头。使用new操作符调用函数时,会发生以下几步:
- 创建一个新的空对象
- 将这个新对象的[[Prototype]]指向构造函数的prototype属性
- 将this绑定到这个新对象
- 执行构造函数内部的代码
- 如果构造函数没有显式返回对象,则返回这个新对象
javascript复制function Animal(name) {
this.name = name;
this.eats = true;
}
let animal = new Animal("Rabbit");
console.log(animal.name); // "Rabbit"
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. JavaScript中的继承实现方式
2.1 原型链继承
这是最基本的继承方式,通过将子类的原型设置为父类的实例来实现继承。
javascript复制function Parent() {
this.parentProp = true;
}
Parent.prototype.getParentProp = function() {
return this.parentProp;
};
function Child() {
this.childProp = false;
}
// 关键继承步骤
Child.prototype = new Parent();
let child = new Child();
console.log(child.getParentProp()); // true
注意:原型链继承的主要问题是所有子类实例共享同一个父类实例的属性,这在引用类型属性时会导致问题。
2.2 构造函数继承
通过在子类构造函数中调用父类构造函数,可以解决原型链继承中引用类型共享的问题。
javascript复制function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
function Child(name, age) {
Parent.call(this, name); // 关键继承步骤
this.age = age;
}
let child1 = new Child("Tom", 10);
child1.colors.push("green");
let child2 = new Child("Jerry", 8);
console.log(child2.colors); // ["red", "blue"] - 不受child1影响
这种方式的缺点是父类原型上的方法无法被子类继承。
2.3 组合继承
组合继承结合了原型链继承和构造函数继承的优点,是最常用的继承模式。
javascript复制function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 第二次调用Parent
this.age = age;
}
Child.prototype = new Parent(); // 第一次调用Parent
Child.prototype.constructor = Child; // 修复constructor指向
let child = new Child("Tom", 10);
child.sayName(); // "Tom"
组合继承的缺点是会调用两次父类构造函数,导致子类原型上有多余的属性。
2.4 原型式继承
Object.create()方法提供了一种简单的继承方式,它创建一个新对象,使用现有对象作为新对象的原型。
javascript复制let parent = {
name: "Parent",
sayName: function() {
console.log(this.name);
}
};
let child = Object.create(parent);
child.name = "Child";
child.sayName(); // "Child"
这种方式适合不需要构造函数的简单场景。
2.5 寄生式继承
寄生式继承创建一个仅用于封装继承过程的函数,在函数内部增强对象。
javascript复制function createAnother(original) {
let clone = Object.create(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
2.6 寄生组合式继承
这是最理想的继承方式,解决了组合继承调用两次构造函数的问题。
javascript复制function inheritPrototype(child, parent) {
let prototype = Object.create(parent.prototype); // 创建对象
prototype.constructor = child; // 增强对象
child.prototype = prototype; // 指定对象
}
function Parent(name) {
this.name = name;
this.colors = ["red", "blue"];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
inheritPrototype(Child, Parent); // 关键继承步骤
let child = new Child("Tom", 10);
child.sayName(); // "Tom"
3. ES6中的class语法
ES6引入了class语法糖,使JavaScript的面向对象编程更加直观。
3.1 基本class语法
javascript复制class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a noise.`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // 调用父类构造函数
}
speak() {
console.log(`${this.name} barks.`);
}
}
let d = new Dog('Rex');
d.speak(); // Rex barks.
3.2 静态方法和属性
静态方法和属性属于类本身,而不是类的实例。
javascript复制class MyClass {
static staticMethod() {
console.log('This is a static method');
}
static staticProperty = 'someValue';
}
MyClass.staticMethod(); // 直接通过类调用
console.log(MyClass.staticProperty); // "someValue"
3.3 私有字段和方法
ES2022正式将私有字段和方法纳入标准。
javascript复制class Counter {
#count = 0; // 私有字段
#increment() { // 私有方法
this.#count++;
}
tick() {
this.#increment();
console.log(this.#count);
}
}
let c = new Counter();
c.tick(); // 1
// c.#count; // 报错:私有字段无法从类外部访问
4. JavaScript面向对象的高级主题
4.1 Mixin模式
JavaScript不支持多继承,但可以通过Mixin模式实现类似功能。
javascript复制let sayMixin = {
say(phrase) {
console.log(phrase);
}
};
let sayHiMixin = {
__proto__: sayMixin, // 或者使用Object.setPrototypeOf
sayHi() {
super.say(`Hello ${this.name}!`); // 调用父mixin的方法
},
sayBye() {
super.say(`Bye ${this.name}!`);
}
};
class User {
constructor(name) {
this.name = name;
}
}
// 将mixin的方法拷贝到User.prototype
Object.assign(User.prototype, sayHiMixin);
new User("Tom").sayHi(); // Hello Tom!
4.2 属性描述符与对象不可变性
JavaScript提供了对属性更精细的控制。
javascript复制let obj = {};
Object.defineProperty(obj, 'readOnlyProp', {
value: 42,
writable: false, // 不可写
enumerable: true, // 可枚举
configurable: false // 不可配置
});
obj.readOnlyProp = 100; // 在严格模式下会报错
console.log(obj.readOnlyProp); // 42
4.3 Proxy与元编程
Proxy可以拦截并自定义对象的基本操作。
javascript复制let target = {
message: "hello"
};
let handler = {
get(target, prop, receiver) {
if (prop === 'message') {
return target[prop] + " world!";
}
return Reflect.get(...arguments);
}
};
let proxy = new Proxy(target, handler);
console.log(proxy.message); // "hello world!"
4.4 Symbol与唯一属性
Symbol是一种新的原始数据类型,用于创建唯一的属性键。
javascript复制const id = Symbol('id');
let user = {
name: "Tom",
[id]: 123 // 而不是"id": 123
};
console.log(user[id]); // 123
console.log(user.id); // undefined
5. 实战中的常见问题与解决方案
5.1 方法丢失this的问题
当对象方法被传递为回调时,可能会丢失this。
javascript复制let user = {
name: "John",
sayHi() {
console.log(`Hello, ${this.name}!`);
}
};
setTimeout(user.sayHi, 1000); // Hello, undefined! - this丢失
// 解决方案1:包装函数
setTimeout(() => user.sayHi(), 1000);
// 解决方案2:bind
setTimeout(user.sayHi.bind(user), 1000);
// 解决方案3:类字段语法(ES2022)
class User {
name = "John";
sayHi = () => {
console.log(`Hello, ${this.name}!`);
};
}
5.2 原型污染问题
修改内置原型可能会引发难以追踪的问题。
javascript复制// 不推荐的做法
Array.prototype.myMethod = function() {
console.log('Custom method');
};
// 更安全的做法
function addArrayMethod() {
if (!Array.prototype.myMethod) {
Array.prototype.myMethod = function() {
console.log('Custom method');
};
}
}
5.3 深层次继承的性能问题
过深的原型链会影响属性查找性能。
javascript复制// 创建100层的原型链
let obj = {};
let current = obj;
for (let i = 0; i < 100; i++) {
current.__proto__ = {};
current = current.__proto__;
}
// 查找属性会遍历整个原型链
console.time('property access');
obj.someProperty;
console.timeEnd('property access'); // 耗时明显增加
5.4 ES6 class的局限性
class语法有一些需要注意的限制。
javascript复制class MyClass {
constructor() {
this.property = 'value';
}
// 类字段提案(ES2022)
anotherProperty = 'another value';
// 方法之间不能加逗号
method1() {}
// , // 语法错误
method2() {}
}
// 类声明不会被提升
new MyClass(); // 正常
// new NotHoisted(); // 报错
class NotHoisted {}
6. JavaScript面向对象的最佳实践
6.1 组合优于继承
优先使用对象组合而非类继承。
javascript复制// 不推荐的深层次继承
class Animal {}
class Mammal extends Animal {}
class Dog extends Mammal {}
// 推荐的组合方式
const canEat = {
eat() {
console.log('Eating');
}
};
const canWalk = {
walk() {
console.log('Walking');
}
};
const canBark = {
bark() {
console.log('Barking');
}
};
function createDog(name) {
return Object.assign(
{ name },
canEat,
canWalk,
canBark
);
}
const dog = createDog('Rex');
dog.bark(); // Barking
6.2 单一职责原则
每个类/对象应该只有一个职责。
javascript复制// 不推荐的做法
class User {
constructor(name) {
this.name = name;
}
saveToDatabase() {
// 保存逻辑
}
sendEmail() {
// 发送邮件逻辑
}
}
// 推荐的做法
class User {
constructor(name) {
this.name = name;
}
}
class UserRepository {
save(user) {
// 保存逻辑
}
}
class EmailService {
send(user, message) {
// 发送邮件逻辑
}
}
6.3 开闭原则
对扩展开放,对修改关闭。
javascript复制class Logger {
log(message) {
console.log(message);
}
}
// 不推荐:直接修改Logger类
// 推荐:通过扩展实现新功能
class TimestampLogger extends Logger {
log(message) {
super.log(`${new Date().toISOString()} - ${message}`);
}
}
const logger = new TimestampLogger();
logger.log('Hello'); // "2023-07-01T12:00:00.000Z - Hello"
6.4 依赖注入
通过依赖注入提高代码的可测试性和灵活性。
javascript复制// 不推荐:紧耦合
class UserService {
constructor() {
this.db = new Database();
}
}
// 推荐:依赖注入
class UserService {
constructor(database) {
this.db = database;
}
}
const db = new Database();
const userService = new UserService(db);
7. JavaScript面向对象的未来趋势
7.1 装饰器提案
装饰器提供了一种声明式的方式来修改类和类成员。
javascript复制// 目前是stage 3提案,需要使用Babel等转译器
@log
class MyClass {
@readonly
method() {}
}
function log(target) {
console.log(`Class ${target.name} is defined`);
}
function readonly(target, name, descriptor) {
descriptor.writable = false;
return descriptor;
}
7.2 私有方法和字段
私有特性正在逐步完善。
javascript复制class Counter {
#count = 0;
#increment() {
this.#count++;
}
tick() {
this.#increment();
}
get count() {
return this.#count;
}
}
7.3 静态类特性
静态字段和方法正在成为标准。
javascript复制class MyClass {
static staticProperty = 'value';
static staticMethod() {
return 'static method';
}
}
7.4 类自动访问私有字段
提案允许类自动访问其实例的私有字段。
javascript复制class MyClass {
#privateField = 42;
static getPrivateField(instance) {
return instance.#privateField; // 目前会报错,提案允许这样做
}
}
在实际项目中,理解JavaScript的原型继承机制至关重要,它能帮助开发者避免常见的陷阱,并编写出更高效、更可维护的代码。虽然ES6的class语法让JavaScript看起来更像传统的面向对象语言,但底层仍然是基于原型的继承模型。掌握这两种视角,才能在JavaScript面向对象编程中游刃有余。
