基于JavaScript如何实现私有成员的语法特征及私有成员的实现方式


Posted in Javascript onOctober 28, 2015

前言

在面向对象的编程范式中,封装都是必不可少的一个概念,而在诸如 Java,C++等传统的面向对象的语言中, 私有成员是实现封装的一个重要途径。但在 JavaScript 中,确没有在语法特性上对私有成员提供支持, 这也使得开发人员使出了各种奇技淫巧去实现 JS 中的私有成员,以下将介绍下目前实现 JS 私有成员特性的几个方案以及它们之间的优缺点对比。

现有的一些实现方案

约定命名方案

约定以下划线'_'开头的成员名作为私有成员,仅允许类成员方法访问调用,外部不得访问私有成员。简单的代码如下:

JavaScript

var MyClass = function () {
  this._privateProp = ‘privateProp';
};
MyClass.prototype.getPrivateProp = function () {
  return this._privateProp;
};
var my = new MyClass();
alert(my.getPrivateProp()); // ‘privateProp';
alert(my._privateProp); // 并未真正隐藏,依然弹出 ‘privateProp'

优点

毫无疑问,约定命名是最简单的私有成员实现方案,没有代码层面上的工作。
调试方便,能够在控制台上直接看到对象上的私有成员,方便排查问题。
兼容性好,ie6+都支持

不足

无法阻止外部对私有成员的访问和变更,如果真有不知道或者不遵守约定的开发人员变更私有属性,也是无能为力。
必须强制或说服大家遵守这个约定,当然这个在有代码规范的团队中不是什么太大的问题。

es6 symbol 方案

在 es6中,引入了一个 Symbol 的特性,该特性正是为了实现私有成员而引入的。
主要的思路是,为每一个私有成员的名称产生一个随机且唯一的字符串key,这个 key 对外不可见,对内的可见性则是通过 js 的闭包变量实现,示例代码如下:

JavaScript

(function() {
   var privateProp = Symbol(); // 每次调用会产生一个唯一的key
   function MyClass() {
     this[privateProp] = ‘privateProp'; // 闭包内引用到这个 key
   }
   MyClass.prototype.getPrivateProp = function () {
     return this[privateProp];
   };
})();
var my = new MyClass();
alert(my.getPrivateProp()); // ‘privateProp';
alert(my.privateProp); // 弹出 undefined,因为成员的key其实是随机字符串

优点

弥补了命名约定方案的缺陷,外部无法通过正常途径获得私有成员的访问权。
调试便捷程度上可以接受,一般是通过给 symbol 的构造函数传入一个字符串参数,则控制台上对应的私有属性名称会展示为:Symbol(key)

兼容性不错,不支持 Symbol的浏览器可以很容易的 shim 出来。

不足

写法上稍显别扭,必须为每一个私有成员都创建一个闭包变量让内部方法可以访问。
外部还是可以通过 Object.getOwnPropertySymbols的方式获取实例的 symbol 属性名称,通过该名称获得私有成员的访问权。这种场景出现得比较少,且知道这种途径的开发人员水平相信都是有足够的能力知道自己的行为会有什么影响,因此这个不足点也算不上真正意义的不足。

es6 WeakMap 方案

在 es6 中引入了 Map, WeakMap 容器,最大的特点是容器的键名可以是任意的数据类型,虽说初衷不是为了实现私有成员引入,但意外的可以被用来实现私有成员特性。

主要的思路是,在类的级别上创建一个 WeakMap 容器,用于存储各个实例的私有成员,这个容器对外不可见,对内通过闭包方式可见;内部方法通过将实例作为键名获取容器上对应实例的私有成员,示例代码如下:

JavaScript

