JavaScript封装设计的三种模式:约定、严格与闭包
本文深入解析JavaScript中封装设计的三种主要模式。首先介绍基于下划线约定的轻量级封装,其次探讨通过构造函数内部变量实现的严格封装及其内存开销问题,最后详解利用闭包实现私有成员共享的高级技巧。通过对比三种方式的优缺点,帮助开发者根据实际场景选择最合适的封装策略,平衡代码安全性与性能。
JavaScript封装概述
JavaScript作为一门弱类型语言,原生并不支持抽象类和接口概念。在类型封装方面,它既缺乏相关机制,也无需过度设计。对于设计模式的实现而言,这种不区分类型的特性虽然看似是一种局限,但从灵活性的角度看,也可视为一种解脱。
从设计模式的视角来看,封装的核心价值在于对变化的封装。
通过封装变化,可以将系统中稳定不变的部分与易变部分隔离。在系统演进过程中,只需替换那些已封装好的易变模块,从而最大程度地保障程序的稳定性与可扩展性。
三种封装模式详解
JavaScript中常见的封装基本模式主要有以下三种:
约定优先模式
第一种模式遵循约定优先原则,即所有私有变量均以单下划线(_)开头。
<script type="text/javascript">
/**
* 使用约定优先的原则,把所有的私有变量都使用_开头
*/
var Person = function (no, name, age)
{
this.setNo(no);
this.setName(name);
this.setAge(age);
}
Person.prototype = {
constructor: Person,
checkNo: function (no)
{
if (!no.constructor == "string" || no.length != 4)
throw new Error("学号必须为4位");
},
setNo: function (no)
{
this.checkNo(no);
this._no = no;
},
getNo: function ()
{
return this._no;
setName: function (name)
{
this._name = name;
},
getName: function ()
{
return this._name;
},
setAge: function (age)
{
this._age = age;
},
getAge: function ()
{
return this._age;
},
toString: function ()
{
return "no = " + this._no + " , name = " + this._name + " , age = " + this._age;
}
};
var p1 = new Person("0001", "小平果", "22");
console.log(p1.toString()); //no = 0001 , name = 小平果 , age = 22
p1.setNo("0003");
console.log(p1.toString()); //no = 0003 , name = 小平果 , age = 22
p1.no = "0004";
p1._no = "0004";
console.log(p1.toString()); //no = 0004 , name =小平果 , age = 22
</script>
查看相关代码后,可能会发现这似乎并未真正阻止外部访问,因为变量依然可以直接读写。这确实不算严格的封装,但正如前文所述,这属于“约定优先”的策略。
这种下划线命名法是一种广为人知的命名规范,意在表明该属性仅供对象内部使用。直接访问或修改它可能导致不可预知的后果。这一规范有助于防止程序员的无意误用,但无法阻止有意的外部访问。
这种方式具有一定的优势,因为成员变量的getter和setter方法定义在prototype中,而非每个对象实例中,整体而言是一个不错的选择。若认为此方式仍不够严格,必须实现真正的封装,则可参考第二种方式。
严格封装模式
第二种模式为严格实现封装。
<script type="text/javascript">
/**
* 使用这种方式虽然可以严格实现封装,但是带来的问题是get和set方法都不能存储在prototype中,都是存储在对象中的
* 这样无形中就增加了开销
*/
var Person = function (no, name, age)
{
var _no , _name, _age ;
var checkNo = function (no)
{
if (!no.constructor == "string" || no.length != 4)
throw new Error("学号必须为4位");
};
this.setNo = function (no)
{
checkNo(no);
_no = no;
};
this.getNo = function ()
{
return _no;
}
this.setName = function (name)
{
_name = name;
}
this.getName = function ()
{
return _name;
}
this.setAge = function (age)
{
_age = age;
}
this.
getAge = function ()
{
return _age;
}
this.setNo(no);
this.setName(name);
this.setAge(age);
}
Person.prototype = {
constructor: Person,
toString: function ()
{
return "no = " + this.getNo() + " , name = " + this.getName() + " , age = " + this.getAge();
}
}
;
var p1 = new Person("0001", "小平果", "22");
console.log(p1.toString()); //no = 0001 , name =小平果 , age = 22
p1.setNo("0003");
console.log(p1.toString()); //no = 0003 , name = 小平果 , age = 22
p1.no = "0004";
console.log(p1.toString()); //no = 0003 , name = 小平果 , age = 22
</script>
这与之前讨论的其他对象创建模式有何不同?在前例中,创建和引用对象属性时需使用this关键字;而在本例中,变量通过var声明。这意味着这些变量仅存在于Person构造函数内部。checkNo函数同样以这种方式声明,因此成为私有方法。
需要访问这些变量和函数的方法只需声明在Person构造函数内部即可。这些方法被称为特权方法,因为它们虽然是公共接口,却能访问私有属性和方法。为了在对象外部调用这些特权函数,它们前面加上了this关键字。由于这些方法定义在Person构造函数的作用域内,因此能够访问私有属性。引用这些属性时不使用this关键字,因为它们并非公开成员。所有取值器和赋值器方法均改为不加this直接引用这些属性。
任何无需直接访问的私有属性方法,仍可在Person.prototype中声明,例如toString()方法。只有那些需要直接访问私有成员的方法才应设计为特权方法。然而,过多的特权方法会占用大量内存,因为每个对象实例都包含所有特权方法的新副本。
观察上述代码,去掉了this.属性名的写法,严格实现了封装,成员变量只能通过getter和setter访问。但存在一个问题:所有方法都存在于对象实例中,增加了内存开销。
闭包封装模式
第三种模式是以闭包的方式实现封装。
<script type="text/javascript">
var Person = (function ()
{
//静态方法(共享方法)
var checkNo = function (no)
{
if (!no.constructor == "string" || no.length != 4)
throw new Error("学号必须为4位");
};
//静态变量(共享变量)
var times = 0;
//return the constructor.
return function (no, name, age)
{
console.log(times++); // 0 ,1 , 2
var no , name , age; //私有变量
this.setNo = function (no) //私有方法
{
checkNo(no);
this._no = no;
};
this.getNo = function ()
{
return this._no;
}
this.setName = function (name)
{
this._name = name;
}
this.getName = function ()
{
return this._name;
}
this.setAge = function (age)
{
this._age = age;
}
this.getAge = function ()
{
return this._age;
}
this.setNo(no);
this.setName(name);
this.setAge(age);
}
})();
Person.prototype = {
constructor: Person,
toString: function ()
{
return "no = " + this._no + " , name = " + this._name + " , age = " + this._age;
}
};
var p1 = new Person("0001", "小平果", "22");
var p2 = new Person("0002", "abc", "23");
var p3 = new Person("0003", "aobama", "24");
console.log(p1.toString()); //no = 0001 , name = 小平果 , age = 22
console.log(p2.toString()); //no = 0002 , name = abc , age = 23
console.log(p3.toString()); //no = 0003 , name = aobama , age = 24
</script>
在上述代码中,JavaScript引擎加载完成后,会立即执行Person = 立即执行函数,该函数返回一个子函数,此子函数才是new Person调用的构造函数。由于子函数保持了对立即执行函数中checkNo(no)和times的引用(明显的闭包特性),因此checkNo和times为所有Person对象所共有。创建3个对象后,times值分别为0、1、2。这种方式的好处在于,使Person中需要复用的方法和属性既保持私有,又在对象间共享。
这里的私有成员和特权成员仍声明在构造函数内,但该构造函数从普通函数变为内嵌函数,并作为包含它的函数的返回值赋给变量Person。这就创建了一个闭包,可以将静态私有成员声明在其中。外层函数声明后的一对空括号至关重要,其作用是在代码载入时立即执行该函数。该函数的返回值是另一个函数,被赋给Person变量,使Person成为构造函数。在实例化Person时,调用的是这个内层函数。外层函数仅用于创建一个可存储静态成员的闭包。
在本例中,checkNo被设计为静态方法,因为为Person的每个实例都生成该方法的新副本毫无必要。此外还有一个静态属性times,用于跟踪Person构造器的总调用次数。
以上就是本文的全部内容,希望对大家的学习有所帮助,大家可以更深入的学习了解封装的意义。
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
