Javascript高级技术篇(2): 深入理解面向对象
我们常说Javascript是一种面向对象的语言,那也就是说具有面向对象的一些基本特性。比如包含对象、类、属性、方法以及构造函数等基本元素,很多人在想:JS类到底是什么玩意?其实很简单,就是一个function,正所谓"简单就是美"嘛。在自定义类的同时,我们也回顾一下JS基本的类:Math,Array,Object以及String等。
//定义JS类的两种方式(注意这里是大写开头) function EmailMessage() { } var EmailMessage = function() { }
有类就有对象存在,同时构造函数也应运而生。常常在构造函数中使用this.**来访问属于当前对象的属性或方法。对于由同一个类生成的多个对象之间是松耦合的,相互独立。
//当创建对象时会触发构造函数(这里无参) var EmailMessage = function() { alert("New message created."); } var myMessage = new EmailMessage(); // 输出 "New message created." var anotherMessage = new EmailMessage(); // 输出 "New message created."
当你想传递参数给对象时,就会使用带参构造函数。
//带参构造函数 var EmailMessage = function(message) { alert(message); } // 输出 "Return to sender" var myMessage = new EmailMessage("Return to sender");
刚才讲过,可以使用this来访问属性,其实在JS中还有一种方式来添加属性: 利用prototype。不仅能添加属性,还能添加方法。
//使用this来访问当前对象的属性和方法 var EmailMessage = function(subject) { this.subject = subject; this.send = function() { alert("Message '" + this.subject + "' sent!"); } } var myMessage = new EmailMessage("Check this out..."); var anotherMessage = new EmailMessage("Have you seen this before?"); //输出属性和方法 alert(myMessage.subject); // 输出 "Check this out..." alert(anotherMessage.subject); // 输出 "Have you seen this before?" myMessage.send();// 输出 "Message 'Check this out...' sent!" anotherMessage.send();// 输出 "Message 'Have you seen this before?' sent!"
利用prototype来添加属性和方法(主要适合于对已有类进行功能扩展)。
var EmailMessage = function(subject) { this.subject = subject; } //使用prototype来添加方法 EmailMessage.prototype.send = function() { alert("Message '" + this.subject + "' sent!"); } var myMessage = new EmailMessage("Check this out..."); var anotherMessage = new EmailMessage("Have you seen this before?"); //输出属性和方法 alert(myMessage.subject); // 输出 "Check this out..." alert(anotherMessage.subject); // 输出 "Have you seen this before?" myMessage.send();// 输出 "Message 'Check this out...' sent!" anotherMessage.send();// 输出 "Message 'Have you seen this before?' sent!"
通常有朋友在讲:要是想JS类只有一个对象存在时,即常说的单例模式如何实现呢?其实JS的内置类就包含了很多单例,如:Math等。在这里,我给出两种实现方式。
var User = function() { this.username = ""; this.password = ""; this.login = function() { return true; } } // 创建User类的对象并存储为相同的对象实例,而原始的类将被移除(可以看成起了个别名而已) User = new User(); // 使用单例对象来访问对应的方法(类似于C#的静态方法) User.login();
另外一种实现方式就是"自我初始化",即在类声明时就立即执行,而该类就只包含一个对象。
var Inbox = new function() { this.messageCount = 0; this.refresh = function() { return true; } }(); //声明就立即执行 Inbox.refresh();
接下来,我谈一下在已有类的基础上添加新的功能以实现扩展,我们称之为"继承"。注意以下代码的高亮片段。
var EmailMessage = function(subject) { this.subject = subject; this.send = function() { alert("Message '" + this.subject + "' sent!"); } } // 创建一个新的空类 var EventInvitation = function() {}; // 继承已有类EmailMessage的属性和方法 EventInvitation.prototype = new EmailMessage(); // EventInvitation将构造函数设置为自身 EventInvitation.prototype.constructor = EventInvitation; // 重置设置已有的属性subject EventInvitation.prototype.subject = "You are cordially invited to..."; // 创建EventInvitation的对象 var myEventInvitation = new EventInvitation(); // 输出 "Message 'You are cordially invited to...' sent!" myEventInvitation.send();
在继承已有类的同时,子类便获取了父类的所有属性和方法,自身只需要定义额外与自己相关的属性和方法。我们来解释一下封装与多态的概念:所有封装,即每个类只关注与自身相关的属性和方法。所谓多态,即子类在包含父类同名属性或方法时,而又不想继承来自父类的同名元素,则可以重新定义该元素(如subject)。
var EmailMessage = function(subject) { this.subject = subject; this.send = function() { alert("Email message sent!"); } } // 继承EmailMessage var EventInvitation = function() {}; EventInvitation.prototype = new EmailMessage("You are cordially invited to..."); EventInvitation.prototype.constructor = EventInvitation; // 重写send方法 EventInvitation.prototype.send = function() { alert("Event invitation sent!"); } var myEmailMessage = new EmailMessage("A new email coming your way."); var myEventInvitation = new EventInvitation(); myEmailMessage.send(); // 输出 "Email message sent!" myEventInvitation.send(); // 输出 "Event invitation sent!"
现在假设子类需要重写父类的同时,又需要调用父类的方法,我们来看怎么实现。
var EmailMessage = function(subject) { this.subject = subject; this.send = function() { alert("Email message sent!"); } } // 继承EmailMessage var EventInvitation = function() {}; EventInvitation.prototype = new EmailMessage("You are cordially invited to..."); EventInvitation.constructor.prototype = EventInvitation; // 重写send方法 EventInvitation.prototype.send = function() { alert("Event invitation sent!"); // 使用this.constructor.prototype来指向父类并执行同名方法 this.constructor.prototype.send.call(this); } var myEmailMessage = new EmailMessage("A new email coming your way."); var myEventInvitation = new EventInvitation(); myEmailMessage.send();// 输出 "Email message sent!" myEventInvitation.send();// 输出 "Event invitation sent!"、"Email message sent!"
从以上的讲解中,我们不难发现this的大量应用,可能很多朋友也知道this代表当前执行的对象实例。我这里详细解释一下this的普遍意义和用法。给一段很简单的代码,大家试着想一想结果是什么?主要是搞清楚此时this到底代表什么?
var showSubject = function() { alert(this.subject); } showSubject();// 输出 "undefined"
刚才提过,this代表当前执行的实例对象,可是现在并没有像之前的代码先定义一个类,然后定义对象,在使用this代表这个定义的对象。准确的解释应该是: this代表其所在的作用域内类自身或正在执行的对象,若this超出类的作用域则代表全局对象window对象。故此时应该输出undefined。接下来我们把这个function移植到已有类EmailMessage上。
var showSubject = function() { alert(this.subject); } showSubject();// 输出"undefined" this.subject = "Global subject";// 设置全局属性 showSubject();// 输出 "Global subject" // 定义EmailMessage类 var EmailMessage = function(subject) { this.subject = subject; } // 将showSubject添加到EmailMessage,注意这里showSubject不含() EmailMessage.prototype.showSubject = showSubject; var myEmailMessage = new EmailMessage("I am the subject."); myEmailMessage.showSubject();// 输出 "I am the subject.",因为现在this变成myEmailMessage showSubject();// 输出"Global subject",因为此时this仍然为window EmailMessage.prototype.outputSubject = function() { //现在为EmailMessage添加新方法outputSubject来调用showSubject showSubject(); } myEmailMessage.outputSubject();// 输出 "Global subject.",因为尽管添加了新方法,但this仍然是window
如果希望能强制切换当前引用的对象this,有两种方法: call、apply。两者的区别很小,前者传递的是参数列表,后者传递的是参数数组。
var showSubject = function() { alert(this.subject); } var setSubjectAndFrom = function(subject, from) { this.subject = subject; this.from = from; } //this代表全局对象window showSubject(); // 输出"undefined" setSubjectAndFrom("Global subject", "miracle@cnblogs.com"); showSubject(); // Outputs "Global subject" //定义EmailMessage类 var EmailMessage = function() { this.subject = ""; this.from = ""; }; var myEmailMessage = new EmailMessage(); //call或apply将this从全局对象window切换到myEmailMessage setSubjectAndFrom.call(myEmailMessage, "New subject", "miracle@sina.com"); setSubjectAndFrom.apply(myEmailMessage, [ "New subject", "miracle@sina.com" ]); showSubject.call(myEmailMessage);// 输出"New subject"
到此为止,我们所定义的属性和方法,在类外部都能访问。如果我们希望能将一些属性和方法仅供类内部使用,即所谓的private变量,我们改如何实现呢?
var EmailMessage = function(subject) { // 公有的属性和方法 this.subject = subject; this.send = function() { alert("Message sent!"); } // 私有的属性和方法(使用var而不是this) var messageHeaders = ""; var addEncryption = function() { return true; } // 特权的属性和方法(对外开放读取接口但不能修改,类似于只读) var messageSize = 1024; this.getMessageSize = function() { alert(messageSize); } }
接下来我们(在类外)开始使用这些变量,看看他们的表现如何?
var myEmailMessage = new EmailMessage("Save these dates..."); alert(myEmailMessage.subject); // 输出 "Save these dates..." myEmailMessage.send(); // 输出 "Message sent!" // 输出"undefined"因为messageHeaders是私有属性 alert(myEmailMessage.messageHeaders); // addEncryption()是私有方法,外部不能访问因此抛出异常 try { myEmailMessage.addEncryption(); } catch (e) { alert("Method does not exist publicly!"); } // 输出"undefined"因为messageSize是私有属性 alert(myEmailMessage.messageSize); // 输出"1024",特权属性可通过方法在外部访问 myEmailMessage.getMessageSize();
通过以上的学习,大家对面向对象的基础知识点已经有所了解了把。接下来,我在最后简单聊一下关于"对象字面量(Object Literal)"的知识。常听别人谈起这个概念,那对象字面量到底是什么呢?简单的说:就是将一系列属性和方法组合起来的集合体,可以用来创建单一对象(Singleton),创建类,设置函数输入参数等。下面我来一一讲解。首先,对象字面量是一个变量,然后将所有的属性和方法以"键值对"的方式全部包含在{}中,这跟后面的系列JSON数据组织格式很相似。
var earth = { name: "Terra Firma", // 字符串 planet: true, // 布尔变量 moons: 1, // 整数 diameter: 12756.36, // 小数 oceans: ["Atlantic", "Pacific", "Indian", "Arctic", "Antarctic"], // 数组 poles: { // 嵌套对象字面量 north: "Arctic", south: "Antarctic" }, setDiameter: function(diameter) { // 函数 this.diameter = diameter; // 此时this代表earth } } // 注意:此处不再声明 alert(earth.diameter); // 输出 "12756.36" earth.setDiameter(12756.37); alert(earth.diameter); // 输出 "12756.37"
同样对象字面量也能创建类(将对象字面量映射到类的prototype上)。
var EmailMessage = function() {}; EmailMessage.prototype = { subject: "", from: "", send: function() { alert("Message sent!"); } } var myEmailMessage = new EmailMessage(); myEmailMessage.subject = "Come over for a party.." myEmailMessage.send(); // 输出"Message sent!"
那对象字面量咋作为函数的输入参数呢?很简单,当我们有时传递的输入参数过多时,而这些参数之间又彼此关联时,我们不妨用对象字面量来作为输入参数。
// 使用多个输入参数 var sendEmail = function(to, from, subject, body) { alert("Message '" + subject + "' from '" + from + "' sent to '" + to + "'!"); } // 调用必须按照顺序 sendEmail("miracle@cnblogs.com", "miracle.he@cnblogs.com", "Dinner this week?", ? "Do you want to come over for dinner this week? Let me know."); // 用对象字面量来作为输入参数(将4个合成为1个) var sendEmail = function(message) { alert("Message '" + message.subject + "' from '" + message.from + "' sent to '" + message.to + "'!"); } // 此时调用不再区分顺序,只要将对象字面量的属性赋值即可 sendEmail({ from: 'miracle@cnblogs.com', to: 'miracle.he@cnblogs.com', subject: 'Dinner this week?', body: 'Do you want to come over for dinner this week? Let me know.' });
是不是觉得调用起来更加灵活和方便呢?再次回到上面的话题:变量作用域,其实这个在大家日常的实际项目经验中应该更加引以重视?我们说了window在全局作用域中均有效,但是我们的JS程序如果过多或不当使用全局变量,导致全局作用域内存在很多全局变量,将使应用程序的安全性面了巨大挑战,对于有些有心机的黑客来说,随意让别人获取全局变量并做恶意的修改,将导致应用程序的崩溃。那如何才能有效避免呢?我的建议:可以采用私有变量对我们的私密数据加以保护,其次还可以采用命名空间来建立模块层次以达到合理的保护。
// 这里MyCompany已经成为一个Singleton var MyCompany = new function(){ this.MyClient = { WebMail: function() { alert("Creating WebMail application..."); } }; }(); // 输出 "Creating WebMail application..." var myWebMail = new MyCompany.MyClient.WebMail();
到此为止,关于JS面向对象的知识点就介绍到这里,以后的系列还将讲解Javascript的性能调优以及测试框架的搭建。