从 proto 到 null:手写 instanceof 揪出对象的“祖宗十八代”🔍 👨‍👩‍👧‍👦

62 阅读5分钟

手写 instanceof:从原型链里揪出 "血缘关系"🤔

一、回顾原型、原型对象和原型链

在 JavaScript 的世界里,"原型" 这东西就像个神秘的族谱,藏着对象们的家族关系。要搞懂 instanceof,得先扒开这层神秘面纱~

1.什么是原型?

先看三个 "老熟人":prototype__proto__constructor

prototype是构造函数的 "专属名片"。比如function Animal() {},这个 Animal 构造函数身上就挂着prototype属性,指向一个原型对象,相当于给未来的 "子孙后代" 准备的 "基因库"🧬。

__proto__则是实例对象的 "寻根指针"。当我们用new创建实例时,比如const dog = new Dog(),dog 身上就有个__proto__,偷偷指向构造函数的prototype。就像刚出生的小狗,天生就知道自己的 "基因来源"~

constructor是原型对象上的 "认亲暗号"。每个原型对象都有constructor属性,指向它对应的构造函数。比如Dog.prototype.constructor就指向 Dog,相当于原型对象在说:"我是 Dog 家的基因库!"

2.核心关联规则 —— 原型链

这些小家伙们串起来,就形成了原型链。看下面代码:

javascript

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

数组 arr 的__proto__指向 Array 的原型,Array 原型的__proto__又指向 Object 的原型,Object 原型的__proto__是 null—— 这就是一条完整的原型链,像链条一样🔗一环扣一环,直到尽头。

二、instanceof 介绍

1.instanceof 是什么?

instanceof就像 JavaScript 里的 "亲子鉴定师",专门判断一个对象是不是某个构造函数的 "后代"(包括直接后代和间接后代)。

看下面的代码:

javascript

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

p 是 Person 的直接实例,同时也是 Animal 的间接实例(因为 Person 继承了 Animal),所以两次判断都是 true。

2.instanceof 有什么作用?

它的核心作用是检测对象的原型链:判断左边的对象(实例)的原型链上,是否存在右边构造函数的prototype。简单说就是:"你家族谱里有没有这个老祖宗?"

3.基于什么需求创造的 instanceof?

在大型项目里,多人协作时经常会遇到 "这对象到底是谁家的?" 的灵魂拷问。比如你拿到一个dog对象,想知道它能不能调用Animal的方法,这时instanceof就能告诉你:"它是 Animal 家的后代,能调用!" 就像团队协作时,先搞清楚每个人的 "所属部门",才好分配任务呀~

三、手写 instanceof

明白了原理,咱们也来当回 "亲子鉴定师",手写一个instanceof

1.手写代码展示

废话不多说,直接上代码:

javascript

function insInstanceOf(left, right) {
    // 拿实例的__proto__当起点
    let proto = left.__proto__;
    // 顺着原型链一路找
    while(proto) {
        // 找到构造函数的prototype就认亲成功
        if(proto === right.prototype) {
            return true;
        }
        // 没找到就继续往上爬
        proto = proto.__proto__;
    }
    // 爬到顶了还没找到,就是外人
    return false;
}

验证一下我们手搓的instanceof有没有bug:

javascript

function Animal() {}
function Cat() {}
Cat.prototype = new Animal();
function Dog() {}
Dog.prototype = new Animal();
const dog = new Dog();

console.log(insInstanceOf(dog,Dog)); // true(直接后代)
console.log(insInstanceOf(dog,Animal)); // true(间接后代)
console.log(insInstanceOf(dog,Object)); // true(所有对象都继承Object)
console.log(insInstanceOf(dog,Cat)); // false(猫和狗不是一家)

结果和原生instanceof一模一样!说明咱们的 "亲子鉴定师" 很称职~

2.手写代码的详细解释

  1. 起点设置let proto = left.__proto__—— 从实例的原型指针开始找,就像查族谱从 "本人" 开始翻。
  2. 循环遍历while(proto)—— 只要还没到原型链尽头(proto 不为 null),就继续找。
  3. 核心判断if(proto === right.prototype)—— 如果当前原型就是构造函数的原型,说明是 "自家人",返回 true。
  4. 继续向上proto = proto.__proto__—— 没找到就往上翻一代,相当于 "查爸爸的爸爸"。
  5. 终点处理:循环结束还没找到,返回 false——"查遍了族谱,确实没这号亲戚"。

这里有个小细节:为什么用let声明proto而不是const?因为proto需要不断更新(往上爬原型链),const声明的变量不能重新赋值呀~

四、面试官会问

  1. instanceof 和 typeof 有啥区别? typeof 是 "粗略分类",比如typeof []返回 "object",啥也分不清;而 instanceof 是 "精细认亲",能准确判断[] instanceof Array为 true。
  2. null 和 undefined 用 instanceof 会怎样? 会返回 false!因为它们没有__proto__,原型链都不存在,自然谈不上 "后代"~
  3. 手写 instanceof 时,为什么要比较 prototype 而不是构造函数本身? 因为实例的原型链上挂的是构造函数的prototype,而不是构造函数本身。比如dog.__proto__指向Dog.prototype,而不是 Dog 函数~
  4. 原型链的终点是什么? 是 null!就像族谱查到最早的老祖宗,再往上就没人啦(arr.__proto__.__proto__.__proto__返回 null)。

五、结语

搞定instanceof的关键,其实是吃透原型链这个 "家族族谱"。从prototype__proto__,从直接实例到间接继承,这些概念环环相扣~ 想要搞定这些知识点,可以看看我前面介绍原型、原型对象和原型链的文章:吃透 JS 原型:从构造函数到原型链,一篇讲透核心逻辑JS原型是其面向对象的灵魂,以“委托”而非传统血缘实现继承,以及讲解原型继承的文章:原型继承不翻车:一个空函数如何拯救了父子关系🐱‍💻从 call/apply 的“灵魂传送”到原型链的“共享陷阱”

下次再遇到 "这对象到底啥来头" 的问题,不妨心里默念:"让我顺着原型链康康~" 实在不行,就把咱们手写的insInstanceOf掏出来,保准能理清所有 "血缘关系"👨‍👩‍👧‍👦

最后友情提示:面试被问手写 instanceof 时,可别光顾着写代码,把原型链的原理讲清楚,才是加分项哦~ 😉