JavaScript的行为驱动开发:第二部分

发布于:2021-02-12 00:00:44

0

70

0

JavaScript 驱动开发 TDD

本文是由两部分组成的系列文章中的第二篇。为了快速起步,您可以去我的博客里找到第一部分。 

JavaScript是面向对象的

JS是一种面向对象的语言。这不同于面向类。如果您具有C ++或Java之类的工业语言背景,您可能会认为JS缺少某些东西。我也这么想了很久。

实际上,诸如C ++或Java之类的流行语言都没有强大的OOP概念实现,它们是从强大的Smalltalk继承而来的。OOP的发展甚至没有停止Smalltalk。有许多语言使Smalltalk中的概念更进一步。这些概念仅在学术界广泛使用。

一种改进是摆脱类。诸如Cecil,Self或最近的IO之类的语言都尝试了一种原型方法,该方法也被引入JS。

您可能仍然认为,与诸如Self之类的语言相比,JS没有最佳的原型实现方式和丑陋的语法。但是,原型继承概念本身是一个很大的改进。 

旧的方式

创建对象的旧方法是提供构造函数。这是针对Java和C ++开发人员的,旨在为他们提供他们习惯的编码样式。但是构造函数也有缺点。因此ECMAScript5引入了一种更好的创建对象的方法。

在ECMAScript5介入之前,它看起来如何?假设您已经建立了一家商店,现在您需要开始在线销售各种产品。在过去,您可能会这样写:

var Product = function(name) { this.name = name; }; Product.prototype.showName = function() {alert(this.name);}; var myProduct = new Product("Super Fancy TV"); myProduct.showName();<span> </span>

构造函数(javascript_refresher / product_in_ecma3.js)

实际上,这还不错。您有一个构造函数来创建对象并将方法附加到原型。所有单个产品都有这些方法,但是有几个缺点需要考虑。 

  • JS语言中没有私有属性 。名称的值可以随时从外部更改。

  • 该 方法 被分散。 即使将它们全部放在一个地方,也没有单一的结构 来定义对象概念–就像许多其他 面向对象语言中的类一样。

  • 您 很容易 忘记 使用“ new”关键字。 这不会引发错误,只会导致完全不同的 行为,并且可能会导致一些很难发现的非常讨厌的错误。

  • 继承 会 带来更多问题。在 JS社区中,没有一种正确执行方法的共识方法。

由于这些问题,许多开发人员创建了提供所有类型的对象创建和实例化逻辑的库,框架和工具。他们中的许多人介绍了类(例如Prototype5或Coffeescript6)。

您可以根据需要选择此路径,但我不建议这样做。也许难以置信,但是原型继承实际上比基于类的继承容易得多,甚至还提供了其他好处。只需看看其他原型语言,例如IO或Self。正是旧的JS语法使原型难以使用。

道格拉斯·克罗克福德(Douglas Crockford)率先提出了一种更好的方法。他编写了一个简短的Object.create方法,该方法以稍微扩展的形式进入了ES5标准。 

对象创建

好消息是,您不需要具有ES5实现的现代浏览器。Mozilla开发人员网络(MDN)提供了一个polyfill。polyfill是一种使较新的浏览器可以使用现代功能的技术。您可以在Modernizr-Wiki上找到其他polyfills。

MDN的Object.create-polyfill允许您使用在旧版浏览器(例如IE8)中创建对象的新方法。当您认为IE7 / IE8的综合市场份额仍约为7%(2013年1月)时,这一点很重要。如果您仅针对IE9 +,Firefox,Chrome,Safari或Opera用户,这不是问题。如有疑问,请查看兼容性表。

if (!Object.create) { Object.create = function (o) { if (arguments.length > 1) { throw new Error('Object.create implementation only accepts the first parameter.'); } function F() {} F.prototype = o; return new F(); }; }<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 12px"> </span>

只需包含清单1.6中的polyfill,以确保您具有Object.create函数。片段的第一行检查Object.create是否已经存在。这样可以确保如果当前浏览器提供的方法,polyfill将不会覆盖本机实现。如果您需要用于其他ES5功能的附加填充料,则可以使用Kris Kowal的或David de Rosier的es5-shim。 

独奏对象

让我们再看看shop项目。如果您只想创建一种产品,则不需要Object.create。只需直接创建对象:

var myProduct = { price: 99.50, name: 'Grindle 3' };

但是,这不是最佳选择。您可以从外部轻松地操作对象的属性,但是没有办法检查或转换分配的值。看一下这个作业: 

MyProduct.price = -20;

像这样的错误将意味着您的公司快速销售许多产品,并拥有非常满意的客户-每次购买可赚取20美元的客户。您的公司将无法长期这样做!

