吃透 JS 原型:从 0 到 1 理解原型式面向
一、为什么 JS 没有 Class,却能实现面向对象?
学过 Java/Python 的同学都知道,这些语言靠class定义类、new创建实例,是 “血缘式” 的面向对象 —— 类是模板,实例是模板的具体产物。 但 ES5 及之前的 JS 没有class关键字,却依然能实现面向对象,核心靠的是原型(prototype) 这套机制:
- JS 的面向对象是 “原型式” 的,没有严格的 “类 - 实例” 血缘关系;
- 所有对象都会关联一个 “原型对象”,实例可以共享原型上的属性和方法;
- 构造函数 + 原型,共同模拟出了 “类” 的效果。 先看一个最直观的例子:想创建多辆 “小米 SU7”,每辆车有自己的颜色,却共享 “下赛道” 的方法。
// 构造函数:模拟“类”的属性(实例独有)
function Car(color) {
// this指向new创建的新对象,每个实例的color都是独有的
this.color = color;
}
// 原型对象:存储共享的属性/方法(所有实例共用)
Car.prototype = {
drive() {
console.log('drive, 下赛道');
},
name: 'su7', // 所有车都叫su7
height: 1.4, // 共享属性
weight: 1.5,
long: 4800,
}
// 创建实例
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, 下赛道(原型共享方法)
从这个例子能看出: • 构造函数(Car)负责定义实例独有的属性(比如color); • 原型对象(Car.prototype)负责定义所有实例共享的属性 / 方法(比如name、drive); • 哪怕创建 100 辆 SU7,drive方法也只会在内存中存一份,所有实例共用,极大节省内存。
二、原型核心概念:3 个关键角色
想要吃透原型,必须先搞懂「构造函数、原型对象、实例」三者的关系,这是原型的核心。
1. 构造函数(Constructor)
- 本质是普通函数,但首字母习惯大写(区分普通函数);
- 必须用new调用,调用时会自动创建新对象,this指向这个新对象;
- 作用:初始化实例的独有属性。
function Person(name, age) {
// this = 新创建的空对象(new自动生成)
this.name = name; // 每个实例的name独有的
this.age = age; // 每个实例的age独有的
}
2. 原型对象(prototype)
- 每个函数都有一个prototype属性,值是一个普通对象;
- 原型上的属性 / 方法,会被该构造函数的所有实例共享;
- 原型对象有一个constructor属性,指向对应的构造函数(默认)。
- 原型对象(prototype) • 每个函数都有一个prototype属性,值是一个普通对象; • 原型上的属性 / 方法,会被该构造函数的所有实例共享; • 原型对象有一个constructor属性,指向对应的构造函数(默认)。
// 给Person原型添加共享属性
Person.prototype.speci = '人类';
// 创建两个实例
const person1 = new Person('张三', 18);
const person2 = new Person('金总', 19);
console.log(person1.speci); // 人类(来自原型)
console.log(person2.speci); // 人类(来自原型)
console.log(Person.prototype.constructor === Person); // true(原型指向构造函数)
3. 实例(Instance)
• 由new 构造函数创建的对象,是 “具体产物”; • 每个实例都有一个私有属性__proto__(ES5 推荐用Object.getPrototypeOf()替代),指向构造函数的原型对象; • 实例访问属性 / 方法时,先查自己的属性,没有就去__proto__指向的原型对象找。
const su = new Person('舒老板', 19);
// 实例的__proto__ 严格等于 构造函数的prototype
console.log(su.__proto__ === Person.prototype); // true
// ES5标准写法(更推荐)
console.log(Object.getPrototypeOf(su) === Person.prototype); // true
三者关系图(核心)
构造函数 Person
↓(prototype)
原型对象 Person.prototype {speci: '人类', constructor: Person}
↑(__proto__)
实例 su {name: '舒老板', age: 19}
实例 person1 {name: '张三', age: 18}
三、原型查找规则:先找自己,再找原型
实例访问属性 / 方法时,遵循 “就近原则”,这是原型最核心的运行机制:
- 先检查实例自身是否有该属性 / 方法,有则直接使用;
- 如果没有,就通过__proto__去原型对象找;
- 如果原型对象也没有,就继续往原型的原型找(原型链),直到null。 看这个例子就能秒懂:
function Person() {}
// 原型上定义species属性
Person.prototype.species = '人类';
const su = new Person();
// 给实例自身添加species属性
su
.species = 'LOL达人';
// 优先用实例自身的属性
console.log(su.species); // LOL达人
// 删除实例自身的species属性
delete su.species;
// 实例没有了,就去原型找
console.log(su.species); // 人类
再举一个隐藏的例子:为什么普通对象能调用toString()?
const su = new Person();
console.log(su.toString()); // [object Object]
// 拆解查找过程:
// 1. su自身没有toString → 查su.__proto__(Person.prototype)
// 2. Person.prototype也没有toString → 查Person.prototype.__proto__(Object.prototype)
// 3. Object.prototype有toString方法,执行!
console.log(su.__proto__.__proto__ === Object.prototype); // true
这就是原型链的雏形:所有对象最终都会指向Object.prototype,而Object.prototype的原型是null(原型链的终点)。
四、原型链:原型的原型,直到 null
原型链是原型机制的延伸,核心是:每个原型对象本身也是对象,也有自己的__proto__。 用一个经典的继承例子理解原型链:
// 1. 创建基础对象(原型终点之一)
var obj = { species: '动物' };
// 2. 定义Animal构造函数,原型指向obj
function Animal() {}
Animal.prototype = obj;
// 3. 定义Person构造函数,原型是Animal的实例
function Person() {}
Person.prototype = new Animal();
// 4. 创建Person实例
var su = new Person();
// 查找su.species的过程:
// su → su.__proto__(Animal实例) → Animal实例.__proto__(obj) → 找到species: '动物'
console.log(su.species); // 动物
// 查找su.toString()的过程:
// su → Animal实例 → obj → Object.prototype → 找到toString
console.log(su.toString()); // [object Object]
原型链的核心价值:实现属性 / 方法的继承,这也是 ES6class继承的底层原理(class只是原型的语法糖)。
五、ES6 Class:原型的语法糖
ES6 新增的class关键字,让 JS 看起来像 “传统面向对象”,但底层依然是原型机制:
// ES6 Class写法
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHi() {
console.log(`你好,我是${this.name}`);
}
}
// 等价于ES5原型写法
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function() {
console.log(`你好,我是${this.name}`);
}
const su = new Person('舒老板', 19);
su
.sayHi(); // 两种写法效果完全一致
记住:Class 是原型的语法糖,没有改变 JS 原型式面向对象的本质。
六、核心知识点总结
- JS 的面向对象是原型式的,构造函数 + 原型模拟 “类”,没有传统 Class 的血缘关系;
- 构造函数管实例独有属性,原型对象(prototype)管所有实例共享属性 / 方法;
- 实例的__proto__指向构造函数的prototype,属性查找遵循 “先自身、后原型、再原型链”;
- 原型链的终点是null,所有对象最终都继承自Object.prototype;
- ES6class是原型的语法糖,底层逻辑不变。