从一辆小米 SU7 彻底搞懂 JavaScript 原型与原型链

65 阅读6分钟

深入浅出 JavaScript 原型与原型链:从小米 SU7 说起

大家好!今天,我想和大家聊聊 JavaScript 中一个核心却常常让人困惑的概念——原型(Prototype)和原型链(Prototype Chain)。为什么说它核心?因为 JavaScript 是一种基于原型的面向对象语言,它不像 Java 或 C++ 那样有严格的类继承体系,而是通过原型来实现对象的共享和继承。这套机制既灵活又强大,但也容易让人迷失在“proto”和“prototype”的迷雾中。

想象一下,你想拥有一辆小米 SU7 电动车。现实中,你不会从零开始造车,而是去工厂订购。工厂有蓝图(模板),生产出多辆相同的车,每辆车有自己的颜色,但共享相同的引擎设计和驾驶方法。这就是原型机制的生动比喻:在 JavaScript 中,构造函数像工厂,prototype 像共享蓝图,实例像一辆辆车。通过本文,我们将基于一些实际代码示例,逐步拆解这个概念。文章基于我整理的代码文件进行优化和细化,确保内容严谨且易懂。让我们开始吧!

image.png

1. JavaScript 中的面向对象基础:没有 class 的时代

在 ES6 引入 class 之前,JavaScript 使用函数来模拟类。这听起来有点“原始”,但正是这种原型式面向对象,让 JS 如此灵活。函数是一等公民(First-Class Object),任何函数都可以作为构造函数使用,只要用 new 关键字调用它。

构造函数:对象的“出生证明”

构造函数就是一个普通函数,但当用 new 调用时,它会创建一个新对象,并将 this 指向这个对象。构造函数负责初始化实例的独有属性,比如每辆车的颜色。

来看一个例子:

// 构造函数:模拟小米 SU7 的“工厂”
function Car(color) {
    // this 指向新创建的实例对象
    this.color = color;  // 实例独有属性:颜色
}

// 原型对象:共享属性和方法,节省内存
Car.prototype = {
    name: 'SU7',     // 共享属性:车型名
    height: 1.4,     // 共享属性:高度(米)
    weight: 1.5,     // 共享属性:重量(吨)
    long: 4800,      // 共享属性:长度(mm)
    drive: function() {  // 共享方法:驾驶
        console.log('Drive 下赛道!');
    }
};

// 创建实例
const car1 = new Car('霞光紫');
console.log(car1);  // 输出: Car { color: '霞光紫' }
console.log(car1.weight, car1.name);  // 输出: 1.5 'SU7'(从原型继承)
car1.drive();  // 输出: Drive 下赛道!

const car2 = new Car('海湾蓝');
console.log(car2);  // 输出: Car { color: '海湾蓝' }
console.log(car2.weight, car2.name);  // 输出: 1.5 'SU7'

这里,Car 是构造函数,它只设置了 color 这个独有属性。共享的部分(如 namedrive)放在 prototype 上。为什么这么做?因为如果把所有属性都放进构造函数,每次 new 都会复制一份方法,浪费内存。原型机制让所有实例共享同一份“蓝图”,高效又优雅。

小贴士:在控制台运行时,你会看到 car1 没有直接的 name 属性,但可以通过原型访问。这就是魔力所在!

image.png

2. Prototype:共享的“家族财产”

每个函数都有一个 prototype 属性,它指向一个对象。这个对象上的属性和方法,会被所有由该构造函数创建的实例共享。prototype 不是实例的私有财产,而是“家族公产”。

从提供的 readme.md 中,我们可以提炼出关键点:prototype 是 JS 实现原型式面向对象的核心。它不像传统类那样基于“血缘”(继承树),而是基于“引用”(指向)。

实例与原型的连接:proto

每个对象(包括实例)都有一个私有属性 __proto__,它指向该对象的原型。这就是原型链的起点。

再看一个例子:

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

// 原型上添加共享属性
Person.prototype.species = '人类';

// 创建实例
const person1 = new Person('张三', 18);
const person2 = new Person('李四', 19);

console.log(person1.name, person1.species);  // 输出: 张三 人类
console.log(person2.name, person2.species);  // 输出: 李四 人类

// 检查原型关系
console.log(person1.__proto__ === Person.prototype);  // true
console.log(Person.prototype.constructor === Person); // true(constructor 指向构造函数)

这里,person1.species 实际上是从 Person.prototype 继承的。__proto__ 是隐式的链接,如果你修改实例上的属性,不会影响原型:

// 基于 5.html 优化
person1.species = '顶级大佬';  // 只修改实例自身
console.log(person1.species);  // 输出: 顶级大佬
console.log(person2.species);  // 输出: 人类(原型未变)
console.log(person1.__proto__.species);  // 输出: 人类

这避免了“一人改,全家乱”的问题。实例优先查找自身属性,没有再去 __proto__ 找。