多年的面向对象编程和设计经验教会我们将对象的内部状态与它的外部接口分开。您通常希望将属性设为私有,并提供一些getter和setter方法。

Getters and setters

ES5提供了一个使用getter和setter访问属性的绝佳概念。不幸的消息是,没有办法让它们在较旧的浏览器中运行–您不能仅使用polyfill。因此,在接下来的两年左右的时间里,我们大多数人都没有奢侈地使用它。同时,您可以使用多种方法(请参阅[Stefanov 2010])。它们都有各自独特的优点和缺点。没有单一的解决方案,因此归结为一个问题。这是我通常的工作:

带下划线的前缀属性,例如_price。这只是将属性标记为私有的约定。您永远不要从对象外部调用它们。通常容易发现违反此规则的情况。还有一些更复杂的方法,使用基于闭包的模式来归档实际隐私(例如,在[Stefanov 2010]中)。我的意见是,大多数时候他们不值得付出努力。

提供以“ set”开头的setter方法,例如setPrice(value)。在较旧的浏览器中,无法覆盖赋值运算符=。所以这是下一件好事。Java和C ++程序员已经习惯了。

提供具有原始属性名称的getter方法,例如price()。许多程序员更喜欢在方法的前面加上“ get”。我认为这在JS中不是必需的,只会使代码的可读性降低–您的工作量可能会有所不同。 有时,一个属性可能只供内部使用,或者您希望它仅准备就绪。在这种情况下,只需忽略适当的方法即可。因此,清单1.8显示了更好的实现。

var myProduct = { _price: 99.50, _name: 'Grindle 3', price: function() {return this._price;}, name: function() {return this._name;}, setPrice: function(p) {this._price = p;}, };<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 12px"> </span>

具有getter和setter的独奏对象(javascript_refresher / product_with_getters_and_setters.js)。 如果要在设置之前检查价格,现在可以轻松执行以下操作:

setPrice: function(p) { if (p <= 0) { throw new Error("Price must be positive"); } this._price = p; }<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 12px"> </span>

带有检查的setPrice方法(javascript_refresher / product_with_price_check.js)。

真正的ECMAScript5实现更好(清单1.10)。它具有隐式调用getter和setter的附加好处,例如myProduct.price = 85.99。最后,JS支持统一访问原则!但是,在本教程中,我们将坚持所提到的第一个解决方法,因为您不能向后移植此语言功能。

