编程范式

JS 面向对象编程(OOP)深入讲解

这篇文章深入讲解 JavaScript 中面向对象编程的核心知识点,包括对象模型、构造函数机制、原型与原型链、属性查找机制以及继承实现方式。

🎯 引言

学会本篇文章后,你将能够理解 JavaScript 的对象模型,掌握构造函数和 new 的工作原理,了解原型与原型链的机制,熟悉属性查找过程,并能灵活应用多种继承方法。对前端开发中面向对象编程有扎实的基础,写出更规范、更高效的代码。


🔍 什么是 JavaScript 的对象模型

JavaScript 是一门基于对象的语言,简单来说,“万物皆对象”是它的核心思想。即使是数字、字符串这些看起来像“值”的东西,在 JavaScript 里也可以作为对象来操作。

值类型 vs 引用类型

先说说这个区分,刚入门的小伙伴一定要理解:

  • 值类型(Primitive Types)包括 NumberStringBooleannullundefinedSymbol(ES6新增)
    这些类型的变量存储的是具体的值,赋值操作是拷贝值本身。
  • 引用类型(Reference Types)主要就是 ObjectArrayFunction
    这些类型的变量存储的是对象的内存地址(引用),赋值操作只是复制了引用地址,指向同一个对象。

举个生活中的例子:

你可以想象值类型就像是“快递包裹”,里面装的是具体物品,复制快递包裹就是复制了物品本身;引用类型就像是“房屋钥匙”,复制钥匙意味着可以打开同一扇门,访问同一套房子。

理解值类型和引用类型的区别,是避免调试中变量赋值导致数据混乱的第一步。

⚙️ 构造函数与 new 的工作机制

构造函数定义方式

在 JavaScript 里,构造函数其实就是普通的函数,但习惯上首字母大写表示它是用来创建对象的。

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

new 做了哪四件事?

当你使用 new Person('Tom', 20) 时,JavaScript 引擎帮你做了下面四件事情:

  1. 创建了一个新对象(空对象)
  2. 将这个新对象的内部 [[Prototype]] 指向构造函数的 .prototype 属性
  3. 调用构造函数,将 this 指向刚创建的新对象,执行函数体代码
  4. 如果构造函数没有返回对象,则返回新创建的对象

模拟实现 new

用代码模拟实现一个简单的 new 操作:

function myNew(fn, ...args) {
    const obj = {};
    Object.setPrototypeOf(obj, fn.prototype);
    const result = fn.apply(obj, args);
    return typeof result === 'object' && result !== null ? result : obj;
}
模拟 new 的过程,能帮助你理解为什么构造函数能创建对象,以及原型链是如何连接的。

🧩 原型与 .prototype 属性

函数为什么有 .prototype?

每个函数在 JavaScript 中都有一个特殊属性叫 .prototype,它是一个对象,默认只有一个 constructor 属性,指向函数本身。

这个 .prototype 对象是构造函数创建实例对象时,实例的 __proto__(内部属性)指向的对象。

实例与原型的关系:proto vs .prototype

  • 实例对象有一个 __proto__ 属性,指向它的构造函数的 .prototype
  • 构造函数有一个 .prototype 属性,指向原型对象。

这种关系就像桥梁,实例通过 __proto__ 连接到构造函数的原型,原型上可以定义共享方法。

const p = new Person('Jack', 25);
console.log(p.__proto__ === Person.prototype); // true

构造函数、原型对象、实例三者关系图

Person (构造函数)
  |
  | .prototype
  ↓
Person.prototype (原型对象) --- (上级原型链) ---> Object.prototype --- null

p (实例对象)
  |__ __proto__ --> Person.prototype
原型对象上的方法是所有实例共享的,可以节省内存,避免每个实例都创建一遍相同的方法。

🌲 原型链与属性查找机制

什么是原型链?

原型链就是实例对象通过 __proto__ 指向构造函数的 .prototype 对象,后者又指向它的上级原型,直到 Object.prototype,再往上是 null,这条链条就像一个链子。

