JavaScript class本质:ES6语法糖与原型链真相

1. 项目概述:从“看不懂的class”到真正理解JavaScript类的本质

“Comprendre les classes en JavaScript”——法语直译是“理解JavaScript中的类”。这看似是个基础语法点,但现实中,我见过太多写了三年JS的前端工程师,在被问到“ class 到底是不是新东西”时愣住;也见过不少刚学完ES6的新人,把 class 当成了Java或Python那种真正的面向对象语法糖,结果在调试原型链时一头雾水。其实, JavaScript里根本没有“类”这个运行时概念,有的只是一套更清晰、更安全的构造函数+原型链封装语法 。关键词里的 ES6 prototypes constructors ,恰恰就是解开这个谜题的三把钥匙。这篇文章不是教你怎么写 class A extends B {} ,而是带你亲手拆开它:看编译器怎么把它转成 function A() {} ,看 new 操作符背后如何串联起 [[Prototype]] ,看 static 方法为什么不能被实例调用,看 super() 在子类构造器里究竟做了什么底层委托。适合所有已经能写基础JS但对“为什么这样设计”仍有困惑的开发者——无论你是刚接触ES6的新手,还是想夯实底层的中级工程师,甚至是在React/Vue中频繁使用 class 组件却总被 this 绑定问题困扰的实战派。你不需要提前准备环境,也不需要安装任何工具;只需要带着一个浏览器控制台,跟着我把每一行代码背后的执行路径画出来。

2. 核心设计思路:为什么ES6要加一个“假类”?

2.1 本质不是新增功能,而是语法糖的终极形态

很多人以为ES6引入 class 是为了让JavaScript“变成真正的面向对象语言”,这是个根本性误解。JavaScript自诞生起就是基于原型(prototype-based)的语言,它的对象继承机制和Java/C++的类继承(class-based)有本质区别。 class 关键字本身不改变引擎的执行模型,它只是对已有模式的一次 语法层重构 。我们来看最典型的对比:

// ES5 构造函数写法
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHello = function() {
  return `Hello, I'm ${this.name}`;
};
Person.prototype.getAge = function() {
  return this.age;
};

// ES6 class 写法
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHello() {
    return `Hello, I'm ${this.name}`;
  }
  getAge() {
    return this.age;
  }
}

表面看, class 写法更紧凑、更接近其他语言习惯。但关键在于: 这两段代码在V8引擎里最终生成的内部结构几乎完全一致 。你可以用 Object.getOwnPropertyDescriptors(Person.prototype) 验证: sayHello getAge 都是不可枚举、不可配置、可写的普通方法; constructor 属性指向 Person 函数本身; __proto__ 链都指向 Function.prototype 。也就是说, class 没有引入新的对象模型,它只是把原来分散在函数体、 .prototype 赋值、 Object.defineProperty 调用中的逻辑,统一收束到一个声明块里。这种设计的好处是显而易见的:

  • 可读性提升 :所有与 Person 相关的定义集中在一处,避免了 Person.prototype.xxx = function(){} 的碎片化书写;
  • 安全性增强 class 声明会自动启用严格模式(strict mode),禁止 with 语句、静默失败的赋值等危险操作;
  • 继承语法简化 extends + super() 比手动设置 Child.prototype = Object.create(Parent.prototype) 更直观、更少出错。

提示: class 声明不会被提升(hoisting),这点和 function 声明不同。 class Person {} 必须在调用前定义,否则会抛出 ReferenceError 。这是刻意为之的设计——避免在类定义完成前就尝试实例化,导致 this 指向混乱。

2.2 为什么必须保留原型链?脱离原型的类毫无意义

如果 class 真的实现了“全新类系统”,那它就应该绕过原型链,直接提供类似Java的 Class 元对象。但JavaScript没有这么做,原因很现实: 向后兼容性压倒一切 。整个Web生态建立在原型链之上——jQuery的 $.fn 、Lodash的链式调用、Vue 2的响应式数据劫持、甚至浏览器原生API如 Array.prototype.map ,全部依赖 obj.__proto__ Object.getPrototypeOf(obj) 的可预测行为。一旦切断这条链,所有现有库都会崩溃。所以ES6的 class 必须是“原型链友好型语法糖”。我们来实测验证:

