JS 进阶:手写 instanceof 与JS继承全面讲解

前言:

在 JavaScript 的江湖里,"对象"这个词听起来很美好(毕竟我们都缺对象),但一旦涉及到 原型链继承,无数前端er就像陷入了一场大型"家庭伦理剧":

  • "我是谁?我从哪里来?"
  • "为什么改了儿子的属性,爹的属性也变了?"
  • "Object 和 Function 到底谁是谁的爸爸?"

今天,我们就把这些"家务事"断个明白。我们将先从手写 instanceof 入手,彻底搞懂血缘关系,再看看 JS 继承是如何从"刀耕火种"进化到"现代文明"的。


一、寻根问祖 ------ 手写 instanceof 🕵️‍♀️

在判断数据类型时,typeof 对于对象类型(Array, Object, null)往往力不从心,这时候就需要 instanceof 出场了。

1.1 核心原理

A instanceof B 的本质并不是判断 A 是否由 B 创建的,而是:

👉 B 的原型对象(prototype),是否出现在 A 的原型链上。

这就好比查家谱:只要在你的祖宗十八代里发现了 B 的名字,那你们就是一家人。

1.2 灵魂三问(基础复习)

在手写之前,必须理清这三个属性:

  1. prototype(显式原型) :函数的专属属性,相当于产品说明书
  2. proto(隐式原型) :对象(实例)的属性,相当于产地贴纸,指向构造函数的 prototype。
  3. constructor:原型对象上的属性,指回构造函数本身(说明书上写着"某某制造")。

1.3 手撕代码:myInstanceof

原理:顺着实例的 proto 链条一直往上找,直到找到目标,或者找到宇宙尽头(null)。

JavaScript 复制代码
/**
 * 模拟 instanceof 原理
 * @param {Object} left - 实例对象 (p)
 * @param {Function} right - 构造函数 (Person)
 */
function myInstanceof(left, right) {
  // 1. 获取右侧构造函数的显式原型(目标说明书)
  let prototype = right.prototype;
  
  // 2. 获取左侧对象的隐式原型(起始查寻点)
  // 推荐使用 Object.getPrototypeOf(left),兼容性更好,这里用 __proto__ 演示原理
  let proto = left.__proto__; 

  // 3. 开启寻根之旅
  while (true) {
    // 走到尽头了还没找到 -> 不是这一家的
    if (proto === null) return false;
    
    // 找到了!-> 确实是一家人
    if (proto === prototype) return true;
    
    // 没找到,继续去爷爷辈找
    proto = proto.__proto__;
  }
}

// --- 测试一下 ---
function Person() {}
const p = new Person();

console.log(myInstanceof(p, Person)); // true
console.log(myInstanceof(p, Object)); // true (所有人都是 Object 的后代)
console.log(myInstanceof(p, Array));  // false

二、 终极难点:Function 与 Object 的"鸡生蛋"问题 🥚

这是面试中的死亡考题,能把这段逻辑理顺,你的原型链功底就超越了 90% 的人。

请看这几行诡异的代码:

JavaScript 复制代码
console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true
console.log(Function instanceof Function); // true

解析:

  1. 万物皆对象:所有的对象,原型链的顶端都是 Object.prototype。

    • Object.prototype.__proto __ === null (宇宙尽头)。
  2. 函数也是对象:所有的函数(包括 Object 构造函数、Function 构造函数),都是 Function 的实例。

    • Object.__proto __ === Function.prototype (Object 是被 Function 生产出来的)。
    • Function.__proto __ === Function.prototype (Function 自己生了自己?)。

原型链中的关系(关键!)

JavaScript 的对象系统基于原型链(prototype chain) ,来看几个关键点:

1. 所有函数都有 prototype 属性
JavaScript 复制代码
function Foo() {}
console.log(Foo.prototype); // { constructor: Foo }
  • 这个 prototype 对象会被用作通过 new Foo() 创建的实例的原型
2. 所有对象都有 __proto__(或通过 Object.getPrototypeOf 访问)
JavaScript 复制代码
const obj = new Foo();
console.log(obj.__proto__ === Foo.prototype); // true
3. 函数的原型链
JavaScript 复制代码
function Foo() {}

// Foo 是一个函数
console.log(Foo.__proto__ === Function.prototype); // true

// Function.prototype 本身是一个函数对象
console.log(Function.prototype.__proto__ === Object.prototype); // true

// Object.prototype 是原型链的顶端(再往上是 null)
console.log(Object.prototype.__proto__); // null

所以整个链条是:

JavaScript 复制代码
Foo (函数)
  → __proto__ → Function.prototype
    → __proto__ → Object.prototype
      → __proto__ → null

Foo.prototype(注意不是 Foo.__proto__)是将来实例的原型:

