我手写instanceof之后,才真正知道了原型链是条什么“链”

90 阅读3分钟

前言

最近在准备面试,总被问到instanceof的实现原理。看了不少文章,感觉都是在背答案。今天自己动手写一遍,才发现原型链这东西真没那么玄乎。

我的实现思路

先上代码,这是我一个字一个字敲出来的:

function isinstanceof(a, b) {
    // 第一反应:空值直接返回false
    if (a == null || b == null) {
        return false;
    }
    
    // 右边必须是个构造函数,不然还查什么?
    if (typeof(b) != 'function') {
        throw new TypeError("右边的参数不是构造函数")
    }
    
    // 左边必须是对象或函数,这是硬性要求
    if (typeof(a) != 'object' && typeof(a) != 'function') {
        throw new TypeError("左边的参数不是对象实例,或者函数")
    }
    
    // 核心逻辑:顺着原型链往上找
    while(a.__proto__ != null) {
        a = a.__proto__;  // 关键一步:往上爬一级
        
        if(a == b.prototype) {  // 找到了!
            return true;
        }
        
        if(a == null) {  // 爬到顶了还没找到
            return false;
        }
    }
    return false;
}

几个让我踩坑的地方

  1. 为什么要用 a.proto

一开始我很迷惑,__proto__到底是个啥?后来想明白了:

function Person() {}
const p = new Person();

// 这三者的关系要搞清楚
console.log(p.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true

__proto__就是对象的“爹”,每个对象都知道自己从哪儿来。

  1. 为什么要判断null?

这里有个坑:null没有原型链!所以:

// 这两个都会返回false
console.log(isinstanceof(null, Object)); // false
console.log(isinstanceof({}, null)); // false
  1. 为什么原始类型要报错?

这是我的设计选择。原生的instanceof对原始类型直接返回false:

console.log(1 instanceof Number); // false
console.log('hello' instanceof String); // false

但我觉得这样太隐晦了,不如直接告诉开发者:“老哥,你这参数传错了!”

实测一下

function foo() {}
const x = new foo();

console.log(isinstanceof(x, foo)); // true - 最基本的
console.log(isinstanceof(x, Object)); // true - 继承关系
console.log(isinstanceof({}, Object)); // true - 字面量对象

// 报错情况
console.log(isinstanceof(1, Number)); // 报错:左边的参数不是对象实例,或者函数
console.log(isinstanceof({}, 123)); // 报错:右边的参数不是构造函数

我学到的

  1. 原型链就是一条链:每个对象都有__proto__,指向创建它的构造函数的prototype。一直找到null为止。
  2. 构造函数是特殊的:只有函数才有prototype属性,普通对象没有。
  3. instanceof不是类型检查:它检查的是原型链,不是数据类型。所以数组也是对象,函数也是对象。

可以改进的地方

写完后看了下MDN,发现我的实现有几个问题:

  1. __proto__不是标准属性,最好用Object.getPrototypeOf()
  2. 应该先获取原型再判断是否为null
  3. 循环条件可以更简洁

改进版:

function isinstanceofV2(a, b) {
    if (typeof b !== 'function') {
        throw new TypeError('右边必须是个函数');
    }
    
    // 沿用原生行为:原始类型返回false
    if (a === null || typeof a !== 'object' && typeof a !== 'function') {
        return false;
    }
    
    let proto = Object.getPrototypeOf(a);
    while (proto !== null) {
        if (proto === b.prototype) {
            return true;
        }
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}

最后说两句

以前总觉得原型链很高深,自己动手写一遍发现也就那么回事。面试官再问instanceof原理,我就可以说:“这个我写过,核心就是顺着__proto__往上找……”

代码还是要自己写,光看是记不住的。