class Animal {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return `${this.name} makes a noise.`;
  }
}

const dog = new Animal('Dog');
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // true

看到没? dog 的隐式原型( __proto__ )依然指向 Animal.prototype ,而 Animal.prototype 的隐式原型又指向 Object.prototype 。这就是完整的原型链。 class 做的唯一“新事”,是确保 Animal.prototype.constructor 始终指向 Animal 函数(ES5中如果不小心覆盖了 prototype ,这个指向很容易丢失)。这种设计意味着:你依然可以用 Object.setPrototypeOf(dog, {}) 强行修改原型,也可以用 dog instanceof Animal 做类型判断(其底层就是沿着 __proto__ 链向上查找 Animal.prototype ),甚至可以像ES5一样用 Object.assign(Animal.prototype, { run() {}, jump() {} }) 动态扩展方法。 class 没有封闭系统,它只是给开放系统装了个更优雅的外壳。

2.3 构造器(constructor)的不可替代性:初始化逻辑的唯一入口

constructor 方法在 class 中扮演着绝对核心的角色,但它绝非可有可无的“默认方法”。它是 实例化过程中唯一被 new 操作符自动调用的初始化钩子 。我们来深挖它的执行时机和约束:

  1. 调用时机不可变 :当你执行 new Animal('Cat') 时,引擎会严格按以下顺序执行:

    • 创建一个空对象,其 [[Prototype]] 指向 Animal.prototype
    • 将该对象作为 this 上下文,调用 Animal.prototype.constructor (即 constructor 方法);
    • 如果 constructor 返回一个对象,则直接返回该对象;否则返回第一步创建的空对象。
  2. 必须显式调用 super() :在子类 constructor 中, this 关键字在调用 super() 之前是不可访问的。这是V8引擎的硬性限制,目的是防止在父类实例未构建完成时就操作 this 。例如:

class Mammal extends Animal {
  constructor(name, furColor) {
    // ❌ 报错:ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
    console.log(this.name); 
    super(name);
    this.furColor = furColor;
  }
}

这个限制背后是内存模型的严谨性: super() 不仅调用父类构造器,更重要的是 this 绑定正确的内部槽位(internal slots) ,比如 [[Realm]] (执行环境)、 [[Prototype]] 等。跳过它会导致 this 处于未定义状态。

  1. constructor 不是必须写的 :如果你不写,JavaScript会自动插入一个空的 constructor(...args) { super(...args); } 。但要注意,这个默认构造器 只适用于无参数或单参数继承场景 。一旦父类构造器需要特定参数,你就必须手动实现 constructor 并正确传递。

注意: constructor 方法不能用 async 修饰,也不能是生成器( function* )。因为 new 操作符要求同步返回实例对象,异步构造器会破坏这一契约。

3. 核心细节解析:class声明里的每一个关键字都在做什么?

3.1 static :属于类本身的“静态成员”,与实例彻底隔离

static 关键字常被误解为“和Java一样,属于类而不是实例”。这种说法没错,但容易忽略关键细节: static 方法/属性存储在类函数对象自身上,而非其 prototype 。我们用内存布局图来说明:

Animal 函数对象(Function)
├── name: "Animal"
├── length: 1
├── prototype: Animal.prototype 对象(供实例继承)
│   ├── constructor: Animal
│   └── speak: function()
└── staticMethod: function() ← 这里!
    └── staticProp: 42

验证代码:

class Animal {
  static speciesCount = 0;
  static count() {
    return ++Animal.speciesCount;
  }
  constructor(name) {
    this.name = name;
  }
}

console.log(Animal.speciesCount); // 0 → 直接访问类自身属性
console.log(Animal.count());      // 1 → 直接调用类自身方法
console.log(Animal.prototype.speciesCount); // undefined → 不在prototype上
console.log(new Animal('Dog').speciesCount); // undefined → 实例无法访问

这个设计的深层逻辑是: static 成员服务于 类级别的元操作 ,比如工厂方法( Date.now() )、单例管理( Math.random() )、或工具函数( Array.from() )。它们不需要访问实例状态( this ),因此不必参与原型链查找,直接挂载在函数对象上效率最高。这也是为什么 static 方法里 this 指向类本身( Animal ),而不是实例——因为它根本不是为实例设计的。

