深挖JS原型链:从手写instanceof到继承的N种实现
在JavaScript中,原型链是面向对象编程的核心基石。无论是判断实例归属的instanceof运算符,还是实现类之间的继承,都绕不开对原型链的操作。本文将从手写instanceof的底层逻辑入手,逐行解析代码实现,再深入探讨JS继承的几种经典方式,带你吃透原型链的本质。
一、先搞懂:instanceof到底在查什么?
instanceof是JS中用于判断对象与构造函数之间原型关系的运算符,其核心规则是:检测构造函数的 prototype 属性是否出现在实例对象的原型链上。
1. 原生instanceof的代码解析
先看一段基础代码:
function Animal() {}
function Person() {}
// 将Person的原型指向Animal的实例,建立原型链关联
Person.prototype = new Animal();
const p = new Person();
console.log(p instanceof Person); // true
console.log(p instanceof Animal); // true
p是Person的实例,因此p.__proto__ === Person.prototype,第一个判断为true;- 而
Person.prototype是Animal的实例,所以Person.prototype.__proto__ === Animal.prototype,p的原型链能找到Animal.prototype,第二个判断也为true。
再看数组的原型链结构:
const arr = []; // 等价于new Array()
console.log(arr.__proto__, // Array.prototype(数组原型对象)
arr.__proto__.constructor, // Array(构造函数本身)
arr.constructor // Array(实例的constructor指向构造函数)
);
console.log(arr.__proto__.__proto__, // Object.prototype(数组原型的原型是对象原型)
arr.__proto__.__proto__.constructor, // Object(对象构造函数)
arr.__proto__.__proto__.__proto__, // null(原型链终点)
arr.__proto__.__proto__.__proto__.__proto__ // 报错
);
这段代码清晰展示了数组的原型链层级:arr -> Array.prototype -> Object.prototype -> null,这也是所有JS对象的原型链最终走向。
2. 手写instanceof:逐行拆解实现逻辑
根据instanceof的核心规则,我们可以手动实现一个isInstanceof函数,代码如下:
function isInstanceof(left, right) {
// 1. 获取实例对象的原型
let proto = left.__proto__;
// 2. 循环遍历原型链
while(proto) {
// 3. 判断当前原型是否等于构造函数的prototype
if (proto === right.prototype) {
return true;
}
// 4. 沿着原型链向上查找(直到proto为null)
proto = proto.__proto__;
}
// 5. 原型链遍历完毕仍未找到,返回false
return false;
}
核心解析:
left.__proto__获取实例的直接原型,这是原型链遍历的起点;while(proto)循环的终止条件是proto为null(原型链终点);- 核心判断逻辑,若当前原型与构造函数的
prototype相等,说明实例属于该构造函数的类型; - 若未匹配到,继续向上遍历原型链(
proto = proto.__proto__是原型链遍历的关键操作); - 遍历完整个原型链仍未匹配,返回
false。
测试该函数:
function Animal() {}
function Cat() {}
Cat.prototype = new Animal();
function Dog() {}
Dog.prototype = new Animal();
const dog = new Dog();
console.log(isInstanceof(dog,Dog)); // true(dog的原型链包含Dog.prototype)
console.log(isInstanceof(dog,Animal)); // true(dog的原型链包含Animal.prototype)
console.log(isInstanceof(dog,Object)); // true(所有对象的原型链都包含Object.prototype)
console.log(isInstanceof(dog,Cat)); // false(dog的原型链不包含Cat.prototype)
测试结果完全符合预期,验证了手写实现的正确性。在大型项目中,instanceof能帮助我们快速判断对象的原型归属,避免因属性/方法归属不清导致的bug,这也是其实际开发价值所在。
二、JS继承的几种实现方式:深入解析代码逻辑
继承的本质是让子类复用父类的属性和方法,JS中继承的实现均基于原型链操作。下面逐行解析几种经典的继承方式。
1. 构造函数绑定继承(call/apply)
代码实现:
function Animal() {
this.species = '动物'; // 父类的实例属性
}
function Cat(name, color) {
// 1. 调用父类构造函数,将this绑定为子类实例
Animal.apply(this);
// 2. 定义子类自身的实例属性
this.name = name;
this.color = color;
}
const cat = new Cat('小黑', '黑色');
console.log(cat.species); // 动物(成功继承父类属性)
核心逻辑:
Animal.apply(this)将父类构造函数的this绑定为子类实例cat,因此父类的species属性被添加到cat实例上;- 这种方式只能继承父类构造函数内的实例属性,无法继承父类原型上的属性/方法,复用性有限。
2. prototype模式继承(父类实例作为子类原型)
代码实现:
function Animal() {
this.species = '动物'; // 父类实例属性(若多个子类实例共享,可能造成内存浪费)
}
function Cat(name, color) {
this.name = name; // 子类实例属性
this.color = color;
}
// 1. 将子类原型指向父类实例,建立原型链继承
Cat.prototype = new Animal();
// 2. 修正子类原型的constructor指向(否则指向Animal)
Cat.prototype.constructor = Cat;
const cat = new Cat('小黑', '黑色');
console.log(cat.species); // 动物(通过原型链访问父类属性)
console.log(cat.__proto__.constructor, // Cat(修正后的constructor)
Cat.prototype.constructor // Cat(与__proto__.constructor一致)
);
核心逻辑:
Cat.prototype = new Animal()是原型链继承的关键,此时Cat.prototype.__proto__ === Animal.prototype,子类实例的原型链能访问到父类的属性/方法;- 由于
Cat.prototype被重新赋值为Animal的实例,其constructor属性会指向Animal,因此需要手动修正为Cat,保证constructor的正确性; - 缺点:父类的实例属性会被所有子类实例共享(如
Animal中若定义this.friends = ['狗'],所有Cat实例都会共享该数组),且创建子类实例时无法向父类构造函数传参。
3. 直接继承prototype(子类原型指向父类原型)
代码实现:
function Animal() {}
// 父类原型上定义属性(避免实例属性共享问题,性能更优)
Animal.prototype.species = '动物';
function Cat(name, color) {
// 1. 调用父类构造函数(若父类有实例属性,需手动绑定this)
Animal.call(this);
this.name = name;
this.color = color;
}
// 2. 子类原型直接指向父类原型(引用传递)
Cat.prototype = Animal.prototype;
// 3. 修正子类原型的constructor指向
Cat.prototype.constructor = Cat;
// 4. 子类原型上添加方法
Cat.prototype.sayHello = function() {
console.log('hello');
}
var cat = new Cat('小黑', '黑色');
console.log(cat.species); // 动物(通过原型链访问)
console.log(Animal.prototype.constructor); // Cat(副作用:父类原型的constructor被修改)
核心逻辑:
Cat.prototype = Animal.prototype让子类原型与父类原型指向同一个对象,原型链更短,访问效率更高;- 缺点(严重副作用):由于是引用传递,修改子类原型的属性/方法会直接影响父类原型(如第15行添加
sayHello方法后,Animal.prototype也会拥有该方法;第13行修正constructor后,Animal.prototype.constructor也被改为Cat),这会导致父类及其它子类受到影响,实际开发中不建议使用。
4. 利用空对象作为中介的继承(优化直接继承prototype的副作用)
基于直接继承prototype的问题,我们可以引入一个空对象作为中介,隔离子类原型与父类原型的直接引用,代码实现如下(补充代码逻辑):
function Animal() {}
Animal.prototype.species = '动物';
function Cat(name, color) {
Animal.call(this);
this.name = name;
this.color = color;
}
// 定义空函数作为中介
function F() {}
// 中介函数的原型指向父类原型
F.prototype = Animal.prototype;
// 子类原型指向中介函数的实例(切断与父类原型的直接引用)
Cat.prototype = new F();
// 修正子类原型的constructor
Cat.prototype.constructor = Cat;
const cat = new Cat('小黑', '黑色');
console.log(cat.species); // 动物(正常继承)
console.log(Animal.prototype.constructor); // Animal(父类原型的constructor未被修改)
核心逻辑:
- 引入空函数
F作为中介,Cat.prototype指向F的实例,而F.prototype指向Animal.prototype,这样既保留了原型链继承的关系,又避免了子类原型与父类原型的直接引用,解决了直接继承prototype的副作用问题。这种方式也是“寄生式继承”和“寄生组合式继承”的核心基础。
三、总结:原型链是JS继承的灵魂
无论是instanceof的原型链检测,还是继承的多种实现方式,本质都是对原型链的操作。理解原型链的核心规则:
- 每个对象都有
__proto__属性,指向其构造函数的prototype; - 构造函数的
prototype也是对象,同样拥有__proto__,形成链式结构(原型链); - 原型链的终点是
null,且Object.prototype.__proto__ === null(所有对象的原型链最终都指向Object.prototype,再到null)。
在ES6中,class和extends语法糖简化了继承的实现,但底层依然基于原型链逻辑。深入理解原型链的运作机制,不仅能帮助我们手写instanceof、实现自定义继承,更能解决实际开发中与原型相关的各类bug,真正掌握JS面向对象编程的本质。