Skip to content

JavaScript 面向对象

> [!important] > - JavaScript 的面向对象本质是基于原型,不是传统的 class-based 继承。 > - class 是语法糖,底层依然是原型和原型链。 > - 实例自己的属性通常放在构造函数或 constructor 里,共享方法放在 prototype 上。 > - new 的核心是:创建对象、绑定 this、连接原型、返回对象。 > - 继承的本质是让子对象能沿着原型链找到父对象的方法。 > - ES5 里更推荐 寄生组合式继承,ES6 里直接用 class extends

1. 对面向对象的理解

面向对象的核心思想是把数据和行为组织到对象上,通过封装、继承、多态提升代码复用性、可维护性和扩展性。
但在 JavaScript 里,面向对象并不是传统语言里的“类实例化一切”,而是建立在对象和原型链之上的。

三大特征

1. 封装

把数据和操作数据的方法放到一起,并对外暴露稳定接口,隐藏内部实现细节。

JavaScript 里常见的封装方式:

  • 对象属性和方法
  • 构造函数 / class
  • 闭包
  • ES Module

2. 继承

让子对象复用父对象的属性和方法,减少重复代码。

JavaScript 的继承底层依赖的是原型链,不是传统语言里的类复制。

3. 多态

同一个接口,在不同对象上表现出不同的行为。

js
class Animal {
  speak() {
    console.log('...');
  }
}

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

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

JavaScript 面向对象和传统面向对象的区别

  • 传统语言通常是基于类
  • JavaScript 本质上是基于原型
  • class 只是让原型写法更像传统 OOP

> [!tip] > 面试里如果被问“JavaScript 是不是面向对象”,可以回答: > JavaScript 支持面向对象编程,但它的对象系统是基于原型的,不是传统的类式继承。

2. 原型、原型链、newinstanceof

这一部分是整篇的基础。如果原型相关概念没理清,后面的“对象创建方式”和“继承方式”都容易背混。

2.1 prototype__proto__constructor 的区别

先看一段代码:

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

const p = new Person('Alice');

它们之间的关系是:

  • Person.prototype 构造函数显式拥有的原型对象

  • p.__proto__ 实例对象的隐式原型,通常等于 Person.prototype

  • Person.prototype.constructor 默认指回构造函数 Person

js
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.constructor === Person); // true

可以先这样记:

  • prototype函数身上的属性
  • __proto__对象身上的属性
  • constructor 通常用来指回“这个原型对象原本属于谁”

> [!warning] > __proto__ 更偏历史兼容属性,理解它可以,但业务代码里更推荐用 Object.getPrototypeOf()Object.setPrototypeOf()

2.2 什么是原型链

当我们访问一个对象属性时:

  1. 先在对象本身找
  2. 找不到就去它的原型上找
  3. 原型上还找不到,就继续沿着原型的原型往上找
  4. 一直找到 Object.prototype
  5. 再找不到就是 undefined

这个查找链条就是原型链

js
function Person() {}
Person.prototype.sayHi = function () {
  console.log('hi');
};

const p = new Person();
p.sayHi();

这里 sayHi 并不在 p 自己身上,而是在 Person.prototype 上。

2.3 new 操作符做了什么

new Foo() 大致会做四件事:

  1. 创建一个新的空对象
  2. 让这个对象的原型指向 Foo.prototype
  3. 让构造函数里的 this 指向这个新对象
  4. 执行构造函数代码,并返回这个对象

可以粗略模拟成:

js
function mockNew(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype);
  const result = Constructor.apply(obj, args);
  return result !== null && (typeof result === 'object' || typeof result === 'function')
    ? result
    : obj;
}

2.4 instanceof 的原理

a instanceof B 的本质是:

判断 B.prototype 是否出现在 a 的原型链上。

js
function Person() {}
const p = new Person();

console.log(p instanceof Person); // true
console.log(p instanceof Object); // true

因为:

  • p.__proto__ === Person.prototype
  • Person.prototype.__proto__ === Object.prototype

所以两次判断都成立。

3. 对象创建的方式有哪些

JavaScript 常见的对象创建方式有:

  • 对字面量
  • 工厂模式
  • 构造函数模式
  • 原型模式
  • 组合模式
  • 动态原型模式
  • 寄生构造函数模式
  • ES6 class