属性/方法查找过程

当你访问对象的属性或方法时,JavaScript 会先在自身属性中查找,如果找不到,会沿着 __proto__ 指向的原型对象查找,一直找到 null 停止。

举个例子:

console.log(p.sayHello); // 首先找 p 对象自身的 sayHello
// 如果 p 自身没有,接着去 Person.prototype 查找

Object.create 的作用与原理

Object.create(proto) 创建了一个新对象,这个新对象的 __proto__ 指向 proto。这是创建继承链的一种简洁方法。

const obj = Object.create(Person.prototype);
Object.create 可以灵活地创建链式继承,而不是通过构造函数。

🌿 继承的实现方式

原型链继承

让子类型的原型指向父类型的实例,实现继承。

function Parent() {
    this.name = 'parent';
}
Parent.prototype.say = function () {
    console.log('hello');
};

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

const c = new Child();
c.say(); // hello

缺点是所有子实例共享父实例的引用属性。

构造函数继承(call)

在子构造函数中调用父构造函数,改变 this 指向,避免引用共享。

function Child() {
    Parent.call(this);
}

缺点是只能继承父构造函数的属性和方法,不能继承原型上的方法。

组合继承

结合上面两种方式,既继承属性也继承方法。

function Child(name) {
    Parent.call(this);
    this.name = name;
}
Child.prototype = new Parent(); // 所以 Child.prototype.constructor 是指向 parent
Child.prototype.constructor = Child; // 修正 constructor 指向为 child

class/extends 的语法糖和本质

ES6 引入 class 语法,让继承写法更简洁:

class Parent {
    constructor() {
        this.name = 'parent';
    }
    say() {
        console.log('hello');
    }
}

class Child extends Parent {
    constructor() {
        super();
    }
}

本质还是基于原型链和构造函数,只是语法更清晰,更像传统面向对象语言。


🧾 小节总结

  • JavaScript 的对象模型基于值类型和引用类型的区分,万物皆对象。
  • 使用构造函数和 new 可以创建对象,new 做了四件关键事。
  • 每个函数都有 .prototype,实例的 __proto__ 指向构造函数的 .prototype,实现原型继承。
  • 属性查找沿着原型链进行,Object.create 是创建原型链的利器。
  • 继承可以通过原型链、构造函数、组合继承等方式实现,ES6 class 是语法糖,本质依旧是原型链和构造函数。

❓ 知识问答(Q&A)

Q:构造函数和普通函数有什么区别?
A:构造函数通常首字母大写,用来创建对象,配合 new 使用,普通函数没有这个限制。

Q:为什么要用原型链实现继承?
A:原型链可以让多个实例共享父类方法,节省内存,写法也更灵活。

Q:Object.create 和直接构造函数继承有什么区别?
A:Object.create 创建对象时能指定原型,更灵活地控制继承关系;构造函数继承更多是属性复制。


🧪 小练习

  1. 使用构造函数和 new 创建一个 Car 对象,有 brandspeed 属性,以及一个 run 方法,实现打印汽车运行状态。
function Car(brand, speed) {
    // 你的代码
}

const myCar = new Car('Toyota', 80);
myCar.run(); // 输出: Toyota is running at 80 km/h
  1. 用原型链方式实现一个动物(Animal)和狗(Dog)的继承,Animal 有 eat 方法,Dog 继承 Animal,并新增 bark 方法。
function Animal() {
    // 你的代码
}

Animal.prototype.eat = function () {
    console.log('Eating food');
};

function Dog() {
    // 你的代码
}

// 继承 Animal 原型

Dog.prototype.bark = function () {
    console.log('Woof!');
};

const dog = new Dog();
dog.eat();
dog.bark();

🎉 恭喜你已经掌握 JavaScript 面向对象编程中对象模型、构造函数与 new、原型链、属性查找以及继承的关键技能啦!用这些知识去写更优雅的代码吧!