你不知道的JavaScript(六)|this和对象原型
委托理论
Task = {
setID: function (ID) { this.id = ID; },
outputID: function () { console.log(this.id); }
};
// 让XYZ 委托Task
XYZ = Object.create(Task);
XYZ.prepareTask = function (ID, Label) {
this.setID(ID);
this.label = Label;
};
XYZ.outputTaskDetails = function () {
this.outputID();
console.log(this.label);
};
// ABC = Object.create( Task );
// ABC ... = ...
在这段代码中,Task和XYZ并不是类(或者函数),它们是对象。XYZ通过Object.create(..)创建,它的[[Prototype]]委托了Task对象。相比于面向类(或者说面向对象),这里会把这种编码风格称为“对象关联”。我们真正关心的只是XYZ对象(和ABC对象)委托了Task对象。
对象关联风格的代码还有一些不同之处。
1、在上面的代码中,id和label数据成员都是直接存储在XYZ上(而不是Task)。通常来说,在[[Prototype]]委托中组好把状态保存在委托者(XYZ、ABC)而不是委托目标(Task)上。
2、在类设计模式中,我们故意让父类(Task)和子类(XYZ)中都有outputTask方法,这样就可以利用重写(多态)的优势。在委托行为中则恰好相反:我们会尽量避免在[[Prototype]]链的不同级别中适用男相同的命名,否则就需要使用笨拙并且脆弱的语法来消除引用歧义。
3、this.setID(ID);XYZ中的方法首先会寻找XYZ自身是否有setID(..),但是XYZ中并没有这个方法名,因此会通过[[Prototype]]委托关联到Task继续寻找,这时就可以找到setID(..)方法。此外,由于调用位置触发了this的隐式绑定规则,因此虽然setID(..)方法在Task中,运行时this仍然会绑定到XYZ,这正是我们想要的。在之后的代码中我们还会看到this.outputID(),原理相同。
换句话说,我们和XYZ进行交互时可以使用Task中的通用方法,因为XYZ委托了Task。
委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一个对象(Task)。
在API接口的设计中,委托最好在内部实现,不要直接暴露出去。在之前的例子中我们并没有开发者通过API直接调用XYZ.setID()。相反,我们把委托隐藏在了API的内部,XYZ.prepareTask(..)会委托Task.setID(..)。
比较思维模式
典型的(“原型”)面向对象风格:
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function () {
return "I am " + this.me;
};
function Bar(who) {
Foo.call(this, who);
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.speak = function () {
alert("Hello, " + this.identify() + ".");
};
var b1 = new Bar("b1");
var b2 = new Bar("b2");
b1.speak();
b2.speak();
以上代码,子类Bar继承了父类Foo,然后生成了b1和b2两个实例。b1委托了Bar.prototype,Bar.prototype委托了Foo.prototype。
使用对象关联风格来编写功能完全相同的代码:
Foo = {
init: function (who) {
this.me = who;
},
identify: function () {
return "I am " + this.me;
}
};
Bar = Object.create(Foo);
Bar.speak = function () {
alert("Hello, " + this.identify() + ".");
};
var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");
b1.speak();
b2.speak();
这段代码中我们同样利用[[Prototype]]把b1委托给Bar并把Bar委托给Foo,和上一段代码一模一样。我们仍然实现了三个对象之间的关联。
但是非常重要的一点是,这段代码简洁了许多,我们只是把对象关联起来,并不需要那些既复杂又令人困惑的模仿类的行为(构造函数、原型以及new)。
控件“类”
// 父类
function Widget(width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
Widget.prototype.render = function ($where) {
if (this.$elem) {
this.$elem.css({
width: this.width + "px",
height: this.height + "px"
}).appendTo($where);
}
};
// 子类
function Button(width, height, label) {
// 调用“super”构造函数
Widget.call(this, width, height);
this.label = label || "Default";
this.$elem = $("<button>").text(this.label);
}
// 让Button“继承”Widget
Button.prototype = Object.create(Widget.prototype);
// 重写render(..)
Button.prototype.render = function ($where) {
// “super”调用
Widget.prototype.render.call(this, $where);
this.$elem.click(this.onClick.bind(this));
};
Button.prototype.onClick = function (evt) {
console.log("Button '" + this.label + "' clicked!");
};
$(document).ready(function () {
var $body = $(document.body);
var btn1 = new Button(125, 30, "Hello");
var btn2 = new Button(150, 40, "World");
btn1.render($body);
btn2.render($body);
});
ES6的class语法糖
class Widget {
constructor(width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where) {
if (this.$elem) {
this.$elem.css({
width: this.width + "px",
height: this.height + "px"
}).appendTo($where);
}
}
}
class Button extends Widget {
constructor(width, height, label) {
super(width, height);
this.label = label || "Default";
this.$elem = $("<button>").text(this.label);
}
render($where) {
super($where);
this.$elem.click(this.onClick.bind(this));
}
onClick(evt) {
console.log("Button '" + this.label + "' clicked!");
}
}
$(document).ready(function () {
var $body = $(document.body);
var btn1 = new Button(125, 30, "Hello");
var btn2 = new Button(150, 40, "World");
btn1.render($body);
btn2.render($body);
});
毫无疑问,使用ES6的class之后,上一段代码中许多丑陋的语法都不见了,super(..)函数棒极了。
尽管语法上得到了改进,但实际上这里并没有真正的类,class仍然是通过[[Prototype]]机制实现的。
委托控件对象
var Widget = {
init: function (width, height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
},
insert: function ($where) {
if (this.$elem) {
this.$elem.css({
width: this.width + "px",
height: this.height + "px"
}).appendTo($where);
}
}
};
var Button = Object.create(Widget);
Button.setup = function (width, height, label) {
// 委托调用
this.init(width, height);
this.label = label || "Default";
this.$elem = $("<button>").text(this.label);
};
Button.build = function ($where) {
// 委托调用
this.insert($where);
this.$elem.click(this.onClick.bind(this));
};
Button.onClick = function (evt) {
console.log("Button '" + this.label + "' clicked!");
};
$(document).ready(function () {
var $body = $(document.body);
var btn1 = Object.create(Button);
btn1.setup(125, 30, "Hello");
var btn2 = Object.create(Button);
btn2.setup(150, 40, "World");
btn1.build($body);
btn2.build($body);
});
使用对象关联风格来编写代码时不需要把Widget和Button当做父类和子类。相反,Widget只是一个对象,包含一组通用的函数,任何类型的控件都可以委托,Button同样只是一个对。
从设计模式的角度来说,我们并没有像类一样在两个对象中都定义相同的方法名render(..),相反,我们定义了两个更具描述性的方法名(insert(..)和build(..))。同理,初始化方法分别叫做init(..)和setup(..)。