(function() {
   var privateStore = new WeakMap(); // 私有成员存储容器
   function MyClass() {
     privateStore.set(this, {privateProp: ‘privateProp'}); // 闭包内引用到privateStore, 用当前实例做 key,设置私有成员
   }
   MyClass.prototype.getPrivateProp = function () {
     return privateStore.get(this).privateProp; 
   };
})();
var my = new MyClass();
alert(my.getPrivateProp()); // ‘privateProp';
alert(my.privateProp); // 弹出 undefined,实例上并没有 privateProp 属性

优点

弥补了命名约定方案的缺陷,外部无法通过正常途径获得私有成员的访问权。
对 WeakMap 做一些封装,抽出一个私有特性的实现模块,可以在写法上相对 Symbol 方案更加简洁干净,其中一种封装的实现可以查看参考文章3。
最后一个是个人认为最大的优势:基于 WeakMap 方案,可以方便的实现保护成员特性(这个话题会在其他文章说到:))

不足

不好调试,因为是私有成员都在闭包容器内,无法在控制台打印实例查看对应的私有成员
待确认的性能问题,根据 es6的相关邮件列表,weakmap 内部似乎是通过顺序一一对比的方式去定位 key 的,时间复杂度为 O(n),和 hash 算法的 O(1)相比会慢不少

最大的缺陷则是兼容性带来的内存膨胀问题,在不支持 WeakMap 的浏览器中是无法实现 WeakMap 的弱引用特性,因此实例无法被垃圾回收。 比如示例代码中 privateProp 是一个很大的数据项,无弱引用的情况下,实例无法回收,从而造成内存泄露。

现有实现方案小结

从上面的对比来看,Symbol方案最大优势在于很容易模拟实现;而WeakMap的优势则是能够实现保护成员, 现阶段无法忍受的不足是无法模拟实现弱引用特性而导致的内存问题。于是我的思路又转向了将两者优势结合起来的方向。

Symbol + 类WeakMap 的整合方案

在 WeakMap 的方案中最大的问题是无法 shim 弱引用,较次要的问题是不大方便调试。

shim 出来的 WeakMap 主要是无法追溯实例的生命周期,而实例上的私有成员的生命周期又是依赖实例, 因此将实例级别的私有成员部分放在实例上不就好了? 实例没了,自然其属性也随之摧毁。而私有存储区域的隐藏则可以使用 Symol 来做。

该方案的提供一个 createPrivate 函数,该函数会返回一个私有的 token 函数,对外不可见,对内通过闭包函数获得, 传入当前实例会返回当前实例的私有存储区域。使用方式如下:

JavaScript

(function() {
   var $private = createPrivate(); // 私有成员 token 函数,可以传入对象参数,会作为原型链上的私有成员
   function MyClass() {
     $private(this).privateProp = ‘privateProp' ; // 闭包内引用到privateStore, 用当前实例做 key,设置私有成员
   }
   MyClass.prototype.getPrivateProp = function () {
     return $private(this).privateProp; 
   };
})();
var my = new MyClass();
alert(my.getPrivateProp()); // ‘privateProp';
alert(my.privateProp); // 弹出 undefined,实例上并没有 privateProp 属性

代码中主要就是实现 createPrivate 函数,大概的实现如下:

JavaScript

// createPrivate.js
function createPrivate(prototype) {
  var privateStore = Symbol('privateStore');
  var classToken = Symbol(‘classToken');
  return function getPrivate(instance) {
     if (!instance.hasOwnProperty(privateStore)) {
       instance[privateStore] = {};
     }
    var store = instance[classToken];
     store[token] = store[token] || Object.create(prototype || {});
     return store[token];
   };
}

上述实现做了两层存储,privateStore 这层是实例上统一的私有成员存储区域,而 classToken 对应的则是继承层次之间不同类的私有成员定义,基类有基类的私有成员区域,子类和基类的私有成员区域是不同的。

当然,只做一层的存储也可以实现,两层存储仅仅是为了调试方便,可以直接在控制台通过Symbol(‘privateStore')这个属性来查看实例各个层次的私有部分。

奇葩的 es5 property getter 拦截方案

该方案纯粹是闲得无聊玩了玩,主要是利用了 es5 提供的 getter,根据 argument.callee.caller 去判断调用场景,如果是外部的则抛异常或返回 undefined, 如果是内部调用则返回真正的私有成员,实现起来比较复杂,且不支持 strict 模式,不推荐使用。 有兴趣的同学可以看看实现。

总结

以上几个方案对比下来,我个人是倾向 Symbol+WeakMap 的整合方案,结合了两者的优点,又弥补了 WeakMap 的不足和 Symbol 书写的冗余。 当然了,我相信随着 JS 的发展,私有成员和保护成员也迟早会在语法层面上进行支持,正如 es6 对 class 关键字和 super 语法糖的支持一样, 只是现阶段需要开发者使用一些技巧去填补语言特性上的空白。

Javascript私有成员的实现方式

总体来讲这本书还是可以的,但看完这本书还留了几个问题一直困扰着我,如js中私有变量的实现,prototype等,经过自己一系列测试,现在终于弄明白了。

很多书上都是说,Javascript是不能真正实现Javascript私有成员的,因此在开发的时候,统一约定 __ 两个下划线开头为私有变量。

后来,发现Javascript中闭包的特性,从而彻底解决了Javascript私有成员的问题。

function testFn(){ 
    var _Name;//定义Javascript私有成员 
    this.setName = function(name){ 
     _Name = name; //从当前执行环境中获取_Name 
    } 
    this.getName = function(){ 
     return _Name; 
    } 
}// End testFn 
var test = testFn(); 
alert(typeof test._Name === "undefined")//true 
test.setName("KenChen");

test._Name 根本访问不到,但是用对象方法能访问到,因为闭包能从当前的执行环境中获取信息。

接下来我们看看,共有成员是怎样实现的

function testFn(name){ 
  this.Name = name; 
  this.getName = function(){ 
   return this.Name; 
  } 
} 
var test = new testFn("KenChen"); 
test.getName(); //KenChen 
test.Name = "CC"; 
est.getName();//CC

接下来在看看类静态变量是怎样实现的

function testFn(){ 
} 
testFn.Name = "KenChen"; 
alert(testFn.Name);//KenChen 
testFn.Name = "CC"; 
alert(testFn.Name);//CC
Javascript 相关文章推荐
在一个浏览器里呈现所有浏览器测试结果的前端测试工具的思路
Mar 02 Javascript
JS交换变量的方法
Jan 21 Javascript
jquery让指定的元素闪烁显示的方法
Mar 17 Javascript
jQuery实现模拟flash头像裁切上传功能示例
Dec 11 Javascript
jQuery验证表单格式的使用方法
Jan 10 Javascript
Mongoose经常返回e11000 error的原因分析
Mar 29 Javascript
Angular directive递归实现目录树结构代码实例
May 05 Javascript
微信小程序实现文字跑马灯效果
May 26 Javascript
解决vue中修改export default中脚本报一大堆错的问题
Aug 27 Javascript
发布一款npm包帮助理解npm的使用
Jan 03 Javascript
Vue.js中的高级面试题及答案
Jan 13 Javascript
js面向对象之实现淘宝放大镜
Jan 15 Javascript
jQuery实现滑动页面固定顶部显示(可根据显示位置消失与替换)
Oct 28 #Javascript
jquery实现的动态回到顶部特效代码
Oct 28 #Javascript
JavaScript如何调试有哪些建议和技巧附五款有用的调试工具
Oct 28 #Javascript
jQuery实现鼠标滑过链接控制图片的滑动展开与隐藏效果
Oct 28 #Javascript
使用Chart.js图表库制作漂亮的响应式表单
Oct 28 #Javascript
jQuery+PHP+MySQL二级联动下拉菜单实例讲解
Oct 27 #Javascript
jQuery结合CSS制作动态的下拉菜单
Oct 27 #Javascript
You might like
PHP 5.0对象模型深度探索之类的静态成员
2008/03/27 PHP
php下将图片以二进制存入mysql数据库中并显示的实现代码
2010/05/27 PHP
PHP异常Parse error: syntax error, unexpected T_VAR错误解决方法
2014/05/06 PHP
PHP常用的三种设计模式
2017/02/17 PHP
Yii2 批量插入、更新数据实例
2017/03/15 PHP
PHP文件管理之实现网盘及压缩包的功能操作
2017/09/20 PHP
ExtJS GTGrid 简单用户管理
2009/07/01 Javascript
JS打开层/关闭层/移动层动画效果的实例代码
2013/05/11 Javascript
以JSON形式将JS中Array对象数组传至后台的方法
2014/01/06 Javascript
form.submit()不能提交表单的原因分析
2014/10/23 Javascript
javascript面向对象之访问对象属性的两种方式分析
2015/01/13 Javascript
浅谈jquery回调函数callback的使用
2015/01/30 Javascript
javasript实现密码的隐藏与显示
2015/05/08 Javascript
JavaScript实现跑马灯抽奖活动实例代码解析与优化(一)
2016/02/16 Javascript
bootstrap jquery dataTable 异步ajax刷新表格数据的实现方法
2017/02/10 Javascript
JS+Ajax实现百度智能搜索框
2017/08/04 Javascript
基于jQuery实现图片推拉门动画效果的两种方法
2017/08/26 jQuery
详解Vue结合后台的列表增删改案例
2018/08/21 Javascript
详解node字体压缩插件font-spider的用法
2018/09/28 Javascript
详解ES6 Symbol 的用途
2018/10/14 Javascript
在vue中使用G2图表的示例代码
2019/03/19 Javascript
详解vue中在父组件点击按钮触发子组件的事件
2020/11/13 Javascript
[03:55]显微镜下的DOTA2特别篇——430灰烬之灵神级操作
2014/06/24 DOTA
[58:57]2018DOTA2亚洲邀请赛3月29日小组赛B组 Effect VS VGJ.T
2018/03/30 DOTA
Python和Ruby中each循环引用变量问题(一个隐秘BUG?)
2014/06/04 Python
Python argv用法详解
2016/01/08 Python
Python获取SQLite查询结果表列名的方法
2017/06/21 Python
pytorch多GPU并行运算的实现
2019/09/27 Python
Python 读取xml数据,cv2裁剪图片实例
2020/03/10 Python
Python列表切片常用操作实例解析
2020/03/10 Python
sklearn和keras的数据切分与交叉验证的实例详解
2020/06/19 Python
Python+Opencv实现把图片、视频互转的示例
2020/12/17 Python
CSS3用@font-face实现自定义英文字体
2013/09/23 HTML / CSS
总裁办公室主任职责
2014/01/02 职场文书
2014年计划生育协会工作总结
2014/11/14 职场文书
2015年学校党支部工作总结
2015/04/01 职场文书