JavaScript 面向对象编程(OOP)

简介

JavaScript 的面向对象编程(OOP)基于原型(Prototype),而不是传统的类继承。ES6 引入了 class 语法,但它是原型继承的语法糖

参考文档

  • js-data-types.md — 对象基础(创建、属性访问等)
  • js-function.md — 函数、构造函数
  • js-closure.md — 闭包(用于封装)

对象创建方式回顾

对象字面量

// 最简单的方式
let person = {
  name: 'Alice',
  age: 25,
  greet() {
    console.log('Hello, ' + this.name);
  }
};

person.greet();  // "Hello, Alice"

构造函数

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.greet = function() {
    console.log('Hello, ' + this.name);
  };
}

let alice = new Person('Alice', 25);
alice.greet();  // "Hello, Alice"

Object.create()

let personProto = {
  greet() {
    console.log('Hello, ' + this.name);
  }
};

let bob = Object.create(personProto);
bob.name = 'Bob';
bob.greet();  // "Hello, Bob"

工厂函数

function createPerson(name, age) {
  return {
    name,
    age,
    greet() {
      console.log('Hello, ' + this.name);
    }
  };
}

let charlie = createPerson('Charlie', 30);
charlie.greet();  // "Hello, Charlie"

原型(Prototype)

prototype 属性

每个函数都有一个 prototype 属性,它指向一个对象。当使用 new 调用函数时,新对象的原型就是这个 prototype 对象。

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

// 在原型上添加方法(推荐,所有实例共享)
Person.prototype.greet = function() {
  console.log('Hello, ' + this.name);
};

Person.prototype.age = 25;  // 所有实例共享的属性

let p1 = new Person('Alice');
let p2 = new Person('Bob');

p1.greet();  // "Hello, Alice"
p2.greet();  // "Hello, Bob"

// 共享原型上的方法
console.log(p1.greet === p2.greet);  // true

proto 和 Object.getPrototypeOf()

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

let p = new Person('Alice');

// __proto__:访问对象的原型(不推荐直接使用)
console.log(p.__proto__ === Person.prototype);  // true

// 推荐:使用 Object.getPrototypeOf()
console.log(Object.getPrototypeOf(p) === Person.prototype);  // true

// 原型链顶端
console.log(Object.getPrototypeOf(Person.prototype) === Object.prototype);  // true
console.log(Object.getPrototypeOf(Object.prototype));  // null

原型链查找规则

当访问对象属性时,查找顺序是:

  1. 先在当前对象自身查找
  2. 如果没找到,到其原型(proto)中查找
  3. 继续向上,直到 null(原型链顶端)
function Person(name) {
  this.name = name;  // 自身属性
}

Person.prototype.greet = function() {  // 原型上的方法
  console.log('Hello, ' + this.name);
};

let p = new Person('Alice');

// 查找 name:在 p 自身找到
console.log(p.name);  // "Alice"

// 查找 greet:p 自身没有,到原型上找
p.greet();  // "Hello, Alice"

// 查找 toString:原型上没有,继续向上到 Object.prototype
console.log(p.toString());  // "[object Object]"

hasOwnProperty() 和 in 操作符

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

Person.prototype.greet = function() {
  console.log('Hello');
};

let p = new Person('Alice');

console.log(p.hasOwnProperty('name'));   // true(自身属性)
console.log(p.hasOwnProperty('greet'));   // false(原型上的)
console.log(p.hasOwnProperty('toString')); // false(Object.prototype 上的)

console.log('name' in p);   // true(自身或原型链上)
console.log('greet' in p);   // true(原型上)
console.log('age' in p);     // false(不存在)

原型图(文字描述)

实例对象 p
  ├── 自身属性:name = 'Alice'
  └── __proto__ → Person.prototype
                    ├── greet: function()
                    └── __proto__ → Object.prototype
                                  ├── toString: function()
                                  ├── hasOwnProperty: function()
                                  └── __proto__ → null(顶端)

原型链和继承

原型链是什么

原型链是 JavaScript 实现继承的机制。每个对象都有一个指向其原型的内部链接,形成一条链。

// 原型链顶端
console.log(Object.prototype.__proto__);  // null

// 数组的原型链
let arr = [];
console.log(arr.__proto__ === Array.prototype);           // true
console.log(Array.prototype.__proto__ === Object.prototype);  // true
console.log(Object.prototype.__proto__);                // null