var myProduct = { ... get price() {return this._price;}, set price(p) { if (p <= 0) { throw new Error("Price must be positive"); } this._price = p; }, ...<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 12px"> </span>

样机

一个真正的商店将有很多产品,您不想从头开始创建所有产品。您将需要一个可以放置通用结构和行为的地方。JS中通常的模式是为此创建一个父对象-产品的原型,所有其他产品均来自该产品。使用Object.create可以从中构建新产品。

var Product = { _price: 0, _name: '', price: function() {return this._price;}, name: function() {return this._name;}, setPrice: function(p) {this._price = p;}, setName: function(n) {this._name = n;} }; var product1 = Object.create(Product); product1.setName('Grindle 3'); product1.setPrice(99.50); var product2 = Object.create(Product); product2.setName('yPhone 7'); product2.setPrice(599.99);

也有惯例以此类首字母开头这样的原型,例如更传统的语言中的类(即var Product而不是var product)。

初始化器

为了将新产品对象的所有属性设置为正确的值,您需要调用其所有setter方法-这是一个很大的麻烦。您应该改为构建一个初始化方法。可以将这种方法与其他语言中的构造方法进行比较。

var Product = { ... init: function(name, price) { this._name = name; this._price = price; return this; }, ... }; var aProduct = Object.create(Product).init('Grindle 3', 99.50);

更好的是,覆盖产品的create -Method来封装创建和初始化。

var Product = { ... create: function(name, price) { return Object.create(this).init(name, price); }, ... }; var aProduct = Product.create('Grindle 3', 99.50);

原型方法的一大优势是实例化和继承的统一。您不需要任何特殊的操作,也可以只使用Object.create进行继承。

var Product = { ... }; var Book = Object.create(Product); Book._author = null; Book._numPages = null; Book.setAuthor = function(author) {this._author = author;}; Book.setNumPages = function(num_pages) {this._numPages = num_pages;}; Book.author = function() {return this.author();}; Book.numPages = function() {return this.numPages();};

Book是从Product派生的新对象。它无需设置特定的值(如 名称和价格),而是通过getter和setter来获得其他结构和行为(作者页面和num Pages)。

缺点是 多次调用Book是多余的,整个语法与定义基础对象完全不同。因此,您通常会构建一个小的 扩展函数,使继承更加方便(清单1.15)。同样,您在真正的ES5中将不需要此功能。真正的Object.create允许包含扩展名的第二个参数。

提示:jQuery(http://api.jquery.com/jQuery.extend)和Underscore.js(http://underscorejs.org/#extend)中提供了Object.extend的替代实现 。如果您的项目中已经有这些库之一,则可以改用它们。

Object.prototype.extend = function(props) { for (var prop in props) { this[prop] = prop; } return this; };

现在,您可以使用新的extend方法重构代码。

var Product = { _price: 0, _name: '', price: function() {return this._price;}, name: function() {return this._name;}, setPrice: function(p) {this._price = p;}, setName: function(n) {this._name = n;} }; var Book = Object.create(Product).extend({ _author: null, _numPages: null, setAuthor: function(author) {this._author = author;}, setNumPages: function(num_pages) {this._numPages = num_pages;}, author: function() {return this.author();}, numPages: function() {return this.numPages();} }); console.log(Product); console.log(Book);

内部原型继承

与基于静态类的方法相比,对象和原型概念具有许多优点,例如:

  • 继承和实例的统一。您可以使用相同的机制(Object.create)从原型继承或从原型构建实例(类似于类用法)。实际上–这是同一回事。

  • 价值的继承。您可以从原型继承值;不需要在构造函数中设置默认值。

  • 原型的运行时修改。在JS中,运行时和编译时没有区别。您可以在程序执行期间修改原型,而更多的静态语言仅允许在编译器运行之前更改类。这不是原型继承的直接优势,而是JS的动态特性。甚至有基于类的语言可供使用,这些类允许对类进行运行时修改-Ruby或Smalltalk可以满足要求。但是JavaScript的仅对象方法使这一过程变得简单得多。如果您想进行任何元编程,这将是一大收获。

“仅对象”方法的最大优点是其简单性。让我们看一下内部工作原理:首先,您不需要对方法进行任何特殊处理。方法只是碰巧包含函数的对象属性。因此,查找简单数字或调用方法都没关系。现在看一下JS如何确定要调用的方法。清单1.17演示了该原理。

var Product = { init: function(name) { this._name = name; return this; }, _name: '', name: function() { return this._name; }, setName: function(n) { this._name = n; } }; var Book = Object.create(Product).extend({ init: function(name, author) { Product.init(name); this._author = author; return this; }, author: null, setAuthor: function(author) { this._author = author; }, author: function() { return this.author(); } }); var myBook = Object.create(Book).init('Lords of the Rings', 'J.R.R. Tolkien'); myBook.mostImportantHobbit = "Frodo";<span style="font-family: Verdana, Arial, Helvetica, sans-serif;font-size: 12px"> </span>

如果您尝试获取myBook.mostImortantHobbit的值 ,则JavaScript引擎仅查看myBook对象并返回该值。

查找myBook.name()需要更多步骤。JS引擎在myBook对象上找不到名称-属性 ,因此需要在其原型Book中进行查找 。它也不存在,因此它会跟踪原型链,直到找到它。该属性名称 实际上在Book的原型 产品中可用。因此,JS解释了括号并调用了name中包含的函数。该函数在myBook的上下文中执行。因此,this._name 指的是“指环王””。即使JS确实需要执行几个步骤,它们也很容易理解。始终遵循原型链。

实例化和继承不需要区别对待。

其他要考虑的事情

在实际的项目中,还需要考虑许多其他事项。您通常希望将代码保留在名称空间中。要管理名称空间和文件依赖性,您可能需要使用提供AMD(异步模块定义)的工具。 RequireJS 或 curl 是流行的。

您甚至可能希望使用更大的基础框架之一,例如 Ember, Backbone 或 AngularJS。在这里我不会深入研究这些东西,因为它们对于理解行为驱动的开发不是必需的。我敦促您真正研究构建代码库的更好方法。它可以带来很大的不同。

替代样式

JS是一种非常灵活的语言,支持各种编程范例(至少是功能和面向对象的)。这为软件设计和开发提供了许多不同的方法。您可能会喜欢纯函数式方法,或者对原型进行OOP,或者将库用于基于类的面向对象。您可能会考虑使用混合/特征或其他高级构造。也许您会考虑使用像Harmonizr这样的预处理器/编译器来编写ECMAScript6 / Harmony代码,甚至尝试使用CoffeeScript。就本教程而言,没关系。行为驱动方法应使用这些特征或混合中的任何一种起作用。

因此,在这里加点盐就可以成为我的JS风格;与实际项目相比,我将其简化了一些。可以将它带入行为驱动开发的美好世界。