原型与面向对象

理解 JavaScript 继承机制的核心秘密

🎯 学习目标

学完本章,你将能够:

  • 能够使用构造函数和 new 操作符创建对象实例
  • 能够解释原型链的工作原理,绘制原型关系图
  • 能够使用 prototype 在构造函数上共享方法,节省内存
  • 能够使用 instanceof 判断对象类型,理解其底层原理
  • 能够对比不同对象创建模式(工厂、构造函数、原型)的优缺点

你有没有想过

为什么两个对象能调用同一个方法,却不占用两份内存?为什么 arr.push() 能用,但你从来没给 arr 定义过 push?

答案藏在 JavaScript 最独特的机制里:原型链。

JavaScript 没有传统面向对象语言中的"类继承",而是通过原型链实现对象之间的委托。理解原型,就理解了 JS 中对象、继承和 this 的底层逻辑。

Part 1: 构造函数

批量生产对象的"工厂模板"

你已经会写对象字面量 { name: "张三", age: 25 },但如果要创建 100 个结构相同的对象呢?总不能手写 100 遍。构造函数配合 new 关键字,就是用来批量"生产"对象的模板。搞懂 new 背后做了什么,才能理解后面的原型和继承。

构造函数与 new

用一个函数当模板,批量创建相似对象

构造函数就是普通函数,只是约定首字母大写。配合 new 关键字,它能批量生产结构一致的对象。

打个比方:构造函数是"饼干模具",new 是"压一下"的动作。每压一次,就产出一块形状相同的饼干。

// 构造函数(首字母大写是约定)
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHi = function() {
        console.log(`Hi, I'm ${this.name}`);
    };
}

let person1 = new Person("张三", 25);
let person2 = new Person("李四", 30);

person1.sayHi(); // "Hi, I'm 张三"
person2.sayHi(); // "Hi, I'm 李四"

new 做了什么

四步走:创建、绑定、执行、返回

当你写 new Person("张三", 25) 时,引擎背后执行了四个步骤:

1. 创建空对象

在内存中开辟一个新对象 {}

2. 绑定 this

将构造函数的 this 指向新对象

3. 执行代码

运行构造函数内部的赋值语句

4. 返回对象

自动返回新对象(除非手动 return 了对象)

// 手动模拟 new 的过程
function myNew(Constructor, ...args) {
    const obj = {};                    // 步骤1
    Constructor.apply(obj, args);      // 步骤2+3
    return obj;                        // 步骤4
}

忘记 new 怎么办

漏掉 new,this 会"离家出走"

忘记写 new 不会报错,但 this 会指向全局对象(非严格模式)或直接抛错(严格模式)。这是一个隐蔽的 bug 来源。
// 忘记 new 的后果
let person3 = Person("王五", 28);
// 非严格模式:this 指向 window/global
// 严格模式:TypeError: Cannot set property

// 安全的构造函数:无论有没有 new 都能工作
function SafePerson(name) {
    if (!(this instanceof SafePerson)) {
        return new SafePerson(name);
    }
    this.name = name;
}
let p1 = SafePerson("测试"); // 正常工作

Part 2: 原型机制

所有对象共享方法的秘密

用构造函数创建对象有个问题:每个实例都会复制一份方法,100 个实例就有 100 份相同函数,浪费内存。怎么让所有实例共享一份方法?答案就是原型(prototype)。每个函数自带一个 prototype 属性,它就像一个公共公告栏——上面贴的东西,所有实例都能看到。

prototype 是什么

每个函数自带一个"公共仓库"

prototype 是函数的一个属性,它是一个对象,用来存放所有实例共享的属性和方法。

打个比方:构造函数是"班级",prototype 是"班级公告栏"。公告栏上的东西,所有同学(实例)都能看到和使用。

function Animal(name) {
    this.name = name; // 每个实例独有
}

// 把方法放在 prototype 上——所有实例共用一份
Animal.prototype.speak = function() {
    console.log(`${this.name} makes a sound`);
};

let dog = new Animal("Dog");
let cat = new Animal("Cat");

dog.speak(); // "Dog makes a sound"
cat.speak(); // "Cat makes a sound"

// 关键:两个实例共享的是同一个方法
console.log(dog.speak === cat.speak); // true

__proto__ 链接

每个对象都有一个"隐秘指针"指向原型

每个 JS 对象都有一个内部属性 __proto__(也叫 [[Prototype]]),它指向构造函数的 prototype。

这是实例能访问原型方法的根本原因:实例通过 __proto__ 找到了原型对象上的方法。

let dog = new Animal("Dog");

// 实例的 __proto__ 指向构造函数的 prototype
console.log(dog.__proto__ === Animal.prototype); // true