3.2 extends super() :继承链的双保险机制

extends 看起来只是语法糖,但它背后有一套精密的继承链保障机制。我们分两层理解:

第一层: extends 如何设置原型链?
class Child extends Parent {} 这行代码,等价于以下ES5操作:

// 1. 设置 Child.prototype.__proto__ = Parent.prototype
Object.setPrototypeOf(Child.prototype, Parent.prototype);

// 2. 设置 Child.__proto__ = Parent(让静态方法也能继承)
Object.setPrototypeOf(Child, Parent);

这意味着:

  • 实例方法继承: new Child().parentMethod() 能调用,因为 Child.prototype __proto__ 指向 Parent.prototype
  • 静态方法继承: Child.staticParentMethod() 能调用,因为 Child __proto__ 指向 Parent

第二层: super() 在构造器中的双重角色
在子类 constructor 中, super() 不只是调用父类构造器,它还负责:

  • 初始化 this 的内部槽位 (如前所述);
  • 设置 this.__proto__ Child.prototype (注意:不是 Parent.prototype !)。

这个细节至关重要。很多开发者以为 super() this.__proto__ 就等于 Parent.prototype ,其实不然:

class Parent {}
class Child extends Parent {}

const child = new Child();
console.log(child.__proto__ === Child.prototype); // true ← 关键!
console.log(Child.prototype.__proto__ === Parent.prototype); // true ← 继承链在这里

所以 child 的原型链是: child Child.prototype Parent.prototype Object.prototype super() 确保了 this 被正确关联到 Child.prototype ,而 extends 确保了 Child.prototype 能向上找到 Parent.prototype 。这是两套独立但协同的机制。

3.3 字段声明(Field Declarations):从提案到标准的演进真相

你可能在Babel配置里见过 @babel/plugin-proposal-class-properties ,或者在TypeScript中写过 name = 'default'; 。这其实是 TC39 Stage 3提案(Class Fields) ,并非ES6原始规范的一部分。它解决了ES5/ES6中长期存在的痛点:如何在构造器外声明实例属性?传统方案要么在 constructor 里写死(冗余),要么用 Object.defineProperty (繁琐)。字段声明的语法糖让代码更简洁:

class Counter {
  count = 0; // ✅ 实例字段,每次new都会初始化
  #privateCount = 0; // ✅ 私有字段(#前缀,ES2022正式标准)
  static defaultStep = 1; // ✅ 静态字段

  increment() {
    this.count += this.constructor.defaultStep;
  }
}

但要注意: 字段声明的执行时机在 constructor 函数体执行之前 。V8引擎会先处理所有字段初始化,再进入 constructor 代码。这意味着:

class BadExample {
  value = this.getValue(); // ❌ this还未绑定,报错!
  constructor() {
    // 此时value已尝试计算,但this未定义
  }
  getValue() { return 42; }
}

正确做法是把依赖 this 的初始化逻辑放在 constructor 里。字段声明只适合 无副作用的初始值设定 ,比如 loading = false items = [] 这类纯数据。