真正开发里最常见的是:

  • 简单对象:对象字面量
  • 批量创建实例:构造函数 + 原型,或者直接用 class

先看一张对比表

方式优点缺点面试定位
对象字面量简单直观不适合批量创建相似对象基础
工厂模式封装创建过程无法准确识别类型早期方案
构造函数模式可配合 new 批量创建方法会重复创建高频
原型模式方法共享,节省内存引用类型属性容易共享高频
组合模式兼顾实例属性独立和方法共享写法稍显分散高频
动态原型模式把初始化逻辑写在一起理解门槛略高了解
寄生构造函数模式创建时可增强对象类型识别差了解
class写法清晰,现代开发主流本质仍是原型语法糖高频

3.1 对象字面量

最简单直接的创建方式。

js
const user = {
  name: 'Alice',
  age: 18,
  sayHi() {
    console.log(`Hi, ${this.name}`);
  },
};

特点:

  • 适合创建单个对象
  • 写法直观
  • 不适合批量创建结构相似的对象

3.2 工厂模式

通过函数封装对象创建过程。

js
function createPerson(name, age) {
  return {
    name,
    age,
    sayHi() {
      console.log(`Hi, ${this.name}`);
    },
  };
}

const p1 = createPerson('Bob', 20);

优点:

  • 封装创建逻辑
  • 使用简单

缺点:

  • 返回的是普通对象
  • 无法通过 instanceof createPerson 判断类型

3.3 构造函数模式

通过 new 调用函数来创建对象。

js
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHi = function () {
    console.log(`Hi, ${this.name}`);
  };
}

const p2 = new Person('Charlie', 22);

优点:

  • 能批量创建相似实例
  • 实例和构造函数有明确关联
  • 可以用 instanceof

缺点:

  • 如果方法写在构造函数里,每个实例都会拷贝一份方法
  • 不利于方法共享

3.4 原型模式

把需要共享的方法和属性定义在原型上。

js
function Person() {}

Person.prototype.name = 'default';
Person.prototype.sayHi = function () {
  console.log(`Hi, ${this.name}`);
};

const p3 = new Person();
p3.sayHi();

优点:

  • 方法只创建一次
  • 所有实例共享原型方法

缺点:

  • 不适合定义实例独有属性
  • 如果把引用类型数据放在原型上,会被所有实例共享
js
function Person() {}
Person.prototype.hobbies = ['music'];

const a = new Person();
const b = new Person();
a.hobbies.push('game');

console.log(b.hobbies); // ['music', 'game']

3.5 组合模式

实例属性写在构造函数里,把共享方法写在原型上。
这是 ES5 时代最常见、最实用的创建自定义类型方式。

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

Person.prototype.sayHi = function () {
  console.log(`Hi, ${this.name}`);
};

const p4 = new Person('Diana', 23);
p4.sayHi();

优点:

  • 实例属性互不影响
  • 方法可共享

缺点:

  • 构造函数和原型上的定义被拆开了

3.6 动态原型模式

把“原型方法初始化”也放进构造函数里,但只初始化一次。

js
function Person(name, age) {
  this.name = name;
  this.age = age;

  if (typeof Person.prototype.sayHi !== 'function') {
    Person.prototype.sayHi = function () {
      console.log(`Hi, ${this.name}`);
    };
  }
}

const p5 = new Person('Eve', 24);
p5.sayHi();

优点:

  • 既保留了组合模式的优点
  • 又让“对象相关逻辑”看起来集中在一起

缺点:

  • 面试中了解即可,实际开发不如 class 直观

3.7 寄生构造函数模式

本质上是“构造函数写法的工厂模式”,构造函数内部返回一个增强后的对象。

js
function SpecialArray(...values) {
  const arr = [];
  arr.push(...values);
  arr.toPipedString = function () {
    return this.join('|');
  };
  return arr;
}

const arr = new SpecialArray(1, 2, 3);
console.log(arr.toPipedString()); // 1|2|3

优点:

  • 创建对象时可以顺手扩展功能

缺点:

  • 返回的对象和构造函数之间没有正常原型关系
  • arr instanceof SpecialArrayfalse