// 原型对象的 constructor 指回构造函数
console.log(Animal.prototype.constructor === Animal); // true

// 三者关系:
// dog ---> dog.__proto__ (Animal.prototype) ---> Animal
// dog.constructor === Animal(通过原型链找到的)

原型关系图

一张图看懂构造函数、原型、实例三角关系

图:构造函数、原型与实例的关系

实例通过 __proto__ 找到原型,原型通过 constructor 找回构造函数。这就是经典的"三角关系"。

属性与方法分家

属性放构造函数,方法放原型

放哪里什么内容为什么
构造函数里属性(name、age)每个实例的值不同,需要独立存储
prototype 上方法(sayHi、speak)逻辑相同,共享一份节省内存
function User(name, email) {
    this.name = name;       // 属性:每个实例独有
    this.email = email;
}
User.prototype.getInfo = function() {
    return `${this.name} (${this.email})`;
};

let u1 = new User("张三", "z@qq.com");
let u2 = new User("李四", "l@qq.com");
console.log(u1.getInfo()); // "张三 (z@qq.com)"

Part 3: 原型链

对象查找属性的"寻宝路线"

知道原型是什么了,那当你访问 obj.name 时,引擎到底是怎么找的?先查自身,找不到就沿着 __proto__ 往上找,一层一层,直到找到或者到达 null 为止。这条查找路径就是原型链。理解了它,你就明白为什么所有对象都能调用 toString、hasOwnProperty 这些方法。

什么是原型链

对象找属性时,沿着 __proto__ 一层层往上找

当你访问 obj.name 时,引擎先在 obj 自身上找。找不到?就沿着 __proto__ 到原型上找。还找不到?继续往上……直到 null 为止。

这条查找路径就叫原型链。就像你找东西:先翻自己口袋,再翻书包,再翻抽屉,直到找到或找无可找。

function GrandParent() { this.family = "Smith"; }
GrandParent.prototype.heritage = "old money";

function Parent() { this.job = "engineer"; }
Parent.prototype = Object.create(GrandParent.prototype);
Parent.prototype.constructor = Parent;

function Child() { this.school = "MIT"; }
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

let child = new Child();
console.log(child.school);    // "MIT"   ← 自身属性
console.log(child.job);       // "engineer" ← Parent.prototype
console.log(child.heritage);  // "old money" ← GrandParent.prototype

原型链查找图

从 child 出发,一路向上直到 null

图:原型链的查找路径

查找顺序:child → Child.prototype → Parent.prototype → GrandParent.prototype → Object.prototype → null(终点)

自身还是继承

用 hasOwnProperty 区分属性来源

console.log(child.hasOwnProperty("school"));   // true  ← 自身
console.log(child.hasOwnProperty("job"));       // false ← 继承的
console.log(child.hasOwnProperty("heritage"));  // false ← 继承的

// Object.getPrototypeOf:获取原型
console.log(Object.getPrototypeOf(child) === Child.prototype); // true

// 原型链终点
console.log(Object.prototype.__proto__); // null
安全写法:如果对象自身有一个叫 hasOwnProperty 的属性,obj.hasOwnProperty() 会失效。安全做法:Object.prototype.hasOwnProperty.call(obj, "name")

Part 4: 原型风险

动原型链就像改地基,一改全楼晃

原型链很强大,但也意味着修改它会影响到所有实例。往 Array.prototype 上加一个方法,所有数组立刻都能用——听着很方便,但隐患也很大。这一部分讲清楚哪些操作安全、哪些操作危险,以及遇到原型相关 bug 时该怎么排查。

扩展原生原型

能做不代表应该做

不要随意扩展原生对象的原型!这可能导致与其他库冲突、与未来 ES 标准冲突、for...in 遍历到额外属性。
// 危险操作!不要这样做
Array.prototype.first = function() {
    return this[0];
};
[1, 2, 3].first(); // 1 ——能用,但别这样

// 如果必须扩展,至少让它不可枚举
Object.defineProperty(Array.prototype, 'last', {
    value: function() { return this[this.length - 1]; },
    enumerable: false,
    writable: true,
    configurable: true
});

Part 5: instanceof

判断"你是不是某类的实例"

有了原型链的基础,instanceof 的原理就很好理解了:它沿着对象的原型链逐级检查,看构造函数的 prototype 是否在链上。最后还会对比几种创建对象的模式,帮你决定在什么场景下该用哪种。

instanceof 原理

检查构造函数的 prototype 在不在原型链上

instanceof 的判断逻辑:沿着对象的原型链,逐级检查是否存在构造函数的 prototype。

function Dog(name) { this.name = name; }
let d = new Dog("Buddy");

console.log(d instanceof Dog);    // true
console.log(d instanceof Object); // true(所有对象都继承自 Object)

