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
原型链查找规则
当访问对象属性时,查找顺序是:
- 先在当前对象自身查找
- 如果没找到,到其原型(proto)中查找
- 继续向上,直到
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 的关键。