// 字符串的原型链
let str = 'hello';
console.log(str.__proto__ === String.prototype);          // true
console.log(String.prototype.__proto__ === Object.prototype);  // true

Object.prototype 和 null

// Object.prototype 是所有对象的根原型
console.log(Object.prototype.__proto__);  // null

// 函数也是对象,它的原型链
function Foo() {}
console.log(Foo.__proto__ === Function.prototype);       // true
console.log(Function.prototype.__proto__ === Object.prototype);  // true

继承方式

1. 原型链继承(早期方式)

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

Parent.prototype.sayName = function() {
  console.log(this.name);
};

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

// 继承:Child 的原型指向 Parent 的实例
Child.prototype = new Parent('parent');

// 修复 constructor 指向
Child.prototype.constructor = Child;

// 使用
let c1 = new Child('Alice', 25);
c1.colors.push('green');
console.log(c1.name);      // "parent"(来自原型)
console.log(c1.age);       // 25
c1.sayName();              // "parent"

// 问题1:所有实例共享引用类型的属性
let c2 = new Child('Bob', 30);
console.log(c2.colors);  // ["red", "blue", "green"](被 c1 影响了!)

// 问题2:无法向 Parent 构造函数传参

2. 构造函数继承(经典继承)

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

Parent.prototype.sayName = function() {
  console.log(this.name);
};

function Child(name, age) {
  // 调用 Parent,并修改 this 指向
  Parent.call(this, name);  // 或者 Parent.apply(this, arguments)
  this.age = age;
}

// 使用
let c1 = new Child('Alice', 25);
c1.colors.push('green');
console.log(c1.name);  // "Alice"(自身属性,不共享)
console.log(c1.colors);  // ["red", "blue", "green"]

let c2 = new Child('Bob', 30);
console.log(c2.colors);  // ["red", "blue"](不受影响)

// 问题:无法继承 Parent 原型上的方法
// c1.sayName();  // TypeError: c1.sayName is not a function

3. 组合继承(最常用,经典方式)

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;
}

// 继承方法(原型链继承)
Child.prototype = new Parent();
Child.prototype.constructor = Child;  // 修复 constructor

// 添加自己的方法
Child.prototype.sayAge = function() {
  console.log(this.age);
};

// 使用
let c1 = new Child('Alice', 25);
c1.colors.push('green');
console.log(c1.colors);  // ["red", "blue", "green"]
c1.sayName();           // "Alice"
c1.sayAge();            // 25

let c2 = new Child('Bob', 30);
console.log(c2.colors);  // ["red", "blue"](不共享引用属性)

4. 原型式继承(Object.create())

let parent = {
  name: 'parent',
  colors: ['red', 'blue'],
  sayName() {
    console.log(this.name);
  }
};

// 创建以 parent 为原型的对象
let child1 = Object.create(parent);
child1.name = 'Alice';  // 覆盖 name
child1.colors.push('green');

let child2 = Object.create(parent);
console.log(child2.name);    // "parent"(原型上的)
console.log(child2.colors);  // ["red", "blue", "green"](被 child1 影响了!)

// 问题:和原型链继承一样,引用类型会共享

5. 寄生式继承

function createAnother(original) {
  let clone = Object.create(original);  // 创建对象
  clone.sayHi = function() {             // 增强对象
    console.log('Hi');
  };
  return clone;
}

let person = { name: 'Alice' };
let another = createAnother(person);
another.sayHi();  // "Hi"

6. 寄生组合式继承(最优方案,ES5 及之前)

function inheritPrototype(child, parent) {
  let 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;
}

// 继承方法(只调用一次 Parent 构造函数)
inheritPrototype(Child, Parent);

// 添加自己的方法
Child.prototype.sayAge = function() {
  console.log(this.age);
};

// 使用
let c1 = new Child('Alice', 25);
c1.sayName();  // "Alice"
c1.sayAge();   // 25

7. ES6 class 继承(现代方式,推荐)

class Parent {
  constructor(name) {
    this.name = name;
    this.colors = ['red', 'blue'];
  }

  sayName() {
    console.log(this.name);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name);  // 相当于 Parent.call(this, name)
    this.age = age;
  }

  sayAge() {
    console.log(this.age);
  }
}

