JavaScript 的继承机制基于原型链,class(ES6)只是原型继承的语法糖。从构造函数到 class,从 __proto__ 到 prototype 再到 instanceof——这三个概念串起来,构成了 JS 继承的全部面貌。理解原型链也能帮助读懂很多库的源码设计。
ES6 之前:构造函数 + prototype
在没有 class 的年代,JavaScript 用构造函数模拟”类”:
// 构造函数 —— 首字母大写只是约定,不是语法
function Student(name, number) {
this.name = name;
this.number = number;
// 不在构造函数里定义方法,否则每个实例都有一份拷贝
}
// 方法放在 prototype 上,所有实例共享
Student.prototype.sayHi = function () {
console.log('姓名 ' + this.name + ',学号 ' + this.number);
};
var xiaoming = new Student('小明', 100);
xiaoming.sayHi(); // "姓名 小明,学号 100"new 操作符做了四件事:
- 创建一个空对象
- 将空对象的
__proto__指向构造函数的prototype - 将
this绑定到空对象,执行构造函数 - 如果构造函数没有返回对象,则返回
this
可以手写一个 myNew 来理解这个过程:
function myNew(constructor, ...args) {
const obj = Object.create(constructor.prototype);
const result = constructor.apply(obj, args);
return result instanceof Object ? result : obj;
}原型链继承(ES5 方式)
// 父类构造函数
function People(name) {
this.name = name;
}
People.prototype.eat = function () {
console.log(this.name + ' eat something');
};
// 子类构造函数
function Student(name, number) {
People.call(this, name); // 调用父类构造函数,继承实例属性
this.number = number;
}
// 关键:将子类 prototype 的原型指向父类 prototype
Student.prototype = Object.create(People.prototype);
Student.prototype.constructor = Student; // 修正 constructor 指向
Student.prototype.sayHi = function () {
console.log('姓名 ' + this.name + ',学号 ' + this.number);
};这段代码是 ES5 继承的经典范式。对比 ES6 的 class extends,体会两者的关系。
ES6 class:语法糖
class Student {
constructor(name, number) {
this.name = name;
this.number = number;
}
sayHi() {
console.log(`姓名 ${this.name},学号 ${this.number}`);
}
}
typeof Student; // 'function' —— class 本质上就是函数class 内部定义的方法默认是不可枚举的(enumerable: false),而 ES5 直接赋值给 prototype 是可枚举的——这是两者在行为上的细微差别。
继承:extends 和 super
class People {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} eat something`);
}
}
class Student extends People {
constructor(name, number) {
super(name); // 必须先调用 super(),否则 this 不可用
this.number = number;
}
sayHi() {
console.log(`姓名 ${this.name},学号 ${this.number}`);
}
}
const xiaoming = new Student('小明', 100);
xiaoming.sayHi(); // "姓名 小明,学号 100"
xiaoming.eat(); // "小明 eat something"几个细节:
super()必须在this之前调用——子类没有自己的this,需要通过super()从父类借。在super()之前访问this会报 ReferenceError- 方法可以重写(override)——子类定义同名方法覆盖父类,没有定义则沿原型链向上找
eat方法只存在于People.prototype上——所有实例共享同一份函数,这就是原型继承节省内存的地方
原型链:__proto__ 和 prototype
每个函数(包括 class)都有显示原型 prototype。每个对象(实例)都有隐式原型 __proto__。现代规范推荐用 Object.getPrototypeOf() 和 Object.setPrototypeOf(),但调试和面试中 __proto__ 更常用。
xiaoming.__proto__ → Student.prototype
Student.prototype.__proto__ → People.prototype
People.prototype.__proto__ → Object.prototype
Object.prototype.__proto__ → null
xiaoming.__proto__ === Student.prototype; // true
Student.prototype.__proto__ === People.prototype; // true
People.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__; // null —— 原型链终点属性查找沿链向上
执行 xiaoming.sayHi() 时的查找路径:
1. xiaoming 自身有 sayHi 吗? → 没有
2. xiaoming.__proto__(Student.prototype)有吗? → 找到了!
执行 xiaoming.eat() 时:
1. 自身 → 没有
2. Student.prototype → 没有
3. People.prototype → 找到了!
xiaoming.hasOwnProperty('name'); // true —— 构造函数赋值在实例自身
xiaoming.hasOwnProperty('sayHi'); // false —— sayHi 在 Student.prototype 上
xiaoming.hasOwnProperty('eat'); // false —— eat 在 People.prototype 上
xiaoming.hasOwnProperty('toString'); // false —— toString 在 Object.prototype 上hasOwnProperty 只检查自身属性,不看原型链。它来自 Object.prototype,任何对象都可以调用(除非原型链被 Object.create(null) 打断)。
实例方法 vs 原型方法:内存视角
class Bad {
constructor() {
// 每个实例创建一份独立的函数副本 —— 浪费内存
this.sayHi = () => {
console.log('hi');
};
}
}
class Good {
// 所有实例共享 prototype 上的同一份函数
sayHi() {
console.log('hi');
}
}
const a = new Bad();
const b = new Bad();
console.log(a.sayHi === b.sayHi); // false —— 两份不同的函数
const x = new Good();
const y = new Good();
console.log(x.sayHi === y.sayHi); // true —— 共享,高效什么时候需要在 constructor 里定义方法?当你需要箭头函数的
this绑定,或者方法需要访问构造函数闭包里的私有变量时(不过现在有私有字段#field了)。
instanceof 的判断逻辑
沿着变量的 __proto__ 链向上查找,看能否匹配到某个 class 的 prototype:
xiaoming instanceof Student; // true —— __proto__ 匹配到 Student.prototype
xiaoming instanceof People; // true —— 继续向上匹配
xiaoming instanceof Object; // true —— 最终匹配到 Object.prototype
xiaoming instanceof Array; // false —— 遍历完整条链都没找到
[] instanceof Array; // true
[] instanceof Object; // true可以手写一个 myInstanceof 来理解:
function myInstanceof(obj, constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto) {
if (proto === constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}instanceof 可以判断数组,但跨 iframe 场景下会失效(不同 iframe 有各自的 Array 构造函数),这时用 Array.isArray() 更可靠。
Object.create 与原型式继承
除了构造函数和 class,还可以用 Object.create() 直接创建一个以指定对象为原型的实例:
const parent = {
greet() {
console.log('hello ' + this.name);
},
};
const child = Object.create(parent);
child.name = 'xiaoming';
child.greet(); // "hello xiaoming"
// child.__proto__ === parent → trueObject.create(null) 创建一个没有原型链的纯字典对象——连 toString 和 hasOwnProperty 都没有,非常适合做纯粹的 key-value 存储,不用担心原型污染。
手写简易 jQuery
这个例子展示了原型在库设计中的应用——实例方法在 prototype 上,插件通过扩展 prototype 实现:
class jQuery {
constructor(selector) {
const result = document.querySelectorAll(selector);
const length = result.length;
for (let i = 0; i < length; i++) {
this[i] = result[i];
}
this.length = length;
this.selector = selector;
}
get(index) {
return this[index];
}
each(fn) {
for (let i = 0; i < this.length; i++) {
fn.call(this, this[i], i);
}
return this; // 返回 this 支持链式调用
}
on(eventName, fn) {
return this.each((elem) => {
elem.addEventListener(eventName, fn, false);
});
}
}
// 插件:扩展 prototype,所有已有和未来的实例自动获得新方法
jQuery.prototype.dialog = function (info) {
alert(info);
};
// 使用
const $p = new jQuery('p');
$p.get(1);
$p.each((elem) => console.log(elem.nodeName));
$p.on('click', () => alert('clicked'));
$p.dialog('hello');这个设计的精妙之处:任何时候给 jQuery.prototype 添加方法,所有实例都能立刻使用——因为 __proto__ 指向的是同一个 prototype 对象。这也是为什么 jQuery 插件生态能如此繁荣。