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的性能调优以及测试框架的搭建。

posted @ 2012-05-23 18:34  Miracle He  阅读(2129)  评论(4编辑  收藏  举报