// 使用
let c1 = new Child('Alice', 25);
c1.colors.push('green');
console.log(c1.colors);  // ["red", "blue", "green"]
c1.sayName();  // "Alice"
c1.sayAge();   // 25

let c2 = new Child('Bob', 30);
console.log(c2.colors);  // ["red", "blue"](不共享)

ES6 Class(现代方式)

class 基本语法

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

  // 方法定义在原型上
  greet() {
    console.log(`Hello, I am ${this.name}`);
  }

  // getter
  get info() {
    return `${this.name}, ${this.age} years old`;
  }

  // setter
  set nickname(nick) {
    this._nickname = nick;
  }

  get nickname() {
    return this._nickname;
  }
}

let p = new Person('Alice', 25);
p.greet();                 // "Hello, I am Alice"
console.log(p.info);       // "Alice, 25 years old"(getter 像属性一样访问)
p.nickname = 'Ally';   // 调用 setter
console.log(p.nickname);    // "Ally"(调用 getter)

extends 继承

class Animal {
  constructor(name) {
    this.name = name;
  }

  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);           // 调用父类构造函数(必须先调用)
    this.breed = breed;
  }

  speak() {
    super.speak();        // 调用父类方法
    console.log(`${this.name} barks.`);
  }
}

let dog = new Dog('Rex', 'Labrador');
dog.speak();
// 输出:
// Rex makes a noise.
// Rex barks.

super 关键字

class Parent {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    console.log('Hello from Parent');
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name);  // 必须在 this 之前调用!
    this.age = age;
  }

  sayHello() {
    super.sayHello();  // 调用父类同名方法
    console.log('Hello from Child');
  }
}

let c = new Child('Alice', 25);
c.sayHello();
// 输出:
// Hello from Parent
// Hello from Child

static 静态方法

class Person {
  constructor(name) {
    this.name = name;
  }

  // 实例方法(在原型上)
  greet() {
    console.log(`Hello, I am ${this.name}`);
  }

  // 静态方法(在类本身上,不在实例上)
  static create(name) {
    return new Person(name);
  }

  static description = 'A human being';  // 静态属性(ES2022)
}

let p1 = new Person('Alice');
p1.greet();  // "Hello, I am Alice"

// 静态方法通过类调用
let p2 = Person.create('Bob');
console.log(p2.name);  // "Bob"

console.log(Person.description);  // "A human being"
// p1.description;  // undefined(实例不能访问静态属性)

getter 和 setter

class Temperature {
  constructor(celsius) {
    this._celsius = celsius;
  }

  // getter:像属性一样访问
  get celsius() {
    return this._celsius;
  }

  set celsius(value) {
    this._celsius = value;
  }

  // getter:计算属性
  get fahrenheit() {
    return this._celsius * 9/5 + 32;
  }

  set fahrenheit(value) {
    this._celsius = (value - 32) * 5/9;
  }
}

let temp = new Temperature(25);
console.log(temp.celsius);     // 25(调用 getter)
console.log(temp.fahrenheit); // 77(调用 getter)

temp.celsius = 30;              // 调用 setter
console.log(temp.fahrenheit); // 86

temp.fahrenheit = 68;           // 调用 setter
console.log(temp.celsius);     // 20

私有字段(ES2022)

class Person {
  #name;        // 私有字段(# 前缀)
  #age;

  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }

  greet() {
    // 只能在类内部访问私有字段
    console.log(`Hello, I am ${this.#name}`);
  }

  getAge() {
    return this.#age;
  }

  #privateMethod() {   // 私有方法
    console.log('This is private');
  }
}

let p = new Person('Alice', 25);
p.greet();              // "Hello, I am Alice"
console.log(p.getAge());   // 25

// p.#name;        // SyntaxError:私有字段不能在类外访问
// p.#privateMethod(); // SyntaxError

封装

闭包封装(ES5 及之前)

function createPerson(name) {
  let age = 0;  // 私有变量

  return {
    getName() {
      return name;  // name 被闭包捕获
    },
    getAge() {
      return age;
    },
    setAge(newAge) {
      if (newAge >= 0) {
        age = newAge;
        return true;
      }
      return false;
    }
  };
}

let p = createPerson('Alice');
console.log(p.getName());  // "Alice"
console.log(p.getAge());    // 0
p.setAge(25);
console.log(p.getAge());    // 25