3.8 ES6 class

class 是现代开发最常用的写法,但它本质上还是构造函数 + 原型的语法糖。

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

  sayHi() {
    console.log(`Hi, ${this.name}`);
  }
}

const p6 = new Person('Frank', 25);
p6.sayHi();

它和构造函数 + 原型模式的关系可以理解为:

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

Person.prototype.sayHi = function () {
  console.log(`Hi, ${this.name}`);
};

优点:

  • 写法更清晰
  • 更适合表达继承关系
  • 更贴近现代团队代码风格

缺点:

  • 容易让人误以为 JavaScript 真的是基于 class 的语言
  • 如果不理解底层原型机制,面试里容易答浅

3.9 对象创建方式总结

  • 对象字面量:适合简单对象
  • 工厂模式:能封装创建过程,但类型识别差
  • 构造函数模式:解决批量创建,但方法会重复创建
  • 原型模式:方法共享好,但引用属性容易共享
  • 组合模式:ES5 时代最常见的实践
  • 动态原型模式:了解即可
  • 寄生构造函数模式:了解即可
  • class:现代开发主流,但底层还是原型

> [!tip] > 面试里如果被问“最推荐哪种对象创建方式”,可以回答: > ES5 里更推荐构造函数 + 原型的组合模式,现代项目里通常直接用 class

4. 对象继承的方式有哪些

JavaScript 的继承本质是原型链继承,但围绕“属性独立”“方法共享”“可传参”“避免重复调用父构造函数”等问题,衍生出了多种实现方式:

  • 原型链继承
  • 借用构造函数继承
  • 组合继承
  • 原型式继承
  • 寄生式继承
  • 寄生组合式继承
  • ES6 class extends

先看一张对比表

方式能传参引用属性是否独立方法是否共享是否推荐
原型链继承不推荐
借用构造函数不推荐
组合继承可用
原型式继承依赖原型对象了解
寄生式继承一般了解
寄生组合式继承ES5 最推荐
class extends现代开发主流

4.1 原型链继承

让子类原型等于父类实例。

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

Parent.prototype.sayHi = function () {
  console.log('hi');
};

function Child() {}
Child.prototype = new Parent();
Child.prototype.constructor = Child;

const c1 = new Child();
const c2 = new Child();

c1.colors.push('green');
console.log(c2.colors); // ['red', 'blue', 'green']

优点:

  • 写法简单
  • 原型方法可复用

缺点:

  • 引用类型属性会被实例共享
  • 无法给父构造函数传参

4.2 借用构造函数继承

在子构造函数里调用父构造函数,把父类属性拷贝到子实例上。

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

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

const c1 = new Child('Alice', 10);
const c2 = new Child('Bob', 12);

优点:

  • 可以传参
  • 引用类型属性不会共享

缺点:

  • 只能继承父构造函数里的属性
  • 无法复用父类原型上的方法

4.3 组合继承

结合原型链继承和借用构造函数继承

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

Parent.prototype.sayHi = function () {
  console.log(`Hi, ${this.name}`);
};

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

Child.prototype = new Parent();
Child.prototype.constructor = Child;

const c1 = new Child('Alice', 10);
c1.sayHi();

优点:

  • 可以传参
  • 实例属性独立
  • 原型方法共享

缺点:

  • 父构造函数会调用两次 一次是 Parent.call(this, ...) 一次是 new Parent()

4.4 原型式继承

直接基于某个对象创建新对象,本质上就是 Object.create()

js
const parent = {
  name: 'parent',
  sayHi() {
    console.log(`Hi, ${this.name}`);
  },
};

const child = Object.create(parent);
child.name = 'child';
child.sayHi();

优点:

  • 写法简洁
  • 适合基于现有对象创建一个新对象

缺点:

  • 如果原型对象里有引用类型属性,依然会共享
  • 不适合复杂“类式继承”场景

4.5 寄生式继承

在原型式继承基础上,对克隆出的对象再做增强。

js
function createAnother(original) {
  const clone = Object.create(original);
  clone.sayBye = function () {
    console.log('bye');
  };
  return clone;
}

