你不知道的 JS(上):原型与行为委托
本文是《你不知道的JavaScript(上卷)》的阅读笔记,第三部分:原型与行为委托。 供自己以后查漏补缺,也欢迎同道朋友交流学习。
原型
[[Prototype]]
JS 中的对象有一个特殊的 [[Prototype]] 内置属性,它是对其他对象的引用。几乎所有的对象在创建时都会被赋予一个非空的原型值。
当你试图引用对象的属性时会触发 [[Get]] 操作:
- 首先检查对象自身是否有该属性。
- 如果没有,则顺着
[[Prototype]]链向上查找。 - 这个过程会持续到找到匹配的属性名或到达原型链顶端(
Object.prototype)。如果还没找到,则返回undefined。
Object.prototype
所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。
属性设置和屏蔽
当执行 myObject.foo = "bar" 时:
- 如果
foo已存在于myObject中,直接修改它的值。 - 如果
foo不在myObject中而在原型链上层:- 若原型链上的
foo不是只读(writable:true),则在myObject上创建屏蔽属性foo。 - 若为只读(
writable:false),则无法设置。 - 若是一个 setter,则调用该 setter。
- 若原型链上的
- 如果
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(..)关联并委托行为。
委托模式的特点
- 更具描述性的方法名:避免使用通用的方法名,提倡使用能体现具体行为的名字。
- 状态存储在委托者上:数据通常存储在具体对象上,行为委托给基础对象。
类与对象关联的比较
对象关联风格的代码通常更简洁,因为它省去了模拟类所需要的复杂包装(构造函数、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 关键字,它解决了:
- 不再需要显式引用杂乱的
.prototype。 extends简化了继承。super支持相对多态。
class 陷阱
尽管 class 语法更好看,但它仍然是基于原型机制的,存在一些隐患:
- 非静态复制:修改父类方法会实时影响所有子类 and 实例。
- 成员属性限制:无法在类体中直接定义数据属性(只能定义方法),通常仍需操作原型。
- 意外屏蔽:属性名可能屏蔽同名方法。
- super 绑定 :
super是在声明时静态绑定的,而非动态绑定。
结论:静态大于动态吗?
ES6 的 class 试图伪装成一种静态的类声明,但这与 JS 动态的原型本质相冲突。它隐藏了许多底层机制,有时反而会让问题变得更难理解。
ES6 Class 小结
class 很好地伪装了类和继承模式,但它实际上只是原型委托的一层语法糖。使用时应警惕它带来的新问题。