Being able to write maintainable object-oriented JavaScript code not only saves money, but also makes you popular. Do not believe? It's possible that you or someone else will come back and reuse your code one day. If you can make this experience as less painful as possible, you can save a lot of time. Everyone on earth knows that time is money. Likewise, you may earn someone's favor by saving them a headache. But before we start exploring how to write maintainable object-oriented JavaScript code, let’s take a quick look at what object-oriented is. If you already understand object-oriented concepts, you can skip the next section directly.
What is object-oriented?
Object-oriented programming mainly represents physical objects in the real world through code. To create an object, you first need to write a "class" to define it. Classes can represent almost anything: accounts, employees, navigation menus, cars, plants, ads, drinks, etc. And every time you want to create an object, instantiate an object from the class. In other words, an instance of a class is created as an object. In fact, objects are usually used when dealing with more than one thing of the same type. Plus, just simple functional programs can do a great job. Objects are essentially containers for data. Therefore, in an employee object, you may want to store the employee number, name, date of joining, title, salary, seniority, etc.
Objects also include functions (also called "methods") that process data. Methods are used as intermediaries to ensure data integrity and to transform data before storage. For example, a method could accept a date in any format and convert it into a standardized format before storing it. Finally, classes can also inherit from other classes. Inheritance allows you to reuse the same code in different classes. For example, both bank accounts and video store accounts can inherit a basic account class, which includes personal information, account opening date, branch information, etc. Then each can define its own data structures and methods for transaction or loan processing.
Warning: JavaScript object-oriented is not the same
In the previous section, the basics of classic object-oriented programming were outlined. I say classic because JavaScript doesn't follow these rules. In contrast, JavaScript classes are written as functions, and inheritance is implemented through prototypes. Prototypal inheritance basically means using prototype properties to implement inheritance of objects instead of inheriting classes from classes.
Instantiation of objects
The following is an example of object instantiation in 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());
Here, there are three important points to note:
1 The first part of the "class" function name letters should be capitalized. This indicates that the function is intended to be instantiated rather than called like a normal function.
2 The new operator is used during instantiation. If you omit new and just call the function, many problems will occur.
3 Because getFullName is assigned to this operator, it is publicly available, but fname and lname are not. The closure generated by the Employee function gives getFullName access to fname and lname, but remains private to other classes.
Prototypal inheritance
Here is an example of prototypal inheritance in 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());
This time, the Human class created contains all the common attributes of humans - I also put fname and lname in because It’s not just employees who have names, everyone has names. Then assign the Human object to its prototype property.
Code reuse through inheritance
In the previous example, the original Employee class was broken into two parts. All common human attributes were moved to the Human class, and then Employee inherited Human. In this case, the properties in Human can be used by other objects, such as Student, Client, Citizen, Visitor, etc. Now you may have noticed that this is a great way to split and reuse code. When dealing with Human objects, you only need to inherit Human to use the existing properties, instead of re-creating each different object one by one. In addition, if you want to add a "middle name" attribute, you only need to add it once, and those that inherit the Human class can use it immediately. On the contrary, if we just want to add the "middle name" attribute to an object, we can add it directly to that object without adding it to the Human class.
1. Public and Private
The next topic, I want to talk about public and private variables in classes. Depending on how the data is handled in the object, the data will be treated as private or public. Private properties do not necessarily mean that others cannot access them. Maybe only a certain method needs to be used.
Read only
Sometimes, you just want a value when you create an object. Once created, you don't want others to change this value. To do this, create a private variable and assign a value to it during instantiation.
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代码可以节省你,你的团队还有你公司的时间和金钱。这绝对能让你大受欢迎。