一、OOP 基础:面向对象编程核心概念
1.1 面向对象三要素:封装、继承、多态
封装是把数据和方法「打包」成一个整体,比如用函数包裹逻辑;继承是子类复用父类特性,避免重复代码;多态是不同对象对同一消息作出不同响应(比如猫狗都会「叫」但声音不同)。这三者让代码更易维护和扩展,是编程界的「黄金铁三角」~
1.2 传统 OOP vs JS 原型式 OOP
Java、C++ 等语言靠「类」实现 OOP(先定义类,再用类创建对象),而 JS 早期没有 class 关键字,却靠「原型机制」实现了更灵活的面向对象 —— 它不像传统类那样严格划分「父子关系」,而是让对象通过「原型链」互相「委托」,就像搭积木一样灵活组合功能~
二、JS 原型核心:构造函数、原型对象、实例的「三角关系」
2.1 构造函数:对象的「生产线」
JS 里没有真正的「类」,但我们可以用「首字母大写的函数」模拟类(这是约定俗成的规范),这种函数通过 new 调用时,就成了「构造函数」—— 专门生产对象的「生产线」。
// 构造函数(模拟类)
function Person(name, age) {
// this 指向新创建的实例对象
this.name = name; // 给实例添加 name 属性
this.age = age; // 给实例添加 age 属性
}
2.2 原型对象:共享方法的「仓库」
每个函数天生自带 prototype 属性,它的值是一个对象,我们叫它「原型对象」。这个对象的作用很关键:所有通过 new 创建的实例,都能共享原型对象上的属性和方法,不用重复定义。
// 给 Person 的原型对象添加共享方法
Person.prototype.sayHello = function() {
// this 指向调用方法的实例(谁调用就是谁)
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
};
这里有个隐藏的「身份标识」:原型对象上有个 constructor 属性,默认指向它对应的构造函数。比如:
console.log(Person.prototype.constructor === Person); // true(原型对象的构造器是 Person 本身)
2.3 实例对象:__proto__ 连接原型的桥梁
用 new 构造函数创建的对象,就是「实例」。每个实例都有一个隐藏属性 __proto__(隐式原型),它会指向构造函数的 prototype(显式原型)—— 这是实例能访问原型对象方法的关键。
// 创建实例
const hu = new Person('吉他胡', 18);
// 验证连接:实例的 __proto__ 指向构造函数的 prototype
console.log(hu.__proto__ === Person.prototype); // true
// 实例调用原型上的方法(成功!)
hu.sayHello(); // 输出:Hello, my name is 吉他胡 and I'm 18 years old.
三、new 关键字:对象创建的「幕后四步曲」
new 到底做了什么,能让一个函数变成「生产线」?其实就四步,简单到离谱:
-
创建空对象:
const obj = {}(先搞个空壳子) -
绑定原型链:
obj.__proto__ = 构造函数.prototype(给空壳子接上原型「血脉」) -
执行构造函数:
构造函数.call(obj, 参数)(用空壳子当this,给它装属性) -
返回结果:如果构造函数没返回对象,就返回这个空壳子(否则返回构造函数的返回值)
我们甚至能手写一个 new 的模拟函数,加深理解:
function myNew(constructor, ...args) {
// 1. 创建空对象
const obj = {};
// 2. 绑定原型链
obj.__proto__ = constructor.prototype;
// 3. 执行构造函数(用 obj 当 this)
const result = constructor.apply(obj, args);
// 4. 返回结果(如果构造函数返回对象,就用它,否则返回 obj)
return result instanceof Object ? result : obj;
}
// 用自己写的 myNew 创建实例,和原生 new 效果一样!
const hu2 = myNew(Person, '吉他胡2号', 19);
hu2.sayHello(); // 输出:Hello, my name is 吉他胡2号 and I'm 19 years old.
四、原型链:属性查找的「寻宝路线图」
当我们访问一个对象的属性时,JS 会按「原型链」逐层查找,就像寻宝一样:
- 先查自身:如果对象自己有这个属性,直接返回
- 再查原型:如果没有,就通过
__proto__找原型对象 - 逐层向上:原型对象的
__proto__会指向更上层的原型(比如Object.prototype) - 直到终点:查到
Object.prototype还没有?再往上就是null(彻底没了,返回undefined)
案例:解析 hu.contry 的查找过程
// 定义一个对象 a
const a = { contry: '中国', eee: 'eee' };
// 手动修改 hu 的原型链(让 hu.__proto__ 指向 a)
hu.__proto__ = a;
// 查找 hu.contry:
// 1. hu 自身没有 contry → 2. 查 hu.__proto__(即 a)→ 3. a 有 contry,返回 '中国'
console.log(hu.contry); // 输出:中国
// 再查 hu.eee:a 上有,直接返回 'eee'
console.log(hu.eee); // 输出:eee
// 查 hu.name:hu 自身有 name,直接返回 '吉他胡'(不查原型)
console.log(hu.name); // 输出:吉他胡
关键工具:区分自身属性和原型属性
-
obj.hasOwnProperty(prop):判断属性是否是对象「自身」的(不查原型链) -
prop in obj:只要原型链上有这个属性,就返回true(包括原型上的)
console.log(hu.hasOwnProperty('name')); // true(name 是自身属性)
console.log(hu.hasOwnProperty('contry')); // false(contry 是原型 a 上的)
console.log('contry' in hu); // true(原型链上有)
五、ES6 class:原型机制的「优雅糖衣」
ES6 新增的 class 语法,看起来像传统类,其实底层还是原型机制 —— 它只是让原型操作更简单了。
类语法 vs 传统构造函数
// ES6 class(语法糖)
class Person {
// 构造函数(和传统的 constructor 作用一样)
constructor(name, age) {
this.name = name;
this.age = age;
}
// 类方法(等价于挂载到 Person.prototype 上)
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
}
// 本质不变:实例的 __proto__ 依然指向类的 prototype
const hu3 = new Person('吉他胡3号', 20);
console.log(hu3.__proto__ === Person.prototype); // true
继承:extends 背后的原型链
class 的 extends 实现继承,本质是让子类的原型链指向父类:
class Student extends Person {
constructor(name, age, grade) {
super(name, age); // 调用父类构造函数(必须写在 this 之前)
this.grade = grade;
}
}
const stu = new Student('学生胡', 16, '高一');
stu.sayHello(); // 继承自 Person 原型的方法,能直接调用~
六、避坑指南:原型使用的「雷区」
- 别乱改原生对象原型
比如给Array.prototype加方法,可能会污染全局,还可能和未来的 JS 新方法冲突(血的教训!)。 - 方法放原型,属性放构造函数
共享方法(如sayHello)放原型(省内存),实例独有的属性(如name)放构造函数(保证每个实例数据独立)。 - 慎用
__proto__直接修改原型链
虽然能改,但会影响性能(JS 引擎优化会失效),尽量用Object.create等方法更安全。
七、总结:原型是 JS 的「对象基因」
JS 没有传统意义的「类」,但原型机制赋予了它更灵活的面向对象能力:
-
构造函数是「生产线」,原型对象是「共享仓库」,实例通过
__proto__连接它们 -
原型链是属性查找的「路线图」,终点是
null -
ES6
class是原型的「语法糖」,本质没变
理解了原型,你就看透了 JS 对象的「底层逻辑」—— 以后再遇到原型链相关的 bug,就能像拆积木一样轻松解决啦~ 😎