JavaScript 复制代码
new Foo()
  → __proto__ → Foo.prototype
    → __proto__ → Object.prototype
      → __proto__ → null

总结:三者关系图
实体 类型 是对象? 是函数? 由谁构造?
普通对象 {} object Object
普通函数 function f(){} function Function
Object function Function
Function function Function(自身)

🎯 关键结论

  • 一切皆对象(除了原始类型:string, number, boolean, symbol, bigint, null, undefined)。
  • 函数是可调用的对象
  • 构造函数只是函数的一种使用方式
  • ObjectFunction 互相依赖,构成 JS 对象系统的基石

第二章:JS 中的6种继承方式 ------ 从青铜到王者

搞懂了原型链,我们就可以开始谈"继承"了。在 ES6 class 出现之前,JS 继承就是一部"踩坑史"。

目标:创建一个子类 Cat,继承父类 Animal 的属性(name, color)和方法(eat)。

准备工作:父类

JavaScript 复制代码
function Animal() {
    this.species = "动物";
    this.hobbies = ["睡觉", "干饭"]; // 引用类型,这也是个坑
}
Animal.prototype.eat = function() {
    console.log("阿姆阿姆~");
}

1.构造函数继承(只借身,不借魂)

这是最直接的思路:"把爸爸的代码拿来在我身体里跑一遍"。

JavaScript 复制代码
function Cat(name, color) {
    // 核心:修改 this 指向,执行父类代码
    Animal.call(this); 
    this.name = name;
    this.color = color;
}

const cat = new Cat("小黑", "黑");
console.log(cat.hobbies); // ["睡觉", "干饭"] -> 属性继承成功
// cat.eat(); // 报错!Uncaught TypeError -> 原型上的方法没继承到
  • 优点:简单,解决了引用属性共享问题(每个猫有自己的 hobbies),可传参。
  • 缺点只能继承属性,继承不了方法。Animal.prototype 上的东西它统统看不见。这叫"有其父之形,无其父之神"。

2.原型链继承(简单粗暴改族谱)

为了拿到方法,我们尝试直接修改子类的原型。

JavaScript 复制代码
function Cat(name) {
    this.name = name;
}
// 核心:把 Cat 的原型指向一个 Animal 的实例
Cat.prototype = new Animal(); 
Cat.prototype.constructor = Cat; // 修正身份证

const cat1 = new Cat("大黄");
const cat2 = new Cat("小白");

cat1.eat(); // "阿姆阿姆" -> 方法继承成功!

// 💥 灾难现场
cat1.hobbies.push("抓老鼠");
console.log(cat2.hobbies); // ["睡觉", "干饭", "抓老鼠"] -> 小白也被迫喜欢抓老鼠了
  • 缺点

    1. 引用篡改:所有子类实例共用同一个原型对象,修改一个实例的引用类型属性,其他实例全受影响。
    2. 无法传参:实例化子类时,没法给父类传参。

3.直接继承 Prototype(这也是个坑)

既然 new Animal() 浪费内存,那我直接指向 Animal.prototype 行不行?

JavaScript 复制代码
function Cat(name, color) {
  Animal.call(this); // 借用属性
  this.name = name;
}

// 核心代码:直接引喻
Cat.prototype = Animal.prototype; 
Cat.prototype.constructor = Cat;

Cat.prototype.sayHi = function () {
    console.log("喵喵");
};

// 灾难现场
console.log(Animal.prototype.constructor); // Cat !!!
  • 缺点 :这是毁灭性的。因为 Cat.prototype 和 Animal.prototype 指向了内存中同一个对象。你改了儿子的原型(加了 sayHi,改了 constructor),爹的原型也被改了。这叫"子债父偿"。

4.圣杯模式(The Holy Grail)------ 经典的手动优化

这是在 ES5 Object.create 普及之前,业界公认的最佳实践,也是道格拉斯·克罗克福德(Douglas Crockford)提出的经典方案。尤雨溪(Vue创始人)早年也在论坛推崇过这种写法。

它的核心思想是:既然 new Animal() 浪费内存且有副作用,那我就造一个空的构造函数 F 来当中间人!

JavaScript 复制代码
// 圣杯模式封装函数
function inherit(Child, Parent) {
    // 1. 创建一个干净的中间商(空函数)
    const F = function() {};
    
    // 2. 把中间商的原型指向父类原型
    F.prototype = Parent.prototype;
    
    // 3. 子类继承中间商的实例
    // 关键点:new F() 没有实例属性,非常轻量,完全避免了 new Animal() 的副作用
    Child.prototype = new F();
    
    // 4. 修正身份证
    Child.prototype.constructor = Child;
    
    // 5. (可选) 记录"生父",方便以后查找或调用父类方法
    // uber 是德语"super"的意思,这是当时的命名习惯
    Child.prototype.uber = Parent.prototype;
}

