ES5原型深度解析:从构造函数到原型链的底层逻辑

0 阅读5分钟

ES5原型深度解析:从构造函数到原型链的底层逻辑

在ES6 class 语法出现之前,JavaScript并没有真正意义上的“类”,但它通过原型链完美实现了面向对象编程。它不像传统类式面向对象那样搞“血缘继承”,而是靠“共享借用”实现复用——本文结合代码示例,用最通俗的方式拆解原型的核心逻辑,新手也能轻松看懂。

一、先搞懂:ES5没有class,怎么模拟“类”?

ES5没有专门的class关键字,但我们可以用【构造函数+原型对象】的组合,实现和“类”一样的效果。比如想创建“小米SU7”,核心就3步:

  1. 写一个首字母大写的构造函数(比如Car),负责初始化实例的“私有属性”;
  2. new关键字创建实例(比如car1car2),每个实例都是独立的;
  3. 在构造函数的prototype上挂载“共享属性/方法”,让所有实例共用。

代码实操:创建小米SU7

    // 构造函数:初始化实例私有属性(每个实例独一份)
    function Car(color) {
      this.color = color; // 比如霞光紫、海湾蓝,每个车颜色不同
    }

    // 原型对象:存储共享属性/方法(所有实例共用一份)
    Car.prototype = {
      drive() {
        console.log('drive,下赛道');
      },
      name: 'su7',       // 所有Car实例都是su7车型
      height: 1.4,       // 共享高度参数
      weight: 1.5,       // 共享重量参数
      long: 4800         // 共享长度参数
    }

    // 创建2个实例
    const car1 = new Car('霞光紫');
    const car2 = new Car('海湾蓝');

    // 访问私有属性(各自独立)
    console.log(car1.color); // 霞光紫
    console.log(car2.color); // 海湾蓝

    // 访问共享属性/方法(共用一份)
    console.log(car1.name);  // su7
    console.log(car2.weight); // 1.5
    car1.drive();            // drive,下赛道

关键区别:

  • 构造函数内的this.color:实例私有,改一个不影响另一个;
  • Car.prototype上的namedrive():所有实例共享,内存中只存一份,节省空间。

二、核心概念:构造函数的2个核心角色

构造函数是原型体系的“主角”,它要干两件大事:

1. 作为constructor:给实例“造私有属性”

当用new调用构造函数时,JS会自动做4件事:

  • 新建一个空对象;
  • this指向这个空对象;
  • 执行构造函数代码,给对象加私有属性;
  • 返回这个对象(不用手动写return)。

比如new Person('张三', 18),会生成一个{name: '张三', age: 18}的实例,每个实例的nameage都是独立的:

    function Person(name, age) {
      this.name = name; // 私有属性
      this.age = age;   // 私有属性
    }
    Person.prototype.speci = '人类'; // 共享属性

    const person1 = new Person('张三', 18);
    const person2 = new Person('金总', 19);

    console.log(person1.name); // 张三(独立)
    console.log(person2.name); // 金总(独立)
    console.log(person1.speci); // 人类(共享)

2. 作为原型载体:通过prototype共享内容

每个函数(包括构造函数)都天生带一个prototype属性,它的值是一个对象——我们叫它“原型对象”。

这个原型对象的作用很简单:存储所有实例都能用的共享内容。不管是属性(比如speci: '人类')还是方法(比如sayHi),只要挂在prototype上,所有实例都能访问到。

这就是JS原型式的规则:不搞“血缘继承”,只搞“共享借用”——实例不用自己带所有属性方法,需要时从原型上“借用”就行。

三、关键关联:实例、原型对象、构造函数的三角关系

要吃透原型,必须记住3个核心关联,结合代码一看就懂:

1. 实例 ↔ 原型对象:__proto__是“桥梁”

所有对象(包括实例)都有一个私有属性__proto__(ES5标准方法是Object.getPrototypeOf()),它直接指向创建这个实例的构造函数的prototype

简单说:实例.proto === 构造函数.prototype

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    Person.prototype = { speci: '人类' }

    const su = new Person('舒老板', 19);
    console.log(su.__proto__ === Person.prototype); // true
    console.log(Object.getPrototypeOf(su) === Person.prototype); // 同样true(推荐用这个)

