[b]前言[/b]
这篇文章需要知道的一个重要观点,[[prototype]]机制就是指对象的一个内部链接引用另外一个对象。
如果在第一个对象上没有找到需要的属性或者方法的引用,引擎就会继续在[[prototype]]关联的对象上继续查找,同理,如果后者中也没有找到需要的引用就会继续查找他的[[prototype]].以此类推,这一系列对象的链接被称为“原型链”。
[b]对比[/b]
面向委托的设计,是不同于面向类的设计,我们要试着把思路从类和继承的设计模式转换到委托行为的设计模式。下面将分别贴出2种设计模式的示例代码,帮助你进行过渡。
下面是典型的(“原型”)面向对象风格
//父类 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() + "."); } //实例化Bar var b1 = new Bar("b1"); var b2 = new Bar("b2"); b1.speak();//"hello,I am: b1." b2.speak();//"hello,I am: b2."
子类Bar继承了父类Foo 然后生成了b1和b2两个实例,b1 委托了Bar.prototype 后者委托了Foo.prototype 这种风格很常见,你应该很熟悉了。
下面来看看如何使用对象关联风格来编写完全相同的代码。
1 Foo = { 2 init: function(who) { 3 this.me = who; 4 }, 5 identify: function() { 6 return "I am" + this.me; 7 } 8 }; 9 Bar = Object.create(Foo); 10 Bar.speak = function() { 11 console.log("Hello, I am: " + this.identify() + "."); 12 } 13 var b1 = Object.create(Bar); 14 b1.init("b1"); 15 var b2 = Object.create(Bar); 16 b2.init("b2"); 17 18 b1.speak(); 19 b2.speak();
这段代码中我们同样利用了[[prototype]]吧b1委托给了Bar并把Bar委托给了Foo,和上一段代码一模一样的。仍然实现了三个对象之间的关联,
但非常重要的一单是,这段代码简洁了许多,我们只是把对象关联起来,并不需要那些即复杂又令人困惑的模仿类的行为(构造函数,原型以及new)。
问问你自己,如果对象关联的代码能够实现类风格代码的所有功能并且更加简洁易懂,那她是不是比类风格更好?
下面来看看两段代码所对应的思维模型。
首先,类风格代码的思维模型强调以实体以及实体间的关系:
这张图有点不清晰甚至误导人,而且这是十分复杂的关系网。从关系图来看,还有类继承的一个坑,Bar.prototype继承Foo.prototype之后丢失了constructor。通常需要手动将Bar的constructor指回Bar自身。
现在让我们看看对象关联代码的思维模型
通过比较可以看出,对象关联风格的代码显然更加简洁,因为这种代码只关注一件事情:对象之间的关联联系。
经典的“类”技巧非常复杂而且令人困惑,去掉他们之后,事情变得简单许多。
[b]前端开发中真实场景的对比[/b]
我们已经看到“类”和“行为委托”在理论和思维模型方面的区别,现在看看真实场景如何应用这些方法
来看看web开发中非常经典的一种前端场景,创建UI控件。比如“Button”控件
如果你已经习惯了面向对象的设计模式,会用JavaScript实现类风格的代码
1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <title>Document</title> 6 <style type="text/css"> 7 8 </style> 9 </head> 10 <body> 11 <script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.js"></script> 12 <script type="text/javascript"> 13 //父类 14 function Widget(width, height) { 15 this.width = width || 50; 16 this.height = height || 50; 17 this.$elem = null; 18 } 19 20 Widget.prototype.render = function($where) { 21 if(this.$elem) { 22 this.$elem.css({ 23 width: this.width + "px", 24 height: this.height + "px" 25 }).appendTo($where); 26 } 27 } 28 29 //子类 30 function Button(width, height, label) { 31 //调用super 构造函数 32 Widget.call(this, width, height); 33 this.label = label || "Default"; 34 35 this.$elem = $("<button>").text(this.label); 36 } 37 38 //让Button 继承 Widget 39 Button.prototype = Object.create( Widget.prototype); 40 41 //重写 render() 42 Button.prototype.render = function($where) { 43 //"super" 调用 44 Widget.prototype.render.call(this, $where); 45 this.$elem.click( this.onClick.bind(this)); 46 } 47 48 Button.prototype.onClick = function(evt) { 49 console.log("Button: " + this.label + "被点击了"); 50 alert("Button: " + this.label + "被点击了"); 51 } 52 53 $(function() { 54 var $body = $(document.body); 55 var btn1 = new Button(123, 30, "hello"); 56 var btn2 = new Button(150, 40, "World"); 57 58 btn1.render($body); 59 btn2.render($body); 60 }) 61 </script> 62 </body> 63 </html>
在面向对象设计模式中,我们首先需要在父类中定义基础的render(..),然后在子类中重写他,子类并不会替换基础的render(..)。只是添加一些按钮特有的行为。比如点击按钮弹出对话框
恩,这里还出现了丑陋的显式伪多态,即通过widget.call和widget.prototype.render.call从子类方法中引用"父类”的基础方法。太丑陋这算是缺点吧。
下面的例子使用对象关联风格委托来更简单地实现
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Document</title> <style type="text/css"> </style> </head> <body> <script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.js"></script> <script type="text/javascript"> 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) { alert("按钮: " + this.label + "被点击了"); } $(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); }) </script> </body> </html>
使用对象关联风格来编写代码不需要吧Widget和Button当做父类和子类。相反,Widget只是一个对象,包含一组通用的函数,任何类型的控件都可以委托。
从语法的角度来说,我们同样没有任何构造函数,.prototype或new,实际上没有使用它们的必要。
[b]小结[/b]
在软件架构中你可以选择是否使用类和继承设计模式,大多数开发者理所当然地认为类是唯一的代码组织方式,但这里我们看到了另外一种更少见但更强大的设计模式:行为委托。
行为委托认为对象之间是兄弟关系,相互委托,而不是父类和子类的关系,JavaScriptd [[prototype]]机制本质上就是行为委托机制,也就是说,我们可以选择在JavaScript中努力实现“类”机制。也可以拥抱更加自然的[[prototype]]委托机制。
当你只用对象来设计代码时,不仅可以让语法更加简洁,也可以让代码结构更加清晰。
对象关联是一种编码风格。他倡导的是直接创建和关联对象,不拔他们抽象成类。对象关联可以用于基于[[prototype]]的行为委托非常自然的实现。
参考自 《你不知道的JavaScript》上卷 第六章