// 手动实现 instanceof
function myInstanceof(obj, Constructor) {
    let proto = Object.getPrototypeOf(obj);
    while (proto !== null) {
        if (proto === Constructor.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

// 原始类型不适用 instanceof
console.log(42 instanceof Number);   // false
console.log("hi" instanceof String); // false

instanceof 流程

沿着原型链逐级比对 prototype

图:instanceof 运算符的判断流程

创建模式对比

工厂 vs 构造函数 vs Object.create

工厂函数

function createUser(name) {
    return {
        name,
        greet() {
            return `Hi, ${this.name}`;
        }
    };
}
// 简单、灵活
// 没有 prototype 开销
// 不能用 instanceof

构造函数 + 原型

function User(name) {
    this.name = name;
}
User.prototype.greet = function() {
    return `Hi, ${this.name}`;
};
// 方法共享(省内存)
// 可以用 instanceof
// 写法较繁琐

现代开发中工厂函数越来越流行——简单直观、没有 this 绑定问题。构造函数+原型是 JS 继承的底层机制,class 语法糖就是基于它。下一章会看到 ES6 的 class 如何简化写法。

常见坑与排错

原型链上的 bug 通常很隐蔽。以下是最常踩的坑,遇到时先对照排查。

问题一:修改原型后,已有实例不更新

原因:用 Animal.prototype = { ... } 整体替换了原型,但已有实例的 __proto__ 还指向旧原型。

修复:使用 Animal.prototype.newMethod = ... 逐个添加,而不是整体替换。

问题二:constructor 丢失

原因:用 Child.prototype = Object.create(Parent.prototype) 后,constructor 指向了 Parent。

修复:替换后手动修正 Child.prototype.constructor = Child;

问题三:方法放在构造函数里导致内存浪费

原因:每次 new 都会创建一个新的函数实例。

修复:把方法移到 prototype 上,所有实例共享一份。

动手练一练

练习一

任务:创建一个 Car 构造函数,包含品牌和价格属性,并在 prototype 上添加一个 getInfo 方法。

// 要求:
// 1. new Car("Toyota", 200000) 创建实例
// 2. 实例.getInfo() 返回 "Toyota - ¥200000"
// 你的代码:

参考答案

function Car(brand, price) {
    this.brand = brand;
    this.price = price;
}
Car.prototype.getInfo = function() {
    return `${this.brand} - ¥${this.price}`;
};

let myCar = new Car("Toyota", 200000);
console.log(myCar.getInfo()); // "Toyota - ¥200000"

练习二

任务:实现 ElectricCar 继承 Car,增加 battery 属性和 charge() 方法。

// 要求:
// 1. ElectricCar 继承 Car 的所有属性和方法
// 2. new ElectricCar("Tesla", 350000, 100)
// 3. 实例.charge() 返回 "Tesla 正在充电...100kWh"
// 你的代码:

参考答案

function ElectricCar(brand, price, battery) {
    Car.call(this, brand, price);
    this.battery = battery;
}
ElectricCar.prototype = Object.create(Car.prototype);
ElectricCar.prototype.constructor = ElectricCar;

ElectricCar.prototype.charge = function() {
    return `${this.brand} 正在充电...${this.battery}kWh`;
};

let tesla = new ElectricCar("Tesla", 350000, 100);
console.log(tesla.charge());   // "Tesla 正在充电...100kWh"
console.log(tesla.getInfo());  // "Tesla - ¥350000"

综合练习

练习三:类型检查器

任务:实现一个 typeCheck 函数,利用原型链判断值的真实类型。要能区分数组、null、普通对象。

console.log(typeCheck([]));        // "array"
console.log(typeCheck(null));      // "null"
console.log(typeCheck({}));        // "object"
console.log(typeCheck(42));        // "number"
console.log(typeCheck("hello"));   // "string"

提示:用 Object.prototype.toString.call(value),它返回 "[object Type]" 格式。

参考答案

function typeCheck(value) {
    if (value === null) return "null";
    if (Array.isArray(value)) return "array";
    return typeof value === "object"
        ? Object.prototype.toString.call(value)
              .slice(8, -1).toLowerCase()
        : typeof value;
}

📝 要点总结

本章核心知识点

构造函数

new 创建实例,共享 prototype 上的方法

原型链

对象通过 __proto__ 链接形成链式查找

属性共享

方法放 prototype,属性放实例

instanceof

判断对象是否是某个构造函数的实例

创建模式

工厂、构造函数、原型三种模式对比

下一章

你已经掌握了原型链——JavaScript 继承的根基。下一章我们学习 ES6+ 核心特性,看看现代 JS 是如何让代码更简洁、更强大的。

1 / X