这就是实例能访问原型属性的原因:实例自己没有,就通过__proto__去原型对象上找。

2. 原型对象 ↔ 构造函数:constructor是“反向指针”

原型对象上有个constructor属性,它会指向对应的构造函数。

简单说:原型对象.constructor === 构造函数

    // 接上面的代码
    console.log(Person.prototype.constructor === Person); // true

注意:如果直接覆盖prototype(比如Person.prototype = { ... }),会弄丢默认的constructor,需要手动恢复,否则原型链会断:

    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    // 直接覆盖prototype,手动恢复constructor
    Person.prototype = {
      speci: '人类',
      sayHi: function() {
        console.log(`你好,我是${this.name}`);
      },
      constructor: Person // 关键:恢复指向
    }

    const su = new Person('舒老板', 19);
    console.log(Person.prototype.constructor === Person); // true(恢复成功)

3. 三角关系总结

3.png

如图所示:

  • 构造函数 → prototype → 原型对象
  • 原型对象 → constructor → 构造函数
  • 实例 → __proto__ → 原型对象

四、原型访问规则:自身有就用,没有就找原型

当你访问实例的某个属性时,JS会按“就近原则”查找:

  1. 先查实例自身有没有这个属性;
  2. 没有的话,通过__proto__找原型对象;
  3. 原型没有,就继续找原型的原型(这就是原型链);
  4. 直到找到Object.prototype,还没有就返回null

代码验证:实例属性“屏蔽”原型属性

    function Person(){}
    // 原型上的属性
    Person.prototype.species = '人类';
    Person.prototype.sayHi = function() {
      console.log(`你好,我是${this.name}`);
    }

    const su = new Person();
    su.species = 'LOL达人'; // 给实例加同名属性

    console.log(su.species); // LOL达人(用实例自己的)
    console.log(su.__proto__.species); // 人类(原型上的没被改)

这里要注意:给实例加和原型同名的属性,不是修改原型,而是在实例上新增属性,从而“屏蔽”原型上的同名属性。

五、原型链:最终指向Object.prototype

所有对象的原型链,最终都会指向Object.prototype,而Object.prototype的原型是null(原型链的终点)。

1. 为什么实例能调用toString()

我们从没在PersonCar的原型上定义toString(),但实例却能调用——因为Person.prototype本身是对象,它的__proto__指向Object.prototype,而toString()Object.prototype的内置方法。

    function Person(){}
    const su = new Person();
    console.log(su.toString()); // [object Object](来自Object.prototype)
    console.log(su.__proto__.__proto__ === Object.prototype); // true
    console.log(Object.prototype.__proto__); // null(原型链终点)

2. 手动模拟原型链

我们可以通过修改prototype的指向,手动构建原型链:

    // 1. 创建Object实例,作为Animal的原型
    var obj = new Object();
    obj.species = '动物';

    // 2. Animal的原型指向obj
    function Animal() {}
    Animal.prototype = obj;

    // 3. Person的原型指向Animal实例
    function Person() {}
    Person.prototype = new Animal();

    // 4. Person实例的原型链:su → Person.prototype → Animal.prototype → Object.prototype → null
    const su = new Person();
    console.log(su.species); // 动物(从Animal.prototype找到)
    console.log(su.toString()); // [object Object](从Object.prototype找到)

在这个代码例子中,su访问species的查找路径是: su(无)→ Person.prototype(无)→ Animal.prototype(有,返回“动物”)

这里附上一张原型链图便于理解:

5.png

六、核心总结

  1. ES5无class,靠“构造函数+prototype”模拟类;
  2. 构造函数管“私有属性”,prototype管“共享属性/方法”;
  3. 实例__proto__ → 构造函数prototype → 原型对象constructor → 构造函数(三角关系);
  4. 属性查找:自身 → 原型 → 原型链 → Object.prototypenull
  5. JS原型式核心:共享借用,而非血缘继承。

理解了ES5构造函数+原型,再去看ES6的class你就会豁然开朗——class只是原型机制的语法糖,但底层还是基于“构造函数+原型”那一套。掌握原型,才算真正走进JavaScript面向对象的大门。希望这篇文章能帮你看清背后的原型链本质,在未来的开发中写出更扎实、更高效的 JavaScript 代码。