// p.name;   // undefined(不能直接访问)
// p.age;     // undefined

Symbol 封装

const _name = Symbol('name');
const _age = Symbol('age');

class Person {
  constructor(name, age) {
    this[_name] = name;
    this[_age] = age;
  }

  getName() {
    return this[_name];
  }

  getAge() {
    return this[_age];
  }
}

let p = new Person('Alice', 25);
console.log(p.getName());  // "Alice"
console.log(p.getAge());    // 25

// p._name;  // undefined(Symbol 键不容易被访问)
// 但可以通过 Object.getOwnPropertySymbols() 获取

ES6 class 私有字段(#)

class Person {
  #name;
  #age;

  constructor(name, age) {
    this.#name = name;
    this.#age = age;
  }

  greet() {
    console.log(`Hello, I am ${this.#name}`);
  }

  getInfo() {
    return { name: this.#name, age: this.#age };
  }
}

let p = new Person('Alice', 25);
p.greet();              // "Hello, I am Alice"
console.log(p.getInfo());   // { name: "Alice", age: 25 }

// p.#name;  // SyntaxError

多态

方法重写(Override)

class Animal {
  speak() {
    console.log('Animal makes a noise');
  }
}

class Dog extends Animal {
  speak() {
    console.log('Dog barks');
  }
}

class Cat extends Animal {
  speak() {
    console.log('Cat meows');
  }
}

let animals = [new Dog(), new Cat()];

// 多态:不同对象对同一方法有不同的实现
animals.forEach(animal => animal.speak());
// 输出:
// Dog barks
// Cat meows

通过原型链实现多态

function Animal() {}

Animal.prototype.speak = function() {
  console.log('Animal makes a noise');
};

function Dog() {}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
  console.log('Dog barks');
};

function Cat() {}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.speak = function() {
  console.log('Cat meows');
};

let animals = [new Dog(), new Cat()];
animals.forEach(animal => animal.speak());
// 输出:
// Dog barks
// Cat meows

Mixin(混入)

Mixin 是一种将功能"混入"到类中的模式,实现类似多继承的效果。

Object.assign() 实现混入

// 定义 Mixin
let CanEat = {
  eat() {
    console.log(`${this.name} is eating`);
  }
};

let CanSleep = {
  sleep() {
    console.log(`${this.name} is sleeping`);
  }
};

// 使用 Mixin
class Person {
  constructor(name) {
    this.name = name;
  }
}

// 将 Mixin 的方法混入到 Person.prototype
Object.assign(Person.prototype, CanEat, CanSleep);

let p = new Person('Alice');
p.eat();     // "Alice is eating"
p.sleep();   // "Alice is sleeping"

class Mixin

// Mixin 定义
let Flyable = {
  fly() {
    console.log(`${this.name} is flying`);
  }
};

let Swimmable = {
  swim() {
    console.log(`${this.name} is swimming`);
  }
};

// 基类
class Animal {
  constructor(name) {
    this.name = name;
  }
}

// 使用 Mixin
class Bird extends Animal {}
Object.assign(Bird.prototype, Flyable);

class Fish extends Animal {}
Object.assign(Fish.prototype, Swimmable);

let bird = new Bird('Eagle');
bird.fly();    // "Eagle is flying"

let fish = new Fish('Salmon');
fish.swim();  // "Salmon is swimming"

Mixin vs 继承

特性 Mixin 继承
关系 “有能力做…”(has-a) “是一个…”(is-a)
数量 多个 Mixin 单继承(JS 不支持多继承)
适用场景 可复用的横切关注点 层次化的类型关系
方法冲突 后混入的覆盖先混入的 子类覆盖父类

this 指向回顾

方法调用

let person = {
  name: 'Alice',
  greet() {
    console.log('Hello, ' + this.name);
  }
};

person.greet();  // this 指向 person → "Hello, Alice"

函数调用

function showThis() {
  console.log(this);
}

showThis();  // 浏览器: window,Node.js: global 或 undefined(严格模式)

call / apply / bind

function greet(message) {
  console.log(message + ', ' + this.name);
}

let person = { name: 'Alice' };

// call:逐个传参
greet.call(person, 'Hello');  // "Hello, Alice"

// apply:数组传参
greet.apply(person, ['Hi']);   // "Hi, Alice"

// bind:返回新函数,永久绑定 this
let greetAlice = greet.bind(person, 'Hey');
greetAlice();  // "Hey, Alice"

箭头函数继承外层 this

let person = {
  name: 'Alice',
  greet() {
    // 普通函数:this 指向调用者
    setTimeout(function() {
      console.log(this.name);  // undefined(this 不是 person)
    }, 100);

    // 箭头函数:继承外层 this
    setTimeout(() => {
      console.log(this.name);  // "Alice"
    }, 200);
  }
};

person.greet();

综合示例

示例 1:使用 ES6 class 实现用户系统

class User {
  #id;
  #name;
  #email;

  constructor(id, name, email) {
    this.#id = id;
    this.#name = name;
    this.#email = email;
  }

  get info() {
    return {
      id: this.#id,
      name: this.#name,
      email: this.#email
    };
  }

  greet() {
    console.log(`Hello, I am ${this.#name}`);
  }

  static create(id, name, email) {
    if (!email.includes('@')) {
      throw new Error('Invalid email');
    }
    return new User(id, name, email);
  }
}

// 管理员用户(继承 User)
class Admin extends User {
  constructor(id, name, email, role) {
    super(id, name, email);
    this.role = role;
  }

  manage() {
    console.log(`${this.#name} is managing as ${this.role}`);
  }

  // 重写 greet
  greet() {
    super.greet();
    console.log('I am an admin');
  }
}

// 使用
let admin = Admin.create(1, 'Alice', '[email protected]', 'admin');
admin.greet();
// 输出:
// Hello, I am Alice
// I am an admin
console.log(admin.info);

示例 2:使用 Mixin 实现可复用功能

// Mixins
let Timestamped = {
  setTimestamp() {
    this.timestamp = new Date();
  },
  getTimestamp() {
    return this.timestamp;
  }
};

let Activable = {
  activate() {
    this.isActive = true;
    console.log(`${this.name} activated`);
  },
  deactivate() {
    this.isActive = false;
    console.log(`${this.name} deactivated`);
  },
  get isActivated() {
    return !!this.isActive;
  }
};

// 使用 Mixin
class User {
  constructor(name) {
    this.name = name;
  }
}

Object.assign(User.prototype, Timestamped, Activable);

let user = new User('Alice');
user.setTimestamp();
console.log(user.getTimestamp());  // Date object
user.activate();                   // "Alice activated"
console.log(user.isActivated);      // true

示例 3:原型链继承(ES5 方式)

function Shape(color) {
  this.color = color;
}

Shape.prototype.getColor = function() {
  return this.color;
};

function Circle(radius, color) {
  Shape.call(this, color);  // 继承属性
  this.radius = radius;
}

// 继承方法
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;

// 添加自己的方法
Circle.prototype.getArea = function() {
  return Math.PI * this.radius ** 2;
};

// 使用
let circle = new Circle(5, 'red');
console.log(circle.getColor());  // "red"
console.log(circle.getArea().toFixed(2));  // "78.54"

总结:JavaScript OOP 速查表

概念 ES5 方式 ES6+ class 方式
定义"类" function Foo() {} class Foo {}
构造函数 function Foo() { this.x = 1; } constructor() { ... }
原型方法 Foo.prototype.bar = function() {} bar() { ... }
继承 Child.prototype = Object.create(Parent.prototype) class Child extends Parent {}
调用父类 Parent.call(this, args) super(args)
静态方法 Foo.staticMethod = function() {} static method() { ... }
私有性 闭包、Symbol #privateField(ES2022)
多态 方法重写 + 原型链 方法重写 + extends

继承方式对比

方式 优点 缺点 推荐度
原型链继承 简单 引用类型共享,无法传参 ❌ 不推荐
构造函数继承 解决引用共享 无法继承原型方法 ❌ 不推荐
组合继承 常用方式(ES5) 调用两次父类构造函数 ✅ ES5 推荐
寄生组合继承 最优方案(ES5) 代码稍复杂 ✅✅ ES5 最佳
ES6 class 语法简洁,语义清晰 需要现代浏览器/Node.js ✅✅✅ 现代推荐

核心记忆:JavaScript 的 OOP 基于原型,ES6 class 只是语法糖。理解原型链是掌握 JS OOP 的关键。