手写JavaScript 的 instanceof 运算符:从原型链到继承机制详解

41 阅读4分钟

手写 JavaScript 的 instanceof 运算符:从原型链到继承机制详解

JavaScript 作为 web 开发中最受欢迎的编程语言, 其独特的原型链机制是掌握对象导向编程(OOP)的关键。今天,我们深入探讨 instanceof 运算符——这个用于检验实例关系的强大工具。

原型和原型链:JavaScript 对象的“家族血统”

在 JavaScript 中,几乎一切皆对象,而原型(prototype)便是这些对象的“血脉传承”。每个对象都拥有一个隐式属性 __proto__,它指向其构造函数的 prototype 属性。这就构成了原型链(prototype chain),犹如一个家族谱系,从当前对象逐级向上追溯,直至根源 Object.prototype

打个比方:假设你是一个 Person 实例,你的 __proto__ 指向 Person.prototype(家族模板),而 Person.prototype__proto__ 则指向 Object.prototype(万物之祖)。通过这条链条,对象能“继承”上游的属性和方法,例如数组能调用 toString(),因为它在链上找到了 Object.prototype.toString

为什么原型链如此至关重要?在大型多人协作项目中,对象类型多样,如果不明晰原型关系,代码易生bug——如意外重写原型方法,导致应用行为失常。作为扩展,在调试时,常使用 console.dir(obj) 查看原型链,这能快速定位继承问题。

instanceof:原型关系的“血缘鉴定”利器

instanceof 是 JavaScript 的二元运算符,用于判定一个实例是否为某构造函数的“后裔”。其语法为 A instanceof B,本质是检查 A 的原型链上是否包含 B 的 prototype。若有,则返回 true;否则 false。

这一特性在其他 OOP 语言(如 Java 的 instanceof 或 C# 的 is)中亦常见。为什么需要它?在复杂项目中,对象来源纷杂,你可能不确定一个对象的属性与方法。这时,instanceof 充当快速校验工具,避免类型错误。例如,验证数组:arr instanceof Array

在当代 JavaScript 中,虽有 Array.isArray() 等更精准方法,但 instanceof 仍大放异彩,尤其在自定义类继承场景。它注重“血统”,忽略表象,仅凭原型链判断。这在实现多态(polymorphism)时尤为实用,如动物园系统中,辨识动物是否为猫的实例。

来看示例:

function Person() {}
function Animal() {}
Person.prototype = new Animal();
const p = new Person();
console.log(p instanceof Person); // true
console.log(p instanceof Animal); // true

此处,p 的原型链为:p.__proto__Person.prototype(即 new Animal()) → Animal.prototypeObject.prototype。故它兼属 Person 与 Animal。

再观数组原型链:

const arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
console.log(arr.__proto__.__proto__.__proto__); // null

链条以 null 终结,标志尽头。

手写 instanceof:剖析其底层原理

手写 instanceof。核心逻辑:从左操作数(实例)的 __proto__ 出发,沿链向上遍历,检验是否匹配右操作数(构造函数)的 prototype。匹配则 true;至 null 未匹配则 false。

function isInstanceOf(left, right) {
    let proto = left.__proto__;
    while (proto) {
        if (proto === right.prototype) {
            return true;
        }
        proto = proto.__proto__;
    }
    return 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
console.log(isInstanceOf(dog, Animal)); // true
console.log(isInstanceOf(dog, Object)); // true
console.log(isInstanceOf(dog, Cat)); // false

此实现简洁高效。扩展:在 ES6+ 中,可用 Object.getPrototypeOf(left) 替代 __proto__,更规范。但 __proto__ 浏览器兼容性佳,适合教学。在生产代码中,我建议处理边缘案,如 left 非对象时返回 false(仿原生行为)。

JavaScript 继承方式:子类共享父类“遗产”

继承核心在于子类获取父类属性与方法。JavaScript 无传统类继承(ES6 class 系语法糖),而依原型链实现。以下剖析几种方式,由浅入深。

1. 构造函数绑定继承(借助 call/apply)

借父类构造函数,将其 this 绑定子类实例。

function Animal() {
    this.species = '动物';
}
function Cat(name, color) {
    Animal.apply(this); // 借用 this
    this.name = name;
    this.color = color;
}
const cat = new Cat('小黑', '黑色');
console.log(cat.species); // '动物'

优点:简便,继承实例属性。缺点:无法继承原型方法,且重复创建属性,耗内存。扩展:适用于继承私有非共享属性。

2. Prototype 模式:父实例作子原型

赋父实例予子类 prototype,实现原型继承。

function Animal() {
    this.species = '动物'; // 注:共享属性宜置原型,避免浪费
}
function Cat(name, color) {
    this.name = name;
    this.color = color;
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修正 constructor
const cat = new Cat('小黑', '黑色');
console.log(cat.species); // '动物'
console.log(cat.__proto__.constructor === Cat); // true

优点:继承原型方法。缺点:子类共享父实例属性,修改影响全局;子类无法传参父类。扩展:优化共享属性至 Animal.prototype.species = '动物';,并合 call/apply,形成“组合继承”——经典方案,均衡实例与原型。

3. 空对象中介继承(寄生组合继承)

避直接改父原型,用空对象桥接。提供的“直接继承 prototype”变体示例如下,但有副作用:

function Animal() {}
Animal.prototype.species = '动物';
function Cat(name, color) {
    Animal.call(this);
    this.name = name;
    this.color = color;
}
Cat.prototype = Animal.prototype; // 引用同对象,易污染
Cat.prototype.constructor = Cat;
Cat.prototype.sayHello = function() {
    console.log('hello');
};
const cat = new Cat('小黑', '黑色');
console.log(cat.species); // '動物'
console.log(Animal.prototype.constructor === Cat); // true(副作用)

问题在于共享同一 prototype,改 Cat 污 Animal。优化用空对象:

function inheritPrototype(child, parent) {
    const prototype = Object.create(parent.prototype); // 空对象继承
    prototype.constructor = child;
    child.prototype = prototype;
}

inheritPrototype(Cat, Animal);

此法无副作用,为 ES5 首选。扩展:ES6 class 以 extends 掩盖此逻辑,但底层仍原型链。熟稔这些,便于运用 Babel 等转译器。

原型链与继承的实战价值

手写 instanceof 揭开 JavaScript 内幕,凸显原型链于继承之要。在大型项目中,此知识助你筑牢代码基石,如定制类库或排查库问题。继承非万金油,组合常常更灵活。