const parent = {
  name: 'parent',
  sayHi() {
    console.log(`Hi, ${this.name}`);
  },
};

const child = createAnother(parent);
child.sayBye();

优点:

  • 灵活,适合对已有对象做增强

缺点:

  • 不适合需要类型体系的场景
  • 方法往往不是共享的

4.6 寄生组合式继承

这是 ES5 里最理想、最经典的继承方案。

核心思路:

  1. Parent.call(this) 继承实例属性
  2. Object.create(Parent.prototype) 继承原型方法
  3. 避免 Parent 被调用两次
js
function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}

Parent.prototype.sayHi = function () {
  console.log(`Hi, ${this.name}`);
};

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

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

const c1 = new Child('Alice', 10);
const c2 = new Child('Bob', 12);

c1.colors.push('green');
console.log(c2.colors); // ['red', 'blue']
c1.sayHi(); // Hi, Alice

优点:

  • 可以传参
  • 引用属性独立
  • 原型方法共享
  • 父构造函数只调用一次

缺点:

  • 写法比 class 更绕

4.7 ES6 class extends

现代开发里最常见的继承方式。

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

  sayHi() {
    console.log(`Hi, ${this.name}`);
  }
}

class Child extends Parent {
  constructor(name, age) {
    super(name);
    this.age = age;
  }
}

const c3 = new Child('Tom', 12);
c3.sayHi();

特点:

  • extends 建立继承关系
  • super() 调用父类构造函数
  • 写法清晰,语义强

本质:

  • 底层仍然依赖原型链
  • 从效果上看,可以理解为更现代、更规范的原型继承写法

4.8 继承方式总结

  • 原型链继承:简单,但引用属性共享、不能传参
  • 借用构造函数继承:属性独立,但方法不能共享
  • 组合继承:两者结合,但会调用父构造函数两次
  • 原型式继承:适合基于对象克隆
  • 寄生式继承:适合在克隆对象时做增强
  • 寄生组合式继承:ES5 里最推荐
  • class extends:现代开发主流

> [!tip] > 面试里如果被问“最推荐哪种继承方式”,可以回答: > ES5 里推荐寄生组合式继承,现代项目里直接使用 class extends

5. class 和构造函数有什么区别

class 本质上是构造函数和原型继承的语法糖,但它在语法、约束和可读性上都更接近传统 OOP。

1. class 必须通过 new 调用

js
class Person {}

// Person(); // 报错
new Person();

而普通函数是否用 new,取决于你怎么设计它。

2. class 中的方法默认定义在原型上

js
class Person {
  sayHi() {}
}

这和手动写:

js
function Person() {}
Person.prototype.sayHi = function () {};

本质上是一样的。

3. class 更适合表达继承关系

js
class Child extends Parent {}

可读性明显比手动改原型链更强。

4. class 内部默认更严格

比如:

  • 不能直接当普通函数调用
  • 需要先 super() 再使用 this
  • 更贴近现代规范

6. 高频追问

1. 为什么实例方法不推荐定义在构造函数里?

因为每 new 一个实例,就会创建一份新的方法,浪费内存,无法复用。
共享方法更适合放在原型上。

2. Object.create() 有什么作用?

它会创建一个新对象,并把这个新对象的原型指向传入对象。
常用于:

  • 原型式继承
  • 寄生组合式继承
  • 创建一个“纯净对象”如 Object.create(null)

3. newObject.create() 的区别?

  • new 会执行构造函数
  • Object.create() 只负责创建对象并指定原型,不会自动执行构造函数

4. 为什么说 class 是语法糖?

因为它没有改变 JavaScript 的对象模型,底层依然是构造函数、原型对象和原型链。

5. 为什么寄生组合式继承优于组合继承?

因为它避免了父构造函数调用两次,既保留了“属性独立 + 方法共享”,又减少了冗余初始化。

7. 一句话总结

  • JavaScript 面向对象的底层是原型和原型链
  • 对象创建的核心是理解 new、构造函数和原型
  • 继承的核心是理解“实例属性怎么独立,原型方法怎么共享”
  • ES5 里重点掌握组合模式寄生组合式继承
  • ES6 里重点掌握 classextendssuper

评论区

欢迎留言、补充或勘误。

xiaoba.blog