为什么Javascript的面向对象与基于原型存在争议?

JavaScript是基于原型的,且天然支持面向对象编程;class只是原型机制的语法糖,所有继承和方法查找仍依赖[[Prototype]]链,this绑定取决于调用位置而非定义位置。

这个问题本身存在预设偏差:JavaScript 没有“面向对象与基于原型的争议”,它**就是基于原型的,且这种机制天然支持面向对象编程**。所谓“争议”,其实是初学者或从类语言(如 Java、C++)转来的开发者,对 class 语法糖和底层原型链关系理解不清晰导致的认知冲突。 下面直接说清楚几个关键点:

class 不是新范式,只是 prototype 的语法糖

ES6 引入的 class 关键字没有改变 JavaScript 的运行时模型——所有继承、方法查找、this 绑定依然依赖 [[Prototype]] 链。写一个 class Person,本质仍是操作 Person.prototype

常见误解:以为 class 声明后就生成了“类对象”,其实它只是函数:

class Person {}
console.log(typeof Person); // "function"
console.log(Person.prototype.constructor === Person); // true
  • class 内部方法默认不可枚举,但仍在 prototype
  • extends 实际调用的是 Object.setPrototypeOf(Sub.prototype, Super.prototype)
  • super() 在构造函数中,本质是调用父级 [[Construct]] 并绑定 this

new 与 Object.create() 的行为差异容易被忽略

两者都创建对象并设置原型,但初始化逻辑不同:

  • new Foo():执行 Foo 构造函数,this 指向新对象,返回该对象(除非显式返回非空对象)
  • Object.create(Foo.prototype):只设置 [[Prototype]],不执行任何初始化逻辑

错误做法:用 Object.create(Foo.prototype) 替代 new Foo() 后,忘记手动调用 Foo.call(newObj, ...),导致实例缺少属性。

原型污染(Prototype Pollution)是真实风险,不是理论争议

因为所有对象共享原型链,恶意输入若被用于遍历赋值(如深拷贝、merge 工具),可能污染 Object.prototype

const obj = {};
obj.__proto__.polluted = "yes";
console.log({}.polluted); // "yes"
  • 常见触发点:lodash.mergeJSON.parse 后未校验 key、for...in 遍历时未用 hasOwnProperty
  • Node.js 中已多次出现因原型污染导致的 RCE(如 node-serialize
  • 修复方式:禁用 __proto__ / constructor 键;使用 Object.assign({}, input) 替代浅合并到目标对象

this 绑定混乱的本质是函数复用 + 原型链查找

方法从原型上被调用时,this 取决于调用位置,而非定义位置:

const obj = {
  name: "a",
  say() { console.log(this.name); }
};
const fn = obj.say;
fn(); // undefined —— this 指向 global / undefined(严格模式)
obj.say(); // "a"
  • 这不是 bug,是原型机制的自然结果:函数是独立值,obj.say 只是取值表达式
  • 解决方式不是“改成 class 就好了”,而是明确绑定:bind、箭头函数、或调用时用 call/apply
  • Class 字段中的箭头函数(method = () => {})能避免问题,是因为它把函数绑定到了实例,绕过了原型查找
真正需要警惕的,不是“该不该用 class”,而是是否意识到:你写的每一行 classnewsuper,背后都在读写 prototype[[Prototype]]。混淆语法表象和运行时本质,才是多数问题的源头。