JavaScript 面向对象编程与 .NET 对比
本文旨在通过与 .NET CLR 的工作方式进行对比,帮助大家理解 JavaScript 中的面向对象编程。
介绍
本文旨在通过与 .NET CLR 的工作方式进行对比,帮助大家理解 JavaScript 中的面向对象编程。我之前主要在 .Net 领域工作,习惯于直接定义 C# 类,但 JavaScript 却使用了函数构造器、原型(prototype)、原型的 constructor 属性、_proto_ 等概念。为了学习这些概念,我开始将 CLR 的工作方式与 JavaScript 的工作方式进行对比,在理解了其中的差异后,我便开始明白为什么在 JavaScript 中要以某种特定方式做事。我将假设您已经熟悉面向对象编程的基础知识。在 .NET 的示例中,我将使用 C# 代码。
引用类型
在 C# 中,如果要创建一个引用类型,您会使用类。假设我们创建一个 Person 类,如下所示:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string GetFullName()
{
return FirstName + " " + LastName;
}
}
然后,您会像这样实例化一个对象:
var p = new Person { FirstName = "Jon", LastName = "Smith" };
Console.WriteLine(p.GetFullName());
假设您的程序刚刚开始运行,这是 Person 对象首次被使用。究竟发生了什么?对于 CLR 中的任何引用类型,实际上会创建两个对象:类型对象和对象的实际实例。因此,一旦上述代码运行,我们可以说我们有一个 Person 类型对象,以及一个 p 对象,它的类型是 Person。p 的实例会有一个指针连接回 Person 对象,以便它知道自己的类型。Person 类型对象只会存在一个,但 Person 的实例可以有很多。好的,那么这是什么意思呢?为什么是两个对象?如果我们考虑一个类型的成员的职责:字段和方法(FirstName, LastName, GetFullName),每个成员应该做什么?以 FirstName/LastName 为例。每次创建 Person 的新实例时,它都需要有自己独立的 FirstName 和 LastName 值。每次创建 Person p,p1,p2,p3... 等等,它们不能使用相同的 FirstName LastName 实例。如果您更改了 p1 中的 FirstName,它不应该改变 p2 中的 FirstName,对吗?那么,关于方法呢?Person 的每个实例都需要一个 GetFullName 方法的副本吗?这个方法只是获取实例的值并将其连接起来。它没有针对特定人员做任何具体的事情。请务必理解,运行时使用的每个方法都会占用内存。所以,让 Person 的每个实例都有自己的 GetFullName 方法可能不是一个好主意。幸运的是,它们没有。这就是 Person 类型对象的作用。它保存着所有定义在其上的方法的引用,并且因为实例有一个指向 Person 类型对象的指针,它们可以在需要使用方法时找到这些方法的位置。现在,我们已经有了类型结构的基础:每个对象实例都有自己的字段,但它们都共享存在于类型对象上的方法。您可能会问,所有这些是如何实现的?嗯,幸运的是,对于 .NET 开发者来说,这一切都是由 CLR 自动完成的。您不必担心创建类型对象、将事物指向该类型对象,或确保只创建一个方法,或任何类似的事情。
那么,这与 JavaScript 有什么关系呢?我已经描述了 .NET 的工作方式,这对于确保最佳性能是必要的。我想强调的是,在 JavaScript 中进行面向对象编程时,这些事情不会自动为您完成。您必须自己来做。
所以,要以 JavaScript 的方式定义一个 Person 类型,我们会有如下代码:
function Person(fn,ln)
{
this.FirstName = fn;
this.LastName = ln;
this.GetFullName = function()
{
return this.FirstName + ' ' + this.LastName;
}
}
var p = new Person('jon','smith');
alert(p.GetFullName());
很好,这看起来与 C# 非常相似,并且功能完全符合我们的要求,那么问题出在哪里呢?问题在于,我们关于类型结构的一个基本原则没有得到满足。如果您在 JavaScript 中这样做,Person 的每个实例都会有自己的 GetFullName 方法副本。这显然浪费了内存。那么,我们如何让所有实例共享这个方法呢?您可能会问,JavaScript 不会像 .NET 那样创建一个类型对象吗?不,可以说是,但也不完全是。这就是原型对象(prototype object)的作用。这个概念可能会有点令人困惑。在我看来,最好不要想太多。就把它想象成这样:当您在 JavaScript 中创建一个函数时,JS 引擎会在您不知道的情况下在其中放置一个隐藏属性。所以当我们创建 Person 函数时,它实际上是这样的:
function Person(fn,ln)
{
this.FirstName = fn;
this.LastName = ln;
this.GetFullName = function()
{
return this.FirstName + ' ' + this.LastName;
}
}
Person.prototype= {};
您可以将原型对象视为与 .NET 类型对象相同的东西。但最大的区别是,方法默认不会自动放入原型对象。您必须自己完成。请记住,这里是您希望放置所有只需创建一次并与所有实例共享的函数的地方。那么,我们如何将函数放入其中呢?就像这样:
function Person(fn,ln)
{
this.FirstName = fn;
this.LastName = ln;
}
Person.prototype.GetFullName = function()
{
return this.FirstName + ' ' + this.LastName;
}
var p = new Person('jon','smith');
alert(p.GetFullName());
您可能会想,当您调用 p.GetFullName() 时,它怎么会知道去查找原型对象而不是 p 对象呢?JS 会自动这样做,您不必担心。
好的,那么现在问题出在哪里?这种方法在技术上没有问题,但将一个对象的方法定义在对象定义之外可能会让人困惑。我们不能直接把它放在里面吗?当然可以,就像这样:
function Person(fn,ln)
{
this.FirstName = fn;
this.LastName = ln;
Person.prototype.GetFullName = function()
{
return this.FirstName + ' ' + this.LastName;
}
}
var p = new Person('jon','smith');
alert(p.GetFullName());
这看起来不错,但有一个小问题。您现在又在每次创建对象实例时重新创建了 GetFullName 方法。当然,没有对前一个的引用,所以它最终会被垃圾回收,但仍然有点浪费。同样,如果我们与 .NET 进行比较,CLR 会自动处理判断方法是否已被创建,如果已创建,则不再重新创建。但由于 JS 不会为我们做这件事,所以我们需要自己完成。我们通过使用以下方法来做到这一点:
function Person(fn,ln)
{
this.FirstName = fn;
this.LastName = ln;
if(typeof this.GetFullName != "function"){
Person.prototype.GetFullName = function()
{
return this.FirstName + ' ' + this.LastName;
}
}
}
var p = new Person('jon','smith');
alert(p.GetFullName());
所以,我们现在有一个检查来查看函数是否已存在,如果不存在,我们则添加它。就这样,我们得到了一个模仿 C# 类在 JavaScript 中的良好实现。
好了,今天就到这里。我知道这是一篇简短的文章,但我将在第二部分继续介绍继承。