生动比喻:想象 __proto__ 像一根“脐带”,连接实例和原型。原型像祖先的遗产,实例可以继承,但修改自己的不会影响祖先。

9350ebd05fbb4cdf6d3cd5075cb302af.png

3. 原型链:对象的“家谱树”

原型链是 JS 查找属性的机制:从对象自身开始,顺着 __proto__ 一层一层向上找,直到 null

默认情况下,所有对象最终指向 Object.prototype,它提供了像 toString() 这样的内置方法。

例子:

// 创建一个空对象
const obj = new Object();  // 等价于 {}
obj.name = '柯基';
console.log(obj);  // 输出: { name: '柯基' }
console.log(obj.__proto__ === Object.prototype);  // true
console.log(obj.toString());  // 输出: [object Object](从 Object.prototype 继承)

// 原型链继承
function Animal() {}
Animal.prototype.species = '动物';

function Person() {}
Person.prototype = new Animal();  // 设置继承:Person 的原型是 Animal 实例

const kj = new Person();
console.log(kj.species);  // 输出: 动物(从原型链继承)
console.log(kj.__proto__ === Person.prototype);  // true
console.log(kj.__proto__.__proto__ === Animal.prototype);  // true
console.log(kj.toString());  // 输出: [object Object](最终到 Object.prototype)

这里,kj 的原型链是:kjPerson.prototypeAnimal.prototypeObject.prototypenull

查找过程

  1. 查找 kj.species:自身无,查 __proto__(Person.prototype),无,继续查 __proto__.__proto__(Animal.prototype),找到 '动物'。
  2. 如果一直没找到,返回 undefined。
  3. 链的终点是 Object.prototype.__proto__ === null,表示“无原型”。

这实现了继承:Person “继承” Animal 的属性,而无需复制代码。

常见误区

  • __proto__ 是非标准属性(虽浏览器支持),推荐用 Object.getPrototypeOf(obj) 获取原型。
  • 修改 prototype 时小心:如 Person.prototype = {} 会覆盖 constructor,导致 Person.prototype.constructor !== Person。修复方法:手动设置 Person.prototype.constructor = Person
  • 原型链过长会影响性能(层层查找),保持浅层最好。

4. 实际应用:从简单到继承

我们看一个完整例子:

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

Person.prototype = {
    species: '人类',  
    sayHello: function() {
        console.log(`你好,我是 ${this.name}`);
    }
};

const kj = new Person('柯基', 18);
console.log(kj.__proto__ === Person.prototype);  // true
console.log(Person.prototype.constructor === Person);  // true
kj.sayHello();  // 输出: 你好,我是 柯基

现在,扩展到继承:

// 父“类”
function Animal() {}
Animal.prototype = {
    species: '动物',
    eat: function() {
        console.log('吃东西');
    }
};

// 子“类”
function Person(name) {
    this.name = name;
}
Person.prototype = new Animal();  // 继承
Person.prototype.constructor = Person;  // 修复 constructor
Person.prototype.speak = function() {
    console.log(`${this.name} 在说话`);
};

const person = new Person('人类');
console.log(person.species);  // 动物
person.eat();  // 吃东西
person.speak();  // 人类 在说话

这模拟了类继承,但本质是原型委托(delegation)。在现代 JS 中,ES6 的 class 是这个机制的语法糖:

class Animal {
    constructor() {
        this.species = '动物';
    }
    eat() {
        console.log('吃东西');
    }
}

class Person extends Animal {
    constructor(name) {
        super();
        this.name = name;
    }
    speak() {
        console.log(`${this.name} 在说话`);
    }
}

const p = new Person('人类');
p.eat();  // 吃东西

底层仍是 prototype!

5. 深入:原型链的哲学与优化

JS 的面向对象是“原型式”的,不是血缘关系。这体现了 JS 的哲学:一切皆对象,灵活共享。

优化技巧

  • Object.create(proto) 创建对象,指定原型:const obj = Object.create(Animal.prototype);
  • 检查属性来源:obj.hasOwnProperty('prop') 只查自身。
  • 避免循环引用:别让原型链成环,否则无限循环。
  • 性能:原型适合共享不变数据,变数据放实例。

调试工具:在浏览器控制台,用 console.dir(obj) 查看原型链。Chrome DevTools 的“原型”面板也很实用。

结语:掌握原型,驾驭 JS

通过这些例子,我们从小米 SU7 的“工厂”比喻,到原型链的“家谱树”,逐步揭开了 JavaScript 原型的面纱。原型不是抽象概念,而是高效共享的机制。理解它,能让你写出更优雅的代码,避免内存浪费,并在面试中脱颖而出。

建议:动手运行这些代码,修改看看效果。欢迎在评论区分享你的困惑或心得!如果喜欢,点个赞,转发支持一下。更多前端分享,关注我哦~