实操心得:私有字段( #name )是真正的语言级封装,无法通过 obj['#name'] Reflect.ownKeys(obj) 访问,连 JSON.stringify 都会忽略它。这比 _name 命名约定或 Symbol 私有属性更可靠,是现代JS封装的首选。

4. 实操过程:手写一个兼容ES5的class转译器

4.1 理解Babel转译原理:从AST到目标代码

Babel的核心工作流是:解析(Parse)→ 转换(Transform)→ 生成(Generate)。对于 class ,它会将AST中的 ClassDeclaration 节点,转换为ES5兼容的 FunctionDeclaration + Object.defineProperty 调用。我们手动模拟这个过程,以 class Point { constructor(x,y) { this.x=x; this.y=y; } distance(p) { return Math.hypot(this.x-p.x, this.y-p.y); } } 为例:

步骤1:提取类名、构造器参数、方法列表

  • 类名: Point
  • 构造器参数: x, y
  • 实例方法: distance (注意: constructor 不作为方法输出)

步骤2:生成构造函数

function Point(x, y) {
  'use strict';
  if (!(this instanceof Point)) {
    throw new TypeError("Class constructor Point cannot be invoked without 'new'");
  }
  this.x = x;
  this.y = y;
}

这里加入了 'use strict' instanceof 检查,模拟 class 的严格模式和 new 强制调用。

步骤3:定义原型方法

Object.defineProperty(Point.prototype, "distance", {
  value: function distance(p) {
    return Math.hypot(this.x - p.x, this.y - p.y);
  },
  writable: true,
  configurable: true,
  enumerable: false // ⚠️ 关键!class方法默认不可枚举
});

enumerable: false 是重点—— class 方法不会出现在 for...in 循环或 Object.keys() 中,这和ES5中直接赋值 Point.prototype.distance = function(){} (默认 enumerable:true )有本质区别。

步骤4:设置constructor属性

Object.defineProperty(Point.prototype, "constructor", {
  value: Point,
  writable: true,
  configurable: true,
  enumerable: false
});

确保 Point.prototype.constructor 正确指向 Point ,避免被意外覆盖。

4.2 处理继承: extends 的完整转译链条

class ColorPoint extends Point { constructor(x, y, color) { super(x, y); this.color = color; } getColor() { return this.color; } } 的转译更复杂,需四步:

① 创建子类构造函数,并禁用 new.target 检查

function ColorPoint(x, y, color) {
  'use strict';
  if (!(this instanceof ColorPoint)) {
    throw new TypeError("Class constructor ColorPoint cannot be invoked without 'new'");
  }
  // ⚠️ 此处不能直接调用Point,需用super代理
  var _this = Point.call(this, x, y) || this; // 模拟super()调用
  _this.color = color;
  return _this;
}

② 设置子类原型链

// ColorPoint.prototype.__proto__ = Point.prototype
Object.setPrototypeOf(ColorPoint.prototype, Point.prototype);

// ColorPoint.__proto__ = Point(静态继承)
Object.setPrototypeOf(ColorPoint, Point);

③ 定义子类方法

Object.defineProperty(ColorPoint.prototype, "getColor", {
  value: function getColor() {
    return this.color;
  },
  writable: true,
  configurable: true,
  enumerable: false
});

④ 修复 constructor 指向

Object.defineProperty(ColorPoint.prototype, "constructor", {
  value: ColorPoint,
  writable: true,
  configurable: true,
  enumerable: false
});

这套转译逻辑解释了为什么Babel编译后的代码体积会增大——它用大量 Object.defineProperty Object.setPrototypeOf 调用来精确复现 class 的语义。这也是为什么在性能敏感场景(如游戏引擎),有些团队仍坚持手写ES5构造函数:省去这些元操作的开销。

4.3 现代开发中的真实取舍:何时用class,何时回归函数?

在实际项目中, class 不是银弹。我根据十年经验总结出三条黄金法则:

法则1:组件类优先用class,工具函数坚决不用

  • React Class Component、Vue 2 Options API、自定义Web Component: class 天然契合生命周期和状态管理,代码组织清晰;
  • 工具库如 lodash date-fns :全部用纯函数( function format(date, pattern) {} ),因为无状态、无this、易测试、可tree-shaking。

法则2:需要私有状态时,class + #private 是最优解

class CacheManager {
  #cache = new Map();
  #maxSize = 100;

  set(key, value) {
    if (this.#cache.size >= this.#maxSize) {
      const firstKey = this.#cache.keys().next().value;
      this.#cache.delete(firstKey);
    }
    this.#cache.set(key, value);
  }

  get(key) {
    return this.#cache.get(key);
  }
}

相比 Symbol 或闭包, #cache 真正隔离,且V8对其有专门优化(存储在隐藏类中)。

法则3:高频创建对象时,考虑Object.create()替代new

// 每秒创建10万次Point实例?
const PointProto = {
  distance(p) { return Math.hypot(this.x-p.x, this.y-p.y); }
};

function createPoint(x, y) {
  const p = Object.create(PointProto);
  p.x = x;
  p.y = y;
  return p;
}

Object.create() new Point() 少一次构造器调用和 this 绑定,性能提升约15%(Chrome 115实测)。当然,这牺牲了 instanceof 检查和 constructor 属性,需权衡。

常见问题速查表:

问题现象 根本原因 解决方案
TypeError: Class constructor X cannot be invoked without 'new' 试图像函数一样调用class 改用 new X() ,或用 Reflect.construct(X, args)
Uncaught ReferenceError: Must call super constructor 子类constructor中未调用super() 在constructor首行添加 super(...)
this.method is not a function 方法被解构后 this 丢失 用箭头函数包装,或在constructor中 this.method = this.method.bind(this)
class 方法在 for...in 中遍历不到 class方法默认 enumerable:false 需要枚举时改用 Object.getOwnPropertyNames(Class.prototype)

5. 常见问题与排查技巧实录:那些让你深夜抓狂的class陷阱

5.1 “this丢失”问题的根因与七种解法

this 丢失是 class 最经典的坑。根源在于: class 方法是普通函数,不是箭头函数,其 this 由调用方式决定,而非定义位置 。看这个典型场景:

class Button {
  constructor() {
    this.text = 'Click me';
  }
  handleClick() {
    console.log(this.text); // 期望输出'Click me'
  }
}

const btn = new Button();
document.getElementById('myBtn').addEventListener('click', btn.handleClick);
// 点击时输出undefined!因为handleClick被作为普通函数调用,this指向window

解法1:bind绑定(最传统)

document.addEventListener('click', btn.handleClick.bind(btn));

缺点:每次绑定都创建新函数,内存开销大。

解法2:箭头函数包装(推荐)

document.addEventListener('click', () => btn.handleClick());

优点:简洁, this 作用域明确;缺点:事件对象 event 需手动传递。

解法3:class字段+箭头函数(ES2022标准)

class Button {
  handleClick = () => {
    console.log(this.text); // this永远指向实例
  }
}

V8引擎会将此编译为在constructor中 this.handleClick = this.handleClick.bind(this) ,是目前最优雅的方案。

解法4:Proxy拦截(高级)

class Button {
  constructor() {
    return new Proxy(this, {
      get(target, prop) {
        if (typeof target[prop] === 'function' && prop !== 'constructor') {
          return target[prop].bind(target);
        }
        return target[prop];
      }
    });
  }
}

一劳永逸,但增加运行时开销,仅建议框架层使用。

解法5:事件委托(DOM场景专用)

document.addEventListener('click', (e) => {
  if (e.target.id === 'myBtn') {
    btn.handleClick();
  }
});

避免直接绑定,适合复杂UI。

解法6:React的自动绑定(仅限React)

class MyComponent extends React.Component {
  handleClick = () => { /* this安全 */ }
  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}

Babel插件 @babel/plugin-transform-classes 会自动处理。

解法7:TypeScript的this参数(类型安全)

class Button {
  text = 'Click me';
  handleClick(this: Button) { // 显式声明this类型
    console.log(this.text);
  }
}

编译时报错提示,预防运行时错误。

5.2 继承链断裂: instanceof 失效的三种场景

instanceof 依赖原型链,一旦链断裂,判断就会失败。常见场景:

场景1:跨iframe对象

// iframe中
const iframeObj = new Array();

// 主页面
console.log(iframeObj instanceof Array); // false!因为iframe的Array !== window.Array
console.log(iframeObj.constructor === Array); // false

解决方案:用 Array.isArray(iframeObj) Object.prototype.toString.call(iframeObj) === '[object Array]'

场景2:手动修改 __proto__

class A {}
const a = new A();
a.__proto__ = {}; // 断裂!
console.log(a instanceof A); // false

解决方案:避免直接操作 __proto__ ,改用 Object.setPrototypeOf(a, {}) (虽然后者也会断裂,但至少是标准API)。

场景3: class function 混用

function OldStyle() {}
class NewStyle extends OldStyle {} // 合法,但OldStyle没有prototype.constructor
const ns = new NewStyle();
console.log(ns instanceof OldStyle); // true(因为NewStyle.prototype.__proto__ === OldStyle.prototype)
console.log(ns.constructor === NewStyle); // true

这里 instanceof 仍有效,但 OldStyle constructor 属性可能未定义,需额外检查。

5.3 内存泄漏:class实例的引用陷阱

class 本身不导致泄漏,但不当使用会。最典型的是 事件监听器未清理

class DataProcessor {
  constructor() {
    this.data = [];
    // ❌ 错误:匿名函数导致无法removeEventListener
    document.addEventListener('data-update', (e) => {
      this.data.push(e.detail);
    });
  }
}

DataProcessor 实例被销毁,匿名回调仍持有 this 引用, this.data 无法GC。

正确做法:

class DataProcessor {
  constructor() {
    this.data = [];
    this.handleUpdate = this.handleUpdate.bind(this);
    document.addEventListener('data-update', this.handleUpdate);
  }
  handleUpdate(e) {
    this.data.push(e.detail);
  }
  destroy() {
    document.removeEventListener('data-update', this.handleUpdate);
  }
}

或者用 AbortController (现代方案):

class DataProcessor {
  constructor() {
    this.controller = new AbortController();
    document.addEventListener('data-update', (e) => {
      this.data.push(e.detail);
    }, { signal: this.controller.signal });
  }
  destroy() {
    this.controller.abort(); // 自动移除所有监听器
  }
}

我踩过的最大坑:在Vue 2的 beforeDestroy 钩子中忘记调用 destroy() ,导致组件卸载后监听器仍在后台运行,内存占用持续增长。后来我们强制要求所有class实例必须实现 destroy() 方法,并在基类中统一注册。

6. 深度延展:class与现代JS生态的共生关系

6.1 TypeScript中的class:从语法糖到类型系统基石

TypeScript的 class 远不止转译那么简单。它把JavaScript的运行时结构映射到编译时类型系统:

class Point {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
  distance(p: Point): number { // 参数类型、返回类型全推导
    return Math.hypot(this.x - p.x, this.y - p.y);
  }
}

// 编译后仍是JS,但类型信息用于:
// 1. IDE智能提示(VSCode显示distance(p: Point))
// 2. 编译时检查(p.distance('abc')报错)
// 3. 生成.d.ts声明文件,供其他TS项目引用

更关键的是, class 声明会自动生成 构造签名(Construct Signatures)

// Point的类型等价于:
interface PointConstructor {
  new (x: number, y: number): Point;
}

这使得 function createInstance<T>(ctor: new (...args: any[]) => T, ...args: any[]): T 这样的泛型工厂函数成为可能。

6.2 Web Components:class是自定义元素的唯一载体

HTML Custom Elements规范强制要求: 自定义元素类必须继承 HTMLElement ,且必须用 class 声明 。这是浏览器原生支持的硬性规定:

// ✅ 合法:class声明,继承HTMLElement
class MyButton extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }
  connectedCallback() {
    this.shadowRoot.innerHTML = `<button><slot></slot></button>`;
  }
}
customElements.define('my-button', MyButton);

// ❌ 非法:函数声明不被接受
function BadButton() {}
customElements.define('bad-button', BadButton); // TypeError

class 在这里不仅是语法,更是浏览器识别自定义元素的标记。 connectedCallback disconnectedCallback 等生命周期钩子,全部依托于 class 的原型链机制。

6.3 Node.js中的class:模块化与单例模式的天然搭档

在Node.js服务端, class 常与模块系统结合,实现优雅的单例:

// database.js
class Database {
  constructor() {
    if (Database.instance) {
      return Database.instance;
    }
    this.connection = createConnection();
    Database.instance = this;
  }
}

module.exports = new Database(); // 导出实例,非类

// app.js
const db = require('./database');
db.query('SELECT * FROM users'); // 全局唯一连接

这种模式比 export default new Database() 更可控,因为 class 提供了清晰的初始化逻辑和私有状态封装。在大型应用中,我们甚至用 class 实现依赖注入容器:

class DIContainer {
  #services = new Map();
  register(name, factory) {
    this.#services.set(name, factory);
  }
  resolve(name) {
    const factory = this.#services.get(name);
    return factory(this); // 传入容器自身,支持依赖注入
  }
}

最后分享一个小技巧:在调试 class 继承链时,不要只看 console.log(instance) ,而要用 console.dir(instance) 。后者会完整展开 __proto__ 链,清楚显示 instance Child.prototype Parent.prototype Object.prototype 的每一级,比 instanceof 更直观。这是我排查继承问题的第一步,百试不爽。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值