JavaScript中实现类的五种模式详解:从对象字面量到原型模式
本文深入探讨JavaScript中创建对象与模拟类的五种核心模式:对象字面量、构造函数、揭示模块、原型及命名约定模式。详细解析了各模式在私有变量、公有成员、内存占用及继承机制上的差异,帮助开发者根据封装性与性能需求选择最佳方案。
JavaScript类实现方式概述
JavaScript 提供了多种创建对象的手段,这使得对象创建过程极具灵活性。然而,面对构造模式、原型模式或对象字面量模式(Object literal),究竟哪种方式最为恰当?
然而这些模式的具体运作机制是怎样的呢?
在深入讲解具体模式之前,我们需要先回顾 JavaScript 的基础知识。
JavaScript 是否支持面向对象编程(OOP)的实现?
答案是肯定的。JavaScript 能够创建包含数据及操作数据方法的对象,甚至包含其他对象。虽然它没有传统的“类”概念,但拥有构造函数;虽然没有类继承机制,但可以通过原型(prototype)实现继承。
至此,我们已了解了在 JavaScript 中创建对象及实现基于对象编程所需的核心要素。
我们知道 JavaScript 支持私有变量。通过“var”关键字定义的变量仅在函数体内可访问,外部无法直接访问。若不使用“var”定义变量会怎样?这通常涉及通过“this”访问,后续将详细讨论此问题。
回到最初的问题:哪种对象创建方式最为恰当?
让我们通过创建一个 Person 对象来验证上述知识。
代码如下:
var Person = {
firstName : 'John',
lastName : 'Cody',
fullName : '',
message : '',
createFullName : function () {
fullName = this.firstName + ' ' + this.lastName;
},
changeMessage : function (msg) {
this.message = msg;
},
getMessage : function () {
this.createFullName();
return this.message + ' ' + fullName;
}
}
Person.firstName = 'Eli';
Person.lastName = 'Flowers'
Person.changeMessage('welcome');
var message = Person.getMessage(); // welcome Eli Flowers
alert(message); 对象字面量模式
这是对象字面量模式(literal pattern)。它非常接近我们日常创建对象的方式。如果你不关心私有或封装成员,且确定不会创建该对象的多个实例,这种方式非常适合。公有成员可以执行私有成员的功能,但这并非类,而是一个独立对象,无法创建实例或继承。
让我们尝试另一种方式:
代码如下:
var Person = {
firstName : 'John',
lastName : 'Cody',
fullName : '',
message : '',
createFullName : function () {
fullName = this.firstName + ' ' + this.lastName;
},
changeMessage : function (msg) {
this.message = msg;
},
getMessage : function () {
this.createFullName();
return this.message + ' ' + fullName;
}
}
Person.firstName = 'Eli';
Person.lastName = 'Flowers'
Person.changeMessage('welcome');
var message = Person.getMessage(); // welcome Eli Flowers
alert(message); 构造函数模式
这是构造函数模式(Constructor Pattern)。它既是类也是对象。我们可以将其作为 Person 对象使用,因为它本质上是一个函数。然而,通过“new”关键字,它可以创建新实例。
采用此方法时,务必牢记以下关键点:
1. 每次调用该函数时,它拥有一个名为“this”的特殊变量,可在该函数的全局范围内使用。全局范围取决于函数自身的作用域。
2. 每次通过“new”关键字创建函数实例时,“this”变量指向该函数本身,且“new”操作会触发函数体中代码的执行。这正是构造函数模式的原理。
3. 附加到“this”变量下的变量将成为公有属性,而通过“var”关键字定义的变量则属于私有属性。
4. 附加到“this”下的函数称为特权函数,它可以访问所有私有变量以及附加到“this”下的函数和变量。
5. 私有函数可以访问其他私有变量及私有函数。
6. 私有函数不能直接访问附加到“this”变量和函数的内容。我们可以通过创建一个私有变量“_that”并将其赋值为“this”来实现访问。
7. 所有私有变量及函数对其他私有函数及附加到“this”的函数均可见。这在 JavaScript 作用域内是完全可能的。
8. 一个变量:既未通过“var”关键字定义,也未附加到“this”变量上,从而获得全局作用域。例如,对于自定义函数的作用域,需要再次理解作用域及闭包的知识。
这已实现了我们大部分需求,但“this”和“that”这两个入口变量有时容易让人困惑,尤其是对于那些坚持纯粹私有性的开发者而言。
让我们稍作修改再试一次。
代码如下:
var Person = function () {
//private
var firstName = 'John';
var lastName = 'Cody';
var fullName = '';
var message = '';
var createFullName = function () {
fullName = firstName + ' ' + lastName;
}
//public setters
var setMessage = function (msg) {
message = msg;
}
var setFirstName = function (fName) {
firstName = fName;
}
var setLastName = function (lName) {
lastName = lName;
}
var getMessage = function () {
createFullName();
return message + ' ' + fullName;
}
//functions exposed public
return {
setFirstName: setFirstName,
setLastName: setLastName,
setMessage: setMessage,
getMessage: getMessage
};
};
var person1 = new Person();
person1.setFirstName('Eli');
person1.setLastName('Flowers');
person1.setMessage('welcome');
var message = person1.getMessage(); // welcome Eli Flowers
alert(message); 揭示模块模式
这是揭示模块模式(Revealing Pattern)。感谢 Christian Heilmann。这种模式将请求的“getters”和“setters”作为属性使用。我们从传统 Java 编程中可以看到这种身影,实现起来并不复杂。这也类似于类继承接口的情况。
这种模式在大多数方面表现良好,仅有一个微小问题。每次创建类的实例时,新对象都会获得变量和函数的副本。复制变量没问题,我们希望每个对象的数据独立,但成员函数呢?它们仅操作数据,为何需要复制?
这正是原型模式(Prototype)的优势所在。在所有实例中,所有内容都创建为一个原型,并可以相互共享。我们只需依据原型创建共有函数。
代码如下:
var Person = function () {
//private
var welcomeMessage = 'welcome';
var fullName = '';
var firstName = '';
var lastName = "";
var createFullName = function () {
Person.prototype.setFirstName('asdsad');
fullName = firstName + ' ' + lastName;
};
//constructor
var Person = function () { }; //will be created evrytime
//public
Person.prototype = {
getFullName: function () {
createFullName();
return welcomeMessage + ' ' + fullName;
},
setFirstName: function (fName) {
firstName = fName;
},
setLastName: function (lName) {
lastName = lName;
},
ChangeMessage: function (mesg) {
welcomeMessage = mesg;
}
}
return new Person(); // Person; //new Person();
};
var person1 = new Person();
person1.setFirstName ('Eli');
person1.setLastName('Flowers');
person1.ChangeMessage('welcome');
var message = person1.getFullName(); // welcome asdsad Flowers
alert(message); 原型模式及其局限
原型模式的一个问题是它无法访问私有变量及私有函数,正因如此,我们才引入闭包,并始终组织好类中的代码,使其在全局范围内不致混乱。所有内容均属于 Person 类的作用域。
另一个问题是每次创建实例时,全部代码都会执行一遍,包括原型绑定。对部分人来说,这仅是效率问题。处理此问题的一种方式是仅在期望共有函数不可用时绑定原型。
这将使原型绑定操作仅在第一个实例创建时执行,此后其他实例仅进行检查。不幸的是,这仍未解决上述示例中的问题,因为我们仍需重新创建函数以生成闭包来实现类效果。这样至少减少了部分内存使用。
等等,还有另一个问题是私有函数无法直接访问原型函数。
为何一定要需要私有函数和私有变量?我知道你想实现类的封装性,确保类中的属性或内部数据不被突然修改或被内部其他程序修改,或进行其他操作……
你应该记住,JavaScript 代码无法编译为二进制文件,对此你可能感到恼火,因为代码始终可用。因此,如果有人想干扰代码,无论你是否真正实现私有化,无论你将代码交给团队成员还是出售,他们都可以干扰代码。实现私有化可能略有裨益。
命名约定与私有化
其他编程者使用的另一种技术是使用命名约定,给所有你想设为私有的内容加上前缀“_”以规定其为私有。
代码如下:
(function () {
var Person = function () {
this._fullName = '';
this.welcomeMessage = '';
this.firstName = '';
this.lastName = "";
_that = this;
this._createFullName = function () {
this.ChangeMessage('Namaste');
this._fullName = this.firstName + ' ' + this.lastName;
};
}
//Shared Functions for Code optimization
Person.prototype = {
constructor: Person,
getFullName: function () {
this._createFullName();
return this.welcomeMessage + ' ' + this._fullName;
},
ChangeMessage: function (mesg) {
this.welcomeMessage = mesg;
}
}
this.Person = Person;
})();
var person1 = new Person();
person1.firstName = 'Eli';
person1.lastName = 'Flowers';
person1.ChangeMessage('Welcome');
var message = person1.getFullName(); // Namaste Eli Flowers
alert(message); 我不是说你不应该考虑“private”或类似知识。你是代码的设计者,因此你知道如何管理并做出最佳选择。根据你的需求,你可以使用任何一种设计模式或多个设计模式的组合。
无论你决定采用哪种设计模式,始终记住做尽量少的事情,不要在全局作用域内实现闭包,尽量减少内存泄漏,并优化代码,组织好代码。因此,尽量多了解作用域、闭包及“this”的表现行为。
最后,祝编程愉快!
译者后记
经常使用 JavaScript,给人的印象一直是直接拷贝即可使用。最近使用 extjs,其类框架非常好用。通过此类文章,我们明白了 JavaScript 中实现类的各种方式,并在文章最后讨论了类中私有成员的实现情况。
JS合并多个数组的三种方法:concat、spread与flat
2026-11-05 17:39:45
JavaScript怎么准确判断数据类型:typeof、instanceof与Object.prototype.toString
2026-11-05 17:09:21
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
