1. JavaScript 面向对象编程基础
在JavaScript的世界里,面向对象编程(OOP)是一种非常重要的编程范式。与传统的基于类的OOP语言(如Java、C++)不同,JavaScript采用了一种独特的基于原型的OOP实现方式。这种差异常常让初学者感到困惑,但一旦理解了其核心机制,你会发现它既灵活又强大。
1.1 构造函数:对象的工厂
构造函数是JavaScript中创建对象的特殊函数。任何普通函数都可以作为构造函数使用,只需要在调用时加上new关键字。这个简单的语法背后,JavaScript引擎为我们做了很多工作:
javascript复制function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function() {
console.log(`Hello, my name is ${this.name}`);
};
}
const john = new Person('John', 30);
当使用new调用函数时,JavaScript会:
- 创建一个新的空对象
- 将这个新对象的
[[Prototype]](即__proto__)链接到构造函数的prototype属性 - 将构造函数内部的
this绑定到这个新对象 - 执行构造函数内部的代码
- 如果构造函数没有显式返回对象,则返回这个新创建的对象
注意:构造函数通常以大写字母开头,这是一种约定,用于区分普通函数和构造函数。
1.2 为什么箭头函数不能作为构造函数
箭头函数是ES6引入的一种简洁的函数语法,但它有几个关键特性使其不适合作为构造函数:
javascript复制const ArrowFunc = () => {};
const instance = new ArrowFunc(); // TypeError: ArrowFunc is not a constructor
箭头函数不能作为构造函数的根本原因有两点:
- 没有自己的
this绑定:箭头函数的this值由定义时的词法作用域决定,而不是像普通函数那样在调用时确定。这使得它无法将this绑定到新创建的对象实例上。 - 没有
prototype属性:普通函数自动拥有prototype属性,而箭头函数没有。这意味着通过箭头函数创建的对象无法参与原型继承机制。
1.3 构造函数的局限性
虽然构造函数提供了一种创建对象的方式,但它存在一些明显的局限性:
javascript复制function Animal(name) {
this.name = name;
this.eat = function() {
console.log(`${this.name} is eating`);
};
}
const dog = new Animal('Dog');
const cat = new Animal('Cat');
console.log(dog.eat === cat.eat); // false - 每个实例都有自己的方法副本
这种实现方式会导致:
- 每个实例都会创建自己的方法副本,造成内存浪费
- 方法无法在实例间共享
- 修改一个实例的方法不会影响其他实例
这些问题正是原型和原型链要解决的。
2. 原型与原型链机制
2.1 原型对象(Prototype)
每个JavaScript函数(除了箭头函数)都有一个特殊的prototype属性,这个属性指向一个对象,我们称之为原型对象。原型对象的主要作用是包含可以由特定类型的所有实例共享的属性和方法。
javascript复制function Person() {}
console.log(Person.prototype); // 输出原型对象
原型对象默认包含一个constructor属性,指向构造函数本身:
javascript复制console.log(Person.prototype.constructor === Person); // true
2.2 实例与原型的关系
当我们通过构造函数创建实例时,实例内部会包含一个指向构造函数的原型对象的链接(__proto__):
javascript复制const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
这种关系可以用下图表示:
code复制实例(person)
↓ __proto__
Person.prototype
↓ __proto__
Object.prototype
↓ __proto__
null
2.3 原型链查找机制
当访问一个对象的属性时,JavaScript引擎会按照以下顺序查找:
- 首先在对象自身属性中查找
- 如果没有找到,则通过
__proto__在原型对象中查找 - 如果还没有找到,则继续在原型对象的原型中查找
- 直到找到属性或到达原型链末端(null)
javascript复制function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
const dog = new Animal('Dog');
dog.eat(); // 1. dog自身没有eat方法 → 2. 通过原型链找到Animal.prototype.eat
2.4 修改原型的影响
修改构造函数的原型对象会影响所有已经创建和将要创建的实例:
javascript复制function Car() {}
const car1 = new Car();
Car.prototype.drive = function() {
console.log('Driving...');
};
car1.drive(); // 可以调用,因为原型已被修改
const car2 = new Car();
car2.drive(); // 也可以调用
但是,完全替换原型对象会破坏已有实例的原型链:
javascript复制function Car() {}
const car1 = new Car();
Car.prototype = {
drive() {
console.log('New driving method');
}
};
const car2 = new Car();
car1.drive(); // TypeError: car1.drive is not a function
car2.drive(); // 正常工作
3. JavaScript继承实现方式
3.1 原型链继承
原型链继承是最简单的继承方式,通过让子类的原型对象等于父类的实例来实现继承:
javascript复制function Parent() {
this.parentProperty = true;
}
Parent.prototype.getParentValue = function() {
return this.parentProperty;
};
function Child() {
this.childProperty = false;
}
// 关键步骤:继承Parent
Child.prototype = new Parent();
const instance = new Child();
console.log(instance.getParentValue()); // true
优点:
- 简单易实现
- 父类原型方法可以被子类实例访问
缺点:
- 父类的引用类型属性会被所有子类实例共享
- 创建子类实例时无法向父类构造函数传参
- 无法实现多继承
3.2 构造函数继承
构造函数继承通过在子类构造函数中调用父类构造函数来实现:
javascript复制function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
function Child(name) {
Parent.call(this, name); // 关键步骤
}
const child1 = new Child('Child1');
child1.colors.push('green');
const child2 = new Child('Child2');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue'] - 不受影响
优点:
- 避免了引用类型属性共享问题
- 可以向父类构造函数传参
- 可以实现多继承(调用多个父类构造函数)
缺点:
- 无法继承父类原型上的方法
- 方法都在构造函数中定义,无法复用
3.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;
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child1 = new Child('Tom', 10);
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
child1.sayName(); // 'Tom'
child1.sayAge(); // 10
const child2 = new Child('Jerry', 8);
console.log(child2.colors); // ['red', 'blue']
child2.sayName(); // 'Jerry'
child2.sayAge(); // 8
优点:
- 实例属性独立,不共享
- 可以继承父类原型方法
- 可以向父类构造函数传参
缺点:
- 父类构造函数被调用了两次,导致效率问题
- 子类原型上会有多余的父类实例属性
3.4 寄生组合式继承
寄生组合式继承是目前最理想的继承方式,解决了组合继承的效率问题:
javascript复制function inheritPrototype(child, parent) {
const prototype = Object.create(parent.prototype); // 创建父类原型的副本
prototype.constructor = child; // 修复constructor
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);
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child = new Child('Tom', 10);
child.sayName(); // 'Tom'
child.sayAge(); // 10
优点:
- 只调用一次父类构造函数
- 原型链保持不变
- 能够正常使用instanceof和isPrototypeOf
实际上,ES6的class语法中的extends关键字就是基于这种继承方式实现的。
4. 现代JavaScript中的类继承
ES6引入了class语法,使得JavaScript的面向对象编程更加直观:
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.`);
}
}
const d = new Dog('Rex');
d.speak(); // Rex barks.
class关键字的本质:
- class本质上是构造函数的语法糖
- extends实现了原型继承
- 方法都定义在原型上
- 静态方法定义在构造函数上
- 继承关系通过Object.setPrototypeOf建立
5. 实际应用中的注意事项
5.1 原型污染问题
修改内置对象的原型可能会导致意想不到的问题:
javascript复制// 不推荐的做法
Array.prototype.sum = function() {
return this.reduce((a, b) => a + b, 0);
};
// 更好的做法
class MyArray extends Array {
sum() {
return this.reduce((a, b) => a + b, 0);
}
}
5.2 性能考虑
原型链过长会影响属性查找性能。在性能敏感的代码中,可以考虑使用组合而非继承:
javascript复制// 使用组合而非继承
const canEat = {
eat() {
console.log('Eating');
}
};
const canWalk = {
walk() {
console.log('Walking');
}
};
function Person() {
return Object.assign({}, canEat, canWalk);
}
5.3 多继承的实现
JavaScript本身不支持多继承,但可以通过混入(Mixin)模式模拟:
javascript复制class A {
methodA() {}
}
class B {
methodB() {}
}
class C {
constructor() {
Object.assign(this, Object.create(A.prototype));
Object.assign(this, Object.create(B.prototype));
}
}
6. 常见面试问题解析
6.1 new操作符做了什么?
当使用new调用函数时,JavaScript引擎会:
- 创建一个新对象
- 将这个新对象的原型指向构造函数的prototype
- 将this绑定到这个新对象并执行构造函数
- 如果构造函数没有返回对象,则返回这个新对象
6.2 instanceof的原理是什么?
instanceof运算符检查构造函数的prototype属性是否出现在对象的原型链上:
javascript复制function myInstanceof(obj, constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
6.3 Object.create和new的区别?
Object.create(proto)创建一个新对象,并将其原型设置为proto。而new Constructor()会:
- 创建一个新对象
- 设置原型为Constructor.prototype
- 执行Constructor函数
6.4 ES6 class和ES5构造函数有什么区别?
class本质上是构造函数的语法糖,但有一些区别:
- class声明不会提升
- class内部方法不可枚举
- class必须用new调用
- class内部默认使用严格模式
- class的静态方法直接添加到构造函数上
7. 最佳实践与性能优化
7.1 原型方法 vs 实例方法
- 原型方法:适合共享的、不依赖实例特定状态的方法
- 实例方法:适合需要访问实例私有状态的方法
javascript复制// 原型方法 - 推荐
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
};
// 实例方法 - 不推荐(每个实例都会创建方法副本)
function Person(name) {
this.name = name;
this.sayName = function() {
console.log(this.name);
};
}
7.2 内存优化
避免在原型上存储大量数据,因为所有实例都会共享这些数据:
javascript复制// 不推荐
function Tree() {}
Tree.prototype.leaves = new Array(1000).fill('leaf');
// 推荐
function Tree() {
this.leaves = new Array(1000).fill('leaf');
}
7.3 继承层次控制
保持继承层次尽可能浅,深层次的继承链会影响性能和可维护性:
javascript复制// 不推荐 - 继承层次太深
class A {}
class B extends A {}
class C extends B {}
class D extends C {}
// 推荐 - 使用组合
class A {}
class B {
constructor() {
this.a = new A();
}
}
8. 实际案例分析
8.1 UI组件继承
javascript复制class Component {
constructor(element) {
this.element = element;
}
show() {
this.element.style.display = 'block';
}
hide() {
this.element.style.display = 'none';
}
}
class Button extends Component {
constructor(element) {
super(element);
element.addEventListener('click', () => this.onClick());
}
onClick() {
console.log('Button clicked!');
}
}
const btn = new Button(document.querySelector('button'));
8.2 游戏实体继承
javascript复制class Entity {
constructor(x, y) {
this.x = x;
this.y = y;
}
update() {
console.log('Updating entity position');
}
}
class Player extends Entity {
constructor(x, y) {
super(x, y);
this.health = 100;
}
takeDamage(amount) {
this.health -= amount;
}
}
class Enemy extends Entity {
constructor(x, y, type) {
super(x, y);
this.type = type;
}
attack() {
console.log(`${this.type} attacking!`);
}
}
8.3 表单验证继承
javascript复制class Validator {
constructor(rules) {
this.rules = rules;
}
validate(value) {
for (const rule of this.rules) {
if (!rule.test(value)) {
return false;
}
}
return true;
}
}
class EmailValidator extends Validator {
constructor() {
super([
{ test: v => v.includes('@'), message: 'Must contain @' },
{ test: v => v.includes('.'), message: 'Must contain .' }
]);
}
}
const emailValidator = new EmailValidator();
console.log(emailValidator.validate('test@example.com')); // true
9. 常见问题与解决方案
9.1 方法覆盖问题
当子类方法覆盖父类方法时,如何调用父类方法:
javascript复制class Parent {
greet() {
console.log('Hello from Parent');
}
}
class Child extends Parent {
greet() {
super.greet(); // 调用父类方法
console.log('Hello from Child');
}
}
9.2 多态实现
JavaScript通过原型链自然支持多态:
javascript复制class Animal {
makeSound() {
console.log('Some generic sound');
}
}
class Dog extends Animal {
makeSound() {
console.log('Bark');
}
}
class Cat extends Animal {
makeSound() {
console.log('Meow');
}
}
function animalSound(animal) {
animal.makeSound(); // 多态调用
}
animalSound(new Dog()); // Bark
animalSound(new Cat()); // Meow
9.3 静态方法继承
静态方法也会被继承:
javascript复制class Parent {
static staticMethod() {
console.log('Parent static method');
}
}
class Child extends Parent {}
Child.staticMethod(); // 'Parent static method'
10. 高级主题与未来趋势
10.1 私有类字段
ES2022引入了真正的私有字段:
javascript复制class Person {
#age = 0; // 私有字段
constructor(name) {
this.name = name; // 公共字段
}
getAge() {
return this.#age;
}
setAge(age) {
this.#age = age;
}
}
const p = new Person('John');
console.log(p.name); // 'John'
console.log(p.#age); // SyntaxError
console.log(p.getAge()); // 0
10.2 装饰器提案
装饰器提供了一种声明式的方式来修改类及其成员:
javascript复制@serializable
class Person {
@observable name = 'John';
@deprecate
oldMethod() {}
}
10.3 类静态初始化块
ES2022引入了静态初始化块:
javascript复制class Translator {
static translations = {
yes: 'ja',
no: 'nein'
};
static englishWords = [];
static germanWords = [];
static { // 静态初始化块
for (const [english, german] of Object.entries(this.translations)) {
this.englishWords.push(english);
this.germanWords.push(german);
}
}
}
11. 性能对比与基准测试
11.1 创建对象性能
不同创建对象方式的性能比较:
- 对象字面量:最快
- 构造函数:中等
- class:与构造函数相当
- Object.create:较慢
11.2 方法调用性能
方法调用性能比较:
- 原型方法:最快
- 实例方法:较慢(每个实例都有副本)
- 动态添加方法:最慢
11.3 继承方式性能
继承方式性能比较:
- 寄生组合式继承:最快
- 组合继承:中等
- 原型链继承:较慢
- 构造函数继承:最慢(无法共享方法)
12. 工具与调试技巧
12.1 检查原型链
javascript复制function getPrototypeChain(obj) {
const chain = [];
let current = obj;
while (current) {
chain.push(current.constructor.name);
current = Object.getPrototypeOf(current);
}
return chain;
}
12.2 性能分析工具
- Chrome DevTools的Performance面板
- console.time和console.timeEnd
- Node.js的process.hrtime()
12.3 内存泄漏检测
- Chrome DevTools的Memory面板
- Node.js的--inspect标志和Chrome DevTools
- WeakMap和WeakSet可以帮助避免内存泄漏
13. 跨浏览器兼容性考虑
13.1 ES5兼容性处理
使用Babel等工具将ES6+代码转译为ES5:
javascript复制// ES6
class Person {
constructor(name) {
this.name = name;
}
}
// 转译为ES5
function Person(name) {
this.name = name;
}
13.2 原型方法的安全扩展
安全地扩展内置对象原型:
javascript复制if (!Array.prototype.find) {
Array.prototype.find = function(predicate) {
// 实现
};
}
13.3 Polyfill实现
实现Object.create的polyfill:
javascript复制if (!Object.create) {
Object.create = function(proto) {
function F() {}
F.prototype = proto;
return new F();
};
}
14. 安全注意事项
14.1 原型污染攻击
恶意修改原型可能导致安全问题:
javascript复制// 恶意代码
Object.prototype.toString = function() {
// 窃取数据
};
// 防御措施
Object.freeze(Object.prototype);
14.2 安全地使用继承
避免继承不可信的对象:
javascript复制function createSafeObject() {
const obj = Object.create(null); // 无原型
// 添加可信属性
return obj;
}
14.3 防止属性覆盖
使用Object.defineProperty定义不可写属性:
javascript复制function SafeConstructor() {
Object.defineProperty(this, 'criticalMethod', {
value: function() {
// 安全实现
},
writable: false
});
}
15. 测试策略
15.1 单元测试继承关系
javascript复制class Animal {}
class Dog extends Animal {}
test('Dog extends Animal', () => {
const dog = new Dog();
expect(dog instanceof Animal).toBe(true);
expect(Dog.prototype instanceof Animal).toBe(true);
});
15.2 测试方法覆盖
javascript复制class Parent {
method() { return 'parent'; }
}
class Child extends Parent {
method() { return 'child'; }
}
test('Child overrides Parent method', () => {
const child = new Child();
expect(child.method()).toBe('child');
});
15.3 性能测试
javascript复制function testCreationPerformance() {
console.time('Class creation');
for (let i = 0; i < 100000; i++) {
new MyClass();
}
console.timeEnd('Class creation');
}
16. 设计模式中的应用
16.1 工厂模式
javascript复制class Car {
constructor(options) {
// ...
}
}
class CarFactory {
create(type) {
switch (type) {
case 'sedan': return new Car({ doors: 4 });
case 'coupe': return new Car({ doors: 2 });
}
}
}
16.2 策略模式
javascript复制class PaymentStrategy {
pay(amount) {}
}
class CreditCardStrategy extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} with credit card`);
}
}
class PayPalStrategy extends PaymentStrategy {
pay(amount) {
console.log(`Paid ${amount} with PayPal`);
}
}
16.3 观察者模式
javascript复制class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log('Received data:', data);
}
}
17. 与函数式编程的结合
17.1 不可变数据
javascript复制class ImmutablePoint {
constructor(x, y) {
this.x = x;
this.y = y;
Object.freeze(this);
}
withX(x) {
return new ImmutablePoint(x, this.y);
}
}
17.2 高阶函数
javascript复制class Calculator {
constructor() {
this.operations = {
add: (a, b) => a + b,
subtract: (a, b) => a - b
};
}
compute(operation, a, b) {
return this.operations[operation](a, b);
}
}
17.3 组合函数
javascript复制class Pipeline {
constructor() {
this.steps = [];
}
addStep(step) {
this.steps.push(step);
}
execute(input) {
return this.steps.reduce((result, step) => step(result), input);
}
}
18. 模块化与代码组织
18.1 ES模块中的类
javascript复制// animal.js
export class Animal {
// ...
}
// dog.js
import { Animal } from './animal.js';
export class Dog extends Animal {
// ...
}
18.2 命名空间模式
javascript复制const MyApp = {
Models: {},
Views: {},
Controllers: {}
};
MyApp.Models.Person = class {
// ...
};
18.3 单例模式
javascript复制class Logger {
constructor() {
if (!Logger.instance) {
Logger.instance = this;
}
return Logger.instance;
}
log(message) {
console.log(message);
}
}
const logger1 = new Logger();
const logger2 = new Logger();
console.log(logger1 === logger2); // true
19. 调试技巧与常见错误
19.1 忘记使用new
javascript复制function Person(name) {
this.name = name;
}
const p = Person('John'); // 忘记new,this指向全局对象
console.log(name); // 'John' - 污染全局命名空间
// 解决方案1:使用class语法,强制使用new
class Person {
constructor(name) {
this.name = name;
}
}
// 解决方案2:安全构造函数
function Person(name) {
if (!(this instanceof Person)) {
return new Person(name);
}
this.name = name;
}
19.2 原型链断裂
javascript复制function Parent() {}
function Child() {}
Child.prototype = Parent.prototype; // 错误!应该使用Object.create
// 正确做法
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
19.3 循环引用
javascript复制function A() {}
function B() {}
A.prototype = new B();
B.prototype = new A(); // 循环引用,导致无限递归
// 解决方案:避免循环原型链
20. 总结与最佳实践
JavaScript的面向对象编程基于原型机制,理解原型和原型链是掌握JavaScript OOP的关键。以下是核心要点和最佳实践:
- 优先使用class语法:class语法更清晰,且强制使用new调用
- 理解原型链:知道属性查找的机制和顺序
- 合理使用继承:考虑组合优于继承的原则
- 避免修改内置原型:除非实现polyfill
- 注意内存使用:原型上的引用类型属性会被所有实例共享
- 使用现代继承方式:优先使用寄生组合式继承或class extends
- 考虑性能影响:原型链过长会影响查找性能
- 保持代码可维护性:避免过深的继承层次
- 测试继承关系:确保instanceof和原型链关系正确
- 关注安全:防止原型污染攻击
JavaScript的面向对象特性虽然与传统的基于类的语言不同,但一旦掌握了其原型机制,你会发现它提供了极大的灵活性和表现力。随着ES6+新特性的引入,JavaScript的OOP能力变得更加强大和易用。