function Cat(name) {
    Animal.call(this); // 借属性
    this.name = name;
}

// 使用圣杯
inherit(Cat, Animal);

const cat = new Cat("贝塔");
cat.eat(); // 成功继承方法

🌟 为什么它叫"圣杯"?

因为它完美解决了"既要继承原型方法,又不要父类实例属性干扰"的难题,同时没有引入多余的内存开销。

5.寄生组合式继承(ES5 终极方案)🏆

这是 Vue/React 源码中(ES5转译版)真正使用的继承方式。它结合了上面两种方式的优点,并去掉了副作用。

核心思路:属性用 call 借用,原型用 Object.create 连接。

JavaScript 复制代码
function Cat(name, color) {
    // 1. 构造函数借用:继承属性,解决引用共享,可传参
    Animal.call(this); 
    this.name = name;
    this.color = color;
}

// 2. 原型继承:继承方法
// ❌ 别写 Cat.prototype = new Animal() -> 会多执行一次构造函数
// ✅ 用 Object.create 创建一个干净的空对象,其 __proto__ 指向 Animal.prototype
Cat.prototype = Object.create(Animal.prototype);

// 3. 修正 constructor 指向
Cat.prototype.constructor = Cat;

// 4. 添加子类独有方法
Cat.prototype.say = function() {
    console.log("喵喵喵~");
};

// 测试
const cat1 = new Cat("大橘", "橘色");
cat1.hobbies.push("跑酷");
const cat2 = new Cat("英短", "灰");

console.log(cat1.hobbies); // [..., "跑酷"]
console.log(cat2.hobbies); // [..., "干饭"] -> 互不干扰
cat1.eat(); // 方法可用

🌟 为什么这是最完美的 ES5 方案?

它避免了 new Animal() 的调用,因此不会在 Cat.prototype 上产生多余的、无用的父类属性,只保留了纯净的原型链关系。


6.ES6 Class extends(现代标准)

虽然写起来像 Java,但切记:JS 中没有类,class 只是原型的语法糖。底层逻辑依然是上面的"寄生组合式继承"。

JavaScript 复制代码
class Animal {
    constructor() {
        this.species = "动物";
    }
    eat() {
        console.log("eating...");
    }
}

class Cat extends Animal {
    constructor(name, color) {
        // 🚨 重点:子类 constructor 里必须先调用 super()
        // 否则无法使用 this,因为 ES6 的继承机制是先创造父类的实例对象
        super(); 
        
        this.name = name;
        this.color = color;
    }
    say() {
        console.log("miao~");
    }
}
🕵️‍♂️ 面试深度追问:ES6 和 ES5 继承的区别?

这是区分初级和中级开发者的关键点:

  1. this 的创建机制不同(最重要):

    • ES5:先创建子类的实例对象 this,然后再将父类的方法添加到 this 上(Animal.call(this))。
    • ES6:子类自己没有 this,必须先调用 super()(即父类构造函数),由父类构造出 this,再传给子类加工。
    • 这就是为什么不写 super() 会报错的原因。
  2. 静态方法的继承:

    • ES6 的 class 会把父类的静态方法(static)也继承下来,而普通的 ES5 原型链写法默认是不会继承构造函数本身的静态方法的。

📝 总结:面试突击小抄

  1. instanceof 原理:顺着 __proto __ 链往上找,看能不能找到构造函数的 prototype。

  2. 原型链口诀:实例的隐式原型等于构造函数的显式原型。

  3. 继承推荐

    • 手写 ES5:寄生组合式继承(call 借属性 + Object.create 借原型)。

    • 项目开发:ES6 class extends(语法糖,语义清晰)。

  4. 在 JavaScript 中,所有函数对象(包括 FunctionObjectArray 等内置构造函数)的内部原型([[Prototype]],可通过 __proto__ 访问)都指向 Function.prototype


相关推荐
小旭@1 小时前
vue3官方文档巩固
前端·javascript·vue.js
努力往上爬de蜗牛1 小时前
electron 打包
前端·javascript·electron
高桥留2 小时前
可编辑的span
前端·javascript·css
GISer_Jing2 小时前
React Native 2025:从零到精通实战指南
javascript·react native·react.js
三小河2 小时前
js Class中 静态属性和私有属性使用场景得的区别
前端·javascript
hjt_未来可期2 小时前
js实现复制、粘贴文字
前端·javascript·html
小帆聊前端2 小时前
JS this取值深度解读
前端·javascript
videring2 小时前
打字机效果-支持ckeditor5、框架无关
前端·javascript
tianxia2 小时前
主项目通过iframe嵌套子项目,子项目弹框无法全屏
前端·javascript