JS的“坑”源于原型委托模型与类式思维的错位:对象通过[[Prototype]]链单向委托查找属性,而非复制继承;class只是语法糖,底层仍是prototype机制;属性屏蔽、原型污染、this绑定断裂均由此引发。

原型继承和类语法的“坑”,根源不在写法,而在底层存储架构——每个对象内部都有一个不可见的 [[Prototype]] 链接,它决定属性怎么找、方法从哪来、this 指向谁。JS 不是靠“类定义”组织代码,而是靠对象间单向委托关系运行。所谓“坑”,大多是把类式思维硬套在原型委托模型上导致的错位。
原型链的存储本质:委托而非复制
JS 中没有真正的“继承”,只有“属性查找委托”。当你写 obj.method(),引擎做的不是“把父类方法拷一份给子类”,而是:
- 先查
obj自身有没有method - 没有?顺着
obj.[[Prototype]]找到它的原型对象(比如Person.prototype) - 再查那个原型上有没有
method - 还没找到?继续沿
[[Prototype]]往上,直到Object.prototype,最后到null
这个链条全程不复制任何东西,所有实例共享同一份原型上的方法。一旦你在原型上改了方法,所有实例立刻受影响——这不是 bug,是设计本意。但开发者常误以为“每个实例该有自己的一份”,于是手动在构造函数里重复定义方法,白白浪费内存。
类语法(class)只是语法糖,但容易掩盖原型真相
class 关键字看着像 Java,实际编译后仍是基于 prototype 和 __proto__ 的原型操作。比如:
class Animal { eat() { console.log('eating'); } }
class Dog extends Animal { bark() { console.log('barking'); } }
const d = new Dog();
背后实际发生的是:
-
Dog.prototype.__proto__指向Animal.prototype -
Animal.prototype.__proto__指向Object.prototype -
d.__proto__→Dog.prototype→Animal.prototype→Object.prototype→null
问题在于:class 隐藏了 prototype 和 __proto__ 的显式操作,让人误以为“类就是模板”,从而忽略:
-
static方法不进原型链,只挂在构造函数上 -
super()实际是调用Animal.prototype.constructor.call(this),若父类没定义构造函数或忘了写super(),this就未初始化,直接报错 - 箭头函数不能当构造函数,也没有
prototype属性,因此无法被new,也无法参与原型链构建
最隐蔽的三类坑:来自 [[Prototype]] 的隐式行为
这些不是写错语法,而是对底层委托机制理解偏差引发的问题:
-
属性屏蔽陷阱:如果实例自身添加了同名属性(如
d.eat = function(){}),它会“遮住”原型上的eat。后续调用d.eat()走的是实例自己的方法,但delete d.eat后又恢复走原型——这不像类继承里的“重写”,而是动态的查找覆盖 -
原型污染风险:修改
Object.prototype或Array.prototype(比如加个.flatAll),会影响所有对象。因为所有对象最终都连到那里,这是全局副作用,不是某个“类”的改动 -
this 绑定断裂:原型方法被单独提取使用时(如
const fn = d.bark; fn()),this不再指向d,而变成undefined(严格模式)或window。这不是继承出错,而是函数调用时上下文丢失——原型链管“找得到”,不管“调用时是谁”
绕不开的兼容与规范细节
不同访问方式指向同一内部链接,但语义和安全性不同:
-
obj.__proto__是历史遗留,非标准,仅用于调试;ES6 推荐用Object.getPrototypeOf(obj)读、Object.setPrototypeOf(obj, proto)写 -
Function.prototype是所有函数的原型,但函数本身也是对象,所以也有自己的[[Prototype]](指向Function.prototype),而Function.prototype的[[Prototype]]又指向Object.prototype - 构造函数的
prototype属性和实例的__proto__属性,两者指向同一对象,但一个是函数的“出厂设置”,一个是实例的“血脉链接”——混淆二者,就等于分不清“父母准备的知识库”和“孩子实际继承的那套知识”
不复杂但容易忽略

















