你不知道的 JS(上):原型与行为委托

你不知道的 JS(上):原型与行为委托

本文是《你不知道的JavaScript(上卷)》的阅读笔记,第三部分:原型与行为委托。 供自己以后查漏补缺,也欢迎同道朋友交流学习。

原型

[[Prototype]]

JS 中的对象有一个特殊的 [[Prototype]] 内置属性,它是对其他对象的引用。几乎所有的对象在创建时都会被赋予一个非空的原型值。

当你试图引用对象的属性时会触发 [[Get]] 操作:

  1. 首先检查对象自身是否有该属性。
  2. 如果没有,则顺着 [[Prototype]] 链向上查找。
  3. 这个过程会持续到找到匹配的属性名或到达原型链顶端(Object.prototype)。如果还没找到,则返回 undefined
Object.prototype

所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype

属性设置和屏蔽

当执行 myObject.foo = "bar" 时:

  1. 如果 foo 已存在于 myObject 中,直接修改它的值。
  2. 如果 foo 不在 myObject 中而在原型链上层:
    • 若原型链上的 foo 不是只读(writable:true),则在 myObject 上创建屏蔽属性 foo
    • 若为只读(writable:false),则无法设置。
    • 若是一个 setter,则调用该 setter。
  3. 如果 foo 既不在 myObject 也不在原型链上,直接添加到 myObject

"类"

JS 和面向类的语言不同,它并没有类作为蓝图,JS 中只有对象。

"类函数"与原型继承

JS 通过函数的 prototype 属性来模仿类。当你调用 new Foo() 时,创建的新对象会关联到 Foo.prototype

注意 :在 JS 中,我们并不是将"类"复制到"实例",而是将它们关联起来。

"构造函数"

Foo.prototype 默认有一个 .constructor 属性指向 Foo。 通过 new 调用的函数并不是真正的"构造函数",new 只是劫持了普通函数,并以构造对象的形式来调用它。

(原型)继承

常见的"继承"写法:

javascript 复制代码
function Foo(name) {
    this.name = name;
}
function Bar(name, label) {
    Foo.call( this, name );
    this.label = label;
}

// 创建一个新的 Bar.prototype 对象并关联到 Foo.prototype
Bar.prototype = Object.create( Foo.prototype );

Bar.prototype.myLabel = function() {
    return this.label;
};

Object.create(..) 会凭空创建一个新对象并将其 [[Prototype]] 关联到指定的对象。

检查"类"的关系:

  • a instanceof Foo:检查 Foo.prototype 是否出现在 a 的原型链上。
  • Foo.prototype.isPrototypeOf(a):更直观的检查方式。
  • Object.getPrototypeOf(a):获取对象的原型。

对象关联

原型链的本质就是对象之间的关联。Object.create(..) 是创建这种关联的直接方式,它避免了 new 构造函数调用带来的复杂性(如 .prototype.constructor 引用)。

关联关系是备用

比起直接在原型链上查找(直接委托),内部委托往往能让 API 设计更清晰:

javascript 复制代码
var anotherObject = {
    cool: function() { console.log( "cool!" ); }
};
var myObject = Object.create( anotherObject );
myObject.doCool = function() {
    this.cool(); // 内部委托!
};

原型机制小结

JS 的 [[Prototype]] 机制本质上是行为委托。对象之间不是复制关系,而是关联关系。

行为委托

面向委托的设计

类理论 vs. 委托理论
  • 类理论:鼓励继承、重写和多态。将行为抽象到父类,子类实例化时进行复制。
  • 委托理论 :认为对象之间是兄弟关系。定义基础对象,其他对象通过 Object.create(..) 关联并委托行为。
委托模式的特点
  1. 更具描述性的方法名:避免使用通用的方法名,提倡使用能体现具体行为的名字。
  2. 状态存储在委托者上:数据通常存储在具体对象上,行为委托给基础对象。

类与对象关联的比较

对象关联风格的代码通常更简洁,因为它省去了模拟类所需要的复杂包装(构造函数、prototype 等)。

更好的语法 (ES6)

ES6 的简洁方法语法让对象关联看起来更舒服:

javascript 复制代码
var AuthController = {
    errors: [],
    checkAuth() { /* .. */ }
};
Object.setPrototypeOf(AuthController, LoginController);

内省 (Introspection)

在对象关联模式下,检查对象关系变得非常简单:

javascript 复制代码
Foo.isPrototypeOf( Bar ); // true
Object.getPrototypeOf( Bar ) === Foo; // true

行为委托小结

行为委托是一种比类更强大的设计模式。它更符合 JS 的原型本质,能让代码结构更清晰、语法更简洁。

ES6 中的 Class

class 语法

ES6 引入了 class 关键字,它解决了:

  1. 不再需要显式引用杂乱的 .prototype
  2. extends 简化了继承。
  3. super 支持相对多态。

class 陷阱

尽管 class 语法更好看,但它仍然是基于原型机制的,存在一些隐患:

  1. 非静态复制:修改父类方法会实时影响所有子类 and 实例。
  2. 成员属性限制:无法在类体中直接定义数据属性(只能定义方法),通常仍需操作原型。
  3. 意外屏蔽:属性名可能屏蔽同名方法。
  4. super 绑定super 是在声明时静态绑定的,而非动态绑定。

结论:静态大于动态吗?

ES6 的 class 试图伪装成一种静态的类声明,但这与 JS 动态的原型本质相冲突。它隐藏了许多底层机制,有时反而会让问题变得更难理解。

ES6 Class 小结

class 很好地伪装了类和继承模式,但它实际上只是原型委托的一层语法糖。使用时应警惕它带来的新问题。

相关推荐
程序员爱钓鱼1 小时前
Go操作Excel实战详解:github.com/xuri/excelize/v2
前端·后端·go
子兮曰9 小时前
async/await高级模式:async迭代器、错误边界与并发控制
前端·javascript·github
恋猫de小郭10 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
GIS之路12 小时前
ArcGIS Pro 中的 Notebooks 入门
前端
IT_陈寒13 小时前
React状态管理终极对决:Redux vs Context API谁更胜一筹?
前端·人工智能·后端
Kagol14 小时前
TinyVue 支持 Skills 啦!现在你可以让 AI 使用 TinyVue 组件搭建项目
前端·agent·ai编程
柳杉14 小时前
从零打造 AI 全球趋势监测大屏
前端·javascript·aigc
simple_lau14 小时前
Cursor配置MasterGo MCP:一键读取设计稿生成高还原度前端代码
前端·javascript·vue.js
睡不着先生14 小时前
如何设计一个真正可扩展的表单生成器?
前端·javascript·vue.js
天蓝色的鱼鱼14 小时前
模块化与组件化:90%的前端开发者都没搞懂的本质区别
前端·架构·代码规范