JavaScript面向对象封装:从被动约定到主动闭包实现
本文深入解析JavaScript中的封装特性。对比被动封装(如下划线约定)与主动封装(利用函数作用域和闭包),详解特权方法的创建及其内存开销问题。同时介绍静态成员的访问方式,帮助开发者构建更安全、可扩展的JS面向对象代码。
JavaScript封装概述
本文通过具体实例,详细解析JavaScript面向对象编程中的封装特性,旨在为读者提供清晰的参考与学习思路。
封装
封装(Encapsulation)的核心在于隐藏对象内部的数据与实现细节。尽管许多面向对象语言提供如private等关键字来支持这一特性,但JavaScript并未内置此类关键字。其实现机制依赖于闭包,仅允许通过对象内部定义的方法访问特定属性。此外,接口作为数据封装的工具,规定了外部访问方法的规范。在应用开发中,类应仅暴露已实现的接口方法,其余属性和方法均保持隐藏状态,外界只能通过接口定义的存取操作与对象交互。
在JavaScript语言中没有提供专门的信息封装关键字,不过可以使用闭包来创建,只允许从对象内部访问的方法和属性。另外,接口也是数据封装的一种工具,接口提供了外界访问方法的约定。在应用开发中,所有类都应定义接口,类只向外提供已实现接口中规定的方法,任何别的方法都是隐藏的。其所有属性都是私有的,外界只能通过接口中定义的存取操作与之打交道。
以下内容参考自《jQuery开发从入门到精通》。原书可能存在错误或读者购入的是盗版,但下文代码均经过修正,确保可正常运行。
被动封装:约定与局限
被动封装
被动封装是指对对象内部数据施加某种约定,这种约定缺乏强制力,主要面向公共对象。在JavaScript中,数据默认是公开的,不存在隐私保护,信息可被随意访问。以下示例说明了这一情况:
var Person = function (name,gender) {
if(name === undefined) {
throw new Error("name is necessary")
} else {
this.name = name;
}
if(gender === undefined) {
throw new Error("gender is necessary")
} else {
this.gender = gender;
}
}
var p = new Person("Tom",1); // 如果我们实例化的时候,如果传递参数错误了,将会报异常
为了保障数据安全,代码中增加了适当的条件限制,以防止非法数据侵入。
var Animal = function (species) {
if(!this.checkSpecies(species)) {
throw new Error('species is illegal');
} else {
this.species = species;
}
}
Animal.prototype = {
checkSpecies:function(species){
// 检测 species , 参数为 species , 合法返回true ,这里可自定义检测逻辑
// 这里先让它直接返回 true
return true;
}
}
var ani = new Animal("dog");
从安全性与扩展性角度考量,类应当定义接口,以确保数据存取的安全并促进团队协作。虽然内部私有方法检测和接口能在一定程度上保护对象内部数据,但它们存在一个显著缺陷:这些属性和方法可能被公开重置。面对公开覆盖属性和方法值的行为,现有机制无法阻止,例如:
var Util = function() {};
Util.prototype = {
_say:function(){
console.log("这里有一个私有的成员 _say方法");
}
}
// 下面我们来修改它
Util.prototype._say = function(){
console.log('哈哈,我已经被修改了');
}
// 开始检测
var util = new Util;
util._say(); // 哈哈,我已经被修改了
同时内部检测和接口可以在一定程度上占用了系统开销,这个问题也是必须认真考虑的。
如上述代码所示,通常我们通过约定在方法或属性名称前后添加下划线来区分公共与私有成员,以此标识私有性。
下划线命名是一种约定俗成的规范,表示该属性和方法仅供内部使用。直接访问可能导致意外结果。
上述数据保护方式均属于被动防御,因为它们仅依赖约定,仅在遵守时生效。这主要适用于非敏感性的内部方法和属性。
主动封装:闭包与特权方法
主动封装
在JavaScript中,函数具有作用域,函数内部声明的变量在外部无法访问。私有与公有的区别在于对象外部是否可访问,因此利用函数作用域是实现封装的最佳途径。举例如下:
function haha() {
var n = 1;
function hehe() {
n++;
}
hehe();
return n;
}
console.log(haha()); // 2
函数haha内部包含私有变量n。在该作用域内,hehe可以访问n,而haha外部的任何函数均无法访问haha内的n。
同样地,若在函数内部返回hehe,即可在外部调用haha的私有函数hehe。
function haha() {
var n = 1;
function hehe() {
return ++n;
}
return hehe;
}
console.log(haha()()); // 2
函数作用域内部的方法无法被外界直接访问,但作用域内的其他公共方法可以访问它们。利用公共方法作为中转站,可以将内部私有方法公开化。因此,这些公共方法被称为特权方法,即在方法前加上this关键字。
使用这种方式创建的对象具备真正的封装性,例如:
function A() {
// 私有方法 _xx
function _xx(){}
// 公有方法
this.xx = function() {
return _xx;
}
}
A.prototype = {
other:function() {
console.log("other方法代表不需要访问私有属性和方法的方法,一般放在原型里");
}
}
然而,这种方式也存在一些缺点:
① 新生成的每个实例对象都会拷贝构造函数中的属性和方法,而私有的_xx 无疑在每次实例化的时候都会重复拷贝,这样会占用大量内存,所以不适合大量使用,仅在必要时适当使用
② 不利于类的继承,所有派生的子类都不能访问超类中的私有属性和方法。不过可以使用特权方法来访问超类中的私有属性和方法。举个例子:
// B 是超类
function B() {
var _private = 1;
function _checkPrivate() {
return _private;
}
this.checkPrivate = function() {
return _checkPrivate;
}
}
// C 是派生类
function C() {
B.call(this); // 用call 实现继承 ,后面我们会讲到如何继承
// this.private = _private; // 这里尝试访问超类的私有属性,因为无法访问,会报错
}
// 实例化 派生类C
var c = new C();
// console.log(c.private); // 和在C内部的尝试一样,是无法访问的,报错
// 不过可以用下面的方法访问超类中的私有属性
console.log(c.checkPrivate()()); // 1
静态成员与闭包封装推荐
静态方法
在面向对象编程中,大多数方法和属性与类的实例相关联。还有一种情况是,静态属性和方法直接与类本身联系,可直接从类访问,即静态成员在类上操作,而非实例上。在JavaScript中,Math和Global均为静态对象,无需实例化即可直接访问。
类的静态成员包括私有和公共两种。它们在系统中仅保留一份副本,不会分散传递给不同的实例对象,而是通过函数指针进行引用。书中有一个很好的示例,如下:
var F = (function(){ // 把闭包函数赋给F,返回一个构造函数
var _a = 1; // 定义一个闭包体内的私有变量
this.a = _a; // 闭包体内的公共属性 a
this.get1 = function(){ // 闭包体内的公共方法get1
return _a;
}
this.set1 = function(x){ // 闭包体内的公共方法set1
_a = x;
};
return function(){ // 返回一个构造函数,构造函数也是函数,更是对象(相当于一个类)
this.get2 = function() { // 类的get2方法
return _a;
};
this.set2 = function(x) { // 类的set2方法
_a = x;
};
}
})();
// 定义类的静态公共方法和属性
F.get3 = function(){ // 定义一个静态方法get3
return get1(); // 这里可以直接使用 F内的公共方法get1
}
F.set3 = function(x) { // 定义一个静态方法set3
return set1(x); // 同get1
}
// 下面开始测试
var f = new F(); // 实例化这个类
console.log(f.get2()); // 1 用实例对象访问公共方法get2
F.set3(3); // 调用静态方法set3
console.log(F.get3()); // 3
F.A = ++a; // 定义一个静态属性A
console.log(F.A); // 2
我们推荐使用这种闭包的封装方式
感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。
更多关于JavaScript相关内容可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》
希望本文所述对大家JavaScript程序设计有所帮助。
JS大数值转字符串出现科学计数法怎么办
2026-11-05 17:09:21
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09
