JavaScript封装设计的三种模式:约定、严格与闭包

作者:函数茶馆

时间:2026-10-29

浏览:659

本文深入解析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构造器的总调用次数。

以上就是本文的全部内容,希望对大家的学习有所帮助,大家可以更深入的学习了解封装的意义。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章