保守可能なオブジェクト指向 JavaScript コードを作成できると、コストが節約されるだけでなく、人気も高まります。信じないで?いつかあなたまたは他の誰かが戻ってきて、あなたのコードを再利用する可能性があります。この経験をできるだけ苦痛から減らすことができれば、時間を大幅に節約できます。地球上の誰もが時は金なりであることを知っています。同様に、誰かの悩みを解決することで、その人の好意を得ることができるかもしれません。ただし、保守可能なオブジェクト指向 JavaScript コードの記述方法を検討する前に、オブジェクト指向とは何かを簡単に見てみましょう。オブジェクト指向の概念をすでに理解している場合は、次のセクションを直接スキップできます。
オブジェクト指向とは何ですか?
オブジェクト指向プログラミングは、主にコードを通じて現実世界の物理オブジェクトを表します。オブジェクトを作成するには、まずそれを定義する「クラス」を作成する必要があります。 クラスは、アカウント、従業員、ナビゲーション メニュー、車、植物、広告、飲み物など、ほぼあらゆるものを表すことができます。そして、オブジェクトを作成するたびに、クラスからオブジェクトをインスタンス化します。つまり、クラスのインスタンスがオブジェクトとして作成されます。実際、オブジェクトは通常、同じタイプの複数のものを扱うときに使用されます。さらに、単純な関数型プログラムだけでも素晴らしい仕事をすることができます。オブジェクトは本質的にはデータのコンテナです。したがって、従業員オブジェクトには、従業員番号、名前、入社日、役職、給与、年功などを保存することができます。
オブジェクトには、データを処理する関数 (「メソッド」とも呼ばれます) も含まれています。メソッドは、データの整合性を確保し、保存前にデータを変換するための仲介として使用されます。たとえば、メソッドは任意の形式の日付を受け入れ、それを保存する前に標準化された形式に変換できます。最後に、クラスは他のクラスから継承することもできます。継承により、同じコードを異なるクラスで再利用できます。たとえば、銀行口座とビデオ ストアのアカウントはどちらも、個人情報、口座開設日、支店情報などを含む基本的な口座クラスを継承できます。その後、それぞれがトランザクションまたはローン処理のための独自のデータ構造とメソッドを定義できます。
警告: JavaScript のオブジェクト指向は同じではありません
前のセクションでは、古典的なオブジェクト指向プログラミングの基本について概説しました。古典的と言ったのは、JavaScript がこれらのルールに従っていないからです。対照的に、JavaScript クラスは関数として記述され、継承はプロトタイプを通じて実装されます。プロトタイプの継承は基本的に、クラスからクラスを継承するのではなく、プロトタイプのプロパティを使用してオブジェクトの継承を実装することを意味します。
オブジェクトのインスタンス化
以下は、JavaScript でのオブジェクトのインスタンス化の例です:
// 定义Employee类 function Employee(num, fname, lname) { this.getFullName = function () { return fname + " " + lname; } }; // 实例化Employee对象 var john = new Employee("4815162342", "John", "Doe"); alert("The employee's full name is " + john.getFullName());
ここで、注意すべき重要な点が 3 つあります:
1 「クラス」の最初の部分" 関数名の文字は大文字にする必要があります。これは、関数が通常の関数のように呼び出されるのではなく、インスタンス化されることを目的としていることを示します。
2 new 演算子はインスタンス化中に使用されます。 new を省略して関数を呼び出すだけだと、多くの問題が発生します。
3 getFullName はこの演算子に割り当てられているため、公開されていますが、fname と lname は公開されていません。 Employee 関数によって生成されたクロージャは、getFullName に fname および lname へのアクセスを与えますが、他のクラスに対してはプライベートのままです。
プロトタイプの継承
JavaScript でのプロトタイプの継承の例を次に示します:
// 定义Human类 function Human() { this.setName = function (fname, lname) { this.fname = fname; this.lname = lname; } this.getFullName = function () { return this.fname + " " + this.lname; } } // 定义Employee类 function Employee(num) { this.getNum = function () { return num; } }; //让Employee继承Human类 Employee.prototype = new Human(); // 实例化Employee对象 var john = new Employee("4815162342"); john.setName("John", "Doe"); alert(john.getFullName() + "'s employee number is " + john.getNum());
今回、作成された Human クラスには人間のすべての共通属性が含まれています - fname と lname も入れています。名前がある従業員だけでなく、全員が名前を持っています。次に、Human オブジェクトをそのプロトタイプ プロパティに割り当てます。
継承によるコードの再利用
前の例では、元の Employee クラスが 2 つの部分に分割されました。すべての共通の人間属性は Human クラスに移動され、その後 Employee が Human を継承しました。この場合、Human のプロパティは、Student、Client、Citizen、Visitor などの他のオブジェクトによって使用できます。これがコードを分割して再利用するための優れた方法であることに気づいたかもしれません。 Human オブジェクトを扱う場合、異なるオブジェクトを 1 つずつ再作成するのではなく、Human を継承して既存のプロパティを使用するだけで済みます。また、「ミドルネーム」属性を追加したい場合は一度追加するだけで、Humanクラスを継承しているものはすぐに利用することができます。逆に、「ミドルネーム」属性をオブジェクトに追加したいだけの場合は、Human クラスに追加せずに、そのオブジェクトに直接追加できます。
1. パブリック変数とプライベート変数
次のトピックでは、クラスのパブリック変数とプライベート変数について話したいと思います。オブジェクト内でのデータの処理方法に応じて、データはプライベートまたはパブリックとして扱われます。私有財産は、必ずしも他人がアクセスできないことを意味するものではありません。おそらく、特定の方法のみを使用する必要があります。
読み取り専用
オブジェクトを作成するときに値だけが必要な場合があります。作成したら、他の人にこの値を変更させたくないでしょう。これを行うには、プライベート変数を作成し、インスタンス化中にそれに値を割り当てます。
function Animal(type) { var data = []; data['type'] = type; this.getType = function () { return data['type']; } } var fluffy = new Animal('dog'); fluffy.getType(); // 返回 'dog'
在这个例子中,Animal类中创建了一个本地数组data。当 Animal对象被实例化时,传递了一个type的值并将该值放置在data数组中。因为它是私有的,所以该值无法被覆盖(Animal函数定义了它的范围)。一旦对象被实例化了,读取type值的唯一方式是调用getType方法。因为getType是在Animal中定义的,因此凭借Animal产生的闭包,getType可以进到data中。这样的话,虽可以读到对象的类型却无法改变。
有一点非常重要,就是当对象被继承时,“只读”技术就无法运用。在执行继承后,每个实例化的对象都会共享那些只读变量并覆盖其值。最简单的解决办法是将类中的只读变量转换成公共变量。但是你必须保持它们是私有的,你可以使用Philippe在评论中提到的技术。
Public(公有)
当然也有些时候你想要任意读写某个属性的值。要实现这一点,需要使用this操作符。
function Animal() { this.mood = ''; } var fluffy = new Animal(); fluffy.mood = 'happy'; fluffy.mood; // 返回 'happy'
这次Animal类公开了一个叫mood的属性,可以被随意读写。同样地,你还可以将函数指定给公有的属性,例如之前例子中的getType函数。只是要注意不要给getType赋值,不然的话你会毁了它的。
完全私有
最后,可能你发现你需要一个完全私有化的本地变量。这样的话,你可以使用与第一个例子中一样的模式而不需要创建公有方法。
function Animal() { var secret = "You'll never know!" } var fluffy = new Animal();
2. 写灵活的API
既然我们已经谈到类的创建,为了保持与产品需求变化同步,我们需要保持代码不过时。如果你已经做过某些项目或者是长期维护过某个产品,那么你就应该知道需求是变化的。这是一个不争的事实。如果你不是这么想的话,那么你的代码在还没有写之前就将注定荒废。可能你突然就需要将选项卡中的内容弄成动画形式,或是需要通过Ajax调用来获取数据。尽管准确预测未来是不大可能,但是却完全可以将代码写灵活以备将来不时之需。
Saner参数列表
在设计参数列表的时候可以让代码有前瞻性。参数列表是让别人实现你代码的主要接触点,如果没有设计好的话,是会很有问题的。你应该避免下面这样的参数列表:
function Person(employeeId, fname, lname, tel, fax, email, email2, dob) { };
这个类十分脆弱。如果在你发布代码后想要添加一个中间名参数,因为顺序问题,你不得不在列表的最后往上加。这让工作变得尴尬。如果你没有为每个参数赋值的话,将会十分困难。例如:
var ara = new Person(1234, "Ara", "Pehlivanian", "514-555-1234", null, null, null, "1976-05-17");
操作参数列表更整洁也更灵活的方式是使用这个模式:
function Person(employeeId, data) { };
有第一个参数因为这是必需的。剩下的就混在对象的里面,这样才可以灵活运用。
var ara = new Person(1234, { fname: "Ara", lname: "Pehlivanian", tel: "514-555-1234", dob: "1976-05-17" });
这个模式的漂亮之处在于它即方便阅读又高度灵活。注意到fax, email和email2完全被忽略了。不仅如此,对象是没有特定顺序的,因此哪里方便就在哪里添加一个中间名参数是非常容易的:
var ara = new Person(1234, { fname: "Ara", mname: "Chris", lname: "Pehlivanian", tel: "514-555-1234", dob: "1976-05-17" });
类里面的代码不重要,因为里面的值可以通过索引来访问:
function Person(employeeId, data) { this.fname = data['fname']; };
如果data['fname'] 返回一个值,那么他就被设定好了。否则的话,没被设定好,也没有什么损失。
让代码可嵌入
随着时间流逝,产品需求可能对你类的行为有更多的要求。而该行为却与你类的核心功能没有半毛钱关系。也有可能是类的唯一一种实现,好比在一个选项卡的面板获取另一个选项卡的外部数据时,将这个选项卡面板中的内容变灰。你可能想把这些功能放在类的里面,但是它们不属于那里。选项卡条的责任在于管理选项卡。动画和获取数据是完全不同的两码事,也必须与选项卡条的代码分开。唯一一个让你的选项卡条不过时而又将那些额外的功能排除在外的方法是,允许将行为嵌入到代码当中。换句话说,通过创建事件,让它们在你的代码中与关键时刻挂钩,例如onTabChange, afterTabChange, onShowPanel, afterShowPanel等等。那样的话,他们可以轻易地与你的onShowPanel事件挂钩,写一个将面板内容变灰的处理器,这样就皆大欢喜了。JavaScript库让你可以足够容易地做到这一点,但是你自己写也不那么难。下面是使用YUI 3的一个例子。
<script type="text/javascript" src="http://yui.yahooapis.com/combo?3.2.0/build/yui/yui-min.js"></script> <script type="text/javascript"> YUI().use('event', function (Y) { function TabStrip() { this.showPanel = function () { this.fire('onShowPanel'); // 展现面板的代码 this.fire('afterShowPanel'); }; }; // 让TabStrip有能力激发常用事件 Y.augment(TabStrip, Y.EventTarget); var ts = new TabStrip(); // 给TabStrip的这个实例创建常用时间处理器 ts.on('onShowPanel', function () { //在展示面板之前要做的事 }); ts.on('onShowPanel', function () { //在展示面板之前要做的其他事 }); ts.on('afterShowPanel', function () { //在展示面板之后要做的事 }); ts.showPanel(); }); </script>
这个例子有一个简单的 TabStrip 类,其中有个showPanel方法。这个方法激发两个事件,onShowPanel和afterShowPanel。这个能力是通过用Y.EventTarget扩大类来实现的。一旦做成,我们就实例化了一个TabStrip对象,并将一堆处理器都分配给它。这是用来处理实例的唯一行为而又能避免混乱当前类的常用代码。
总结
如果你打算重用代码,无论是在同一网页,同一网站还是跨项目操作,考虑一下在类里面将其打包和组织起来。面向对象JavaScript很自然地帮助实现更好的代码组织以及代码重用。除此以外,有点远见的你可以确保代码具有足够的灵活性,可以在你写完代码后持续使用很长时间。编写可重用的不过时JavaScript代码可以节省你,你的团队还有你公司的时间和金钱。这绝对能让你大受欢迎。