深入了解JavaScript 私有化


Posted in Javascript onMay 30, 2019

Class field declarations for JavaScript(JavaScript 类的字段声明)目前已经进入了 stage-3,其中包含一项 OOP 开发者都很关注的内容:Private fields。JavaScript 一直没有私有成员并不是没有原因,所以这一提议给 JavaScript 带来了新的挑战。但同时,JavaScript 在 ES2015 发布的时候已经在考虑私有化的问题了,所以要实现私有成员也并非毫无基础。


首先挖个坑 —— 这是一段 JS 代码,BusinessView 中要干两件事情,即对表单和地图进行布局。

代表将 _ 前缀约定为私有

class BaseView {
layout() {
console.log("BaseView Layout");
}
}
class BusinessView extends BaseView {
layout() {
super.layout();
this._layoutForm();
this._layoutMap();
}
_layoutForm() {
// ....
}
_layoutMap() {
// ....
}
}

然后,由于业务的发展,发现有很多视图都存在地图布局。这里选用继承的方式来实现,所以从 BusinessView 中把地图相关的内容抽象成一个基类叫 MapView:

class MapView extends BaseView {
layout() {
super.layout();
this._layoutMap();
}
_layoutMap() {
console.log("MapView layout map");
}
}
class BusinessView extends MapView {
layout() {
super.layout();
this._layoutForm();
this._layoutMap();
}
_layoutForm() {
// ....
}
_layoutMap() {
console.log("BusinessView layout map");
}
}

上面这两段代码是很典型的基于继承的 OOP 思想,本意是期望各个层次的类都可以通过 layout() 来进行各层次应该负责的布局任务。但理想和现实总是有差距的,在 JavaScript 中运行就会发现 BusinessView._layoutMap() 被执行了两次,而 MapView._layoutMap() 未执行。为什么?

虚函数

JavaScript 中如果在祖先和子孙类中定义了相同的名称的方法,默认会调用子孙类中的这个方法。如果想调用祖先类中的同名方法,需要在子孙类中通过 super. 来调用。

这里可以分析一下这个过程:

在子类创建对象的时候,其类和所有祖先类的定义都已经加载了。这个时候

  • 调用 BusinessView.layout()
  • 找到 super.layout(),开始调用 MapView.layout()
  • MapView.layout() 中调用this._layoutMap()
    • 于是从当前对象(BusinessView 对象)寻找 _layoutMap()
    • 找到,调用它

你看,由于 BusinessView 定义了 _layoutMap,所以压根都没去搜索原型链。对的,这是基于原型关系的 OOP 的局限。如果我们看看 C# 的处理过程,就会发现有所不同

  • 调用 BusinessView.layout()
  • 找到 base.layout(),开始调用 MapView.layout()
  • MapView.layout() 中调用 this._layoutMap()
    • 在 MapView 中找到 _layoutMap()
    • 检查是否虚函数
      • 如果是,往子类找到最后一个重载(override)函数,调用
      • 如果不是,直接调用

发现区别了吗?关键是在于判断“虚函数”。

然而,这跟私有成员又有什么关系呢?因为私有函数肯定不是虚函数,所以在 C# 中,如果将 _layoutMap 定义为私有,那 MapView.layout() 调用的就一定是 MapView._layoutMap()。

虚函数的概念有点小复杂。不过可以简单理解为,如果一个成员方法被声明为虚函数,在调用的时候就会延着其虚函数链找到最后的重载来进行调用。

JavaScript 中虽然约定 _ 前缀的是私有,那也只是君子之约,它实质上仍然不是私有。君子之约对人有效,计算机又不知道你有这个约定……。但是,如果 JavaScript 真的实现了私有成员,那么计算机就知道了,_layoutMap() 是个私有方法,应该调用本类中的定义,而不是去寻找子类中的定义。

解决当下的私有化问题

JavaScript 当下没有私有成员,但是我们又需要切时有效地解决私有成员问题,怎么办?当然有办法,用 Symbol 和闭包来解决。

注意,这里的闭包不是指导在函数函数中生成闭包,请继续往下看

首先搞清楚,我们变通的看待这个私有化问题 —— 就是让祖先类调用者在调用某个方法的时候,它不会先去子类中寻找。这个问题从语法上解决不了,JavaScript 就是要从具体的实例从后往前去寻找指定名称的方法。但是,如果找不到这个方法名呢?

之所以能找到,因为方法名是字符串。一个字符串在全局作用域内都表示着同样的意义。但是 ES2015 带来了 Symbol,它必须实例化,而且每次实例化出来一定代表着不同的标识 —— 如果我们将类定义在一个闭包中,在这个闭包中声明一个 Symbol,用它来作为私有成员的名称,问题就解决了,比如

const MapView = (() => {
const _layoutMap = Symbol();
return class MapView extends BaseView {
layout() {
super.layout();
this[_layoutMap]();
}
[_layoutMap]() {
console.log("MapView layout map");
}
}
})();
const BusinessView = (() => {
const _layoutForm = Symbol();
const _layoutMap = Symbol();
return class BusinessView extends MapView {
layout() {
super.layout();
this[_layoutForm]();
this[_layoutMap]();
}
[_layoutForm]() {
// ....
}
[_layoutMap]() {
console.log("BusinessView layout map");
}
}
})();

而现代基于模块的定义,甚至连闭包都可以省了(模块系统会自动封闭作用域)

const _layoutMap = Symbol();
export class MapView extends BaseView {
layout() {
super.layout();
this[_layoutMap]();
}
[_layoutMap]() {
console.log("MapView layout map");
}
}
const _layoutForm = Symbol();
const _layoutMap = Symbol();
export class BusinessView extends MapView {
layout() {
super.layout();
this[_layoutForm]();
this[_layoutMap]();
}
[_layoutForm]() {
// ....
}
[_layoutMap]() {
console.log("BusinessView layout map");
}
}

改革过后的代码就可以按预期输出了:

BaseView Layout
MapView layout map
BusinessView layout map

后记

笔者在多年开发过程中养成了分析和解决问题的一系列思维习惯,所以常常可以迅速的透过现象看到需要解决的实质性问题,并基于现有条件来解决它。确实,Symbol 出现的理由之一就是解决私有化问题,但是为什么要用以及怎么用就需要去分析和思考了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Javascript-Mozilla和IE中的一个函数直接量的问题
Jan 09 Javascript
JavaScript XML操作 封装类
Jul 01 Javascript
比Jquery的document.ready更快的方法
Apr 28 Javascript
JavaScript入门之事件、cookie、定时等
Oct 21 Javascript
Javascript:为input设置readOnly属性(示例讲解)
Dec 25 Javascript
js表格排序实例分析(支持int,float,date,string四种数据类型)
May 06 Javascript
学习JavaScript设计模式(接口)
Nov 26 Javascript
javascript中异常处理案例(推荐)
Oct 03 Javascript
jQuery Easy UI中根据第一个下拉框选中的值设置第二个下拉框是否可以编辑
Nov 29 Javascript
深入理解Vue 单向数据流的原理
Nov 09 Javascript
微信小程序-API接口安全详解
Jul 16 Javascript
解决包含在label标签下的checkbox在ie8及以下版本点击事件无效果兼容的问题
Oct 27 Javascript
jQuery模拟html下拉多选框的原生实现方法示例
May 30 #jQuery
Vue CL3 配置路径别名详解
May 30 #Javascript
Vue CLI3中使用compass normalize的方法
May 30 #Javascript
通过实践编写优雅的JavaScript代码
May 30 #Javascript
AngularJs中$cookies简单用法分析
May 30 #Javascript
JS使用new操作符创建对象的方法分析
May 30 #Javascript
vue滚动固定顶部及修改样式的实例代码
May 30 #Javascript
You might like
使用 eAccelerator加速PHP代码的方法
2007/09/30 PHP
判断Keep-Alive模式的HTTP请求的结束的实现代码
2011/08/06 PHP
深入掌握include_once与require_once的区别
2013/06/17 PHP
thinkphp 一个页面使用2次分页的实现方法
2013/07/15 PHP
PHP中可以自动分割查询字符的Parse_str函数使用示例
2014/07/25 PHP
PHP实现的数独求解问题示例
2017/04/18 PHP
PHP手机短信验证码实现流程详解
2018/05/17 PHP
PHP生成二维码与识别二维码的方法详解【附源码下载】
2019/03/07 PHP
$.ajax json数据传递方法
2008/11/19 Javascript
简略的前端架构心得&&基于editor为例子的编码小技巧
2010/11/25 Javascript
js操作checkbox遇到的问题解决
2013/06/29 Javascript
js中的for如何实现foreach中的遍历
2014/05/31 Javascript
用js读、写、删除Cookie代码续篇
2014/12/03 Javascript
jQuery前端框架easyui使用Dialog时bug处理
2014/12/05 Javascript
jQuery焦点图插件SaySlide
2015/12/21 Javascript
js+css绘制颜色动态变化的圈中圈效果
2016/01/27 Javascript
Websocket 向指定用户发消息的方法
2020/01/09 Javascript
vue radio单选框,获取当前项(每一项)的value值操作
2020/09/10 Javascript
[01:19]DOTA2城市挑战赛报名开始 开启你的城市传奇
2018/03/23 DOTA
[01:04:32]DOTA2-DPC中国联赛 正赛 Aster vs LBZS BO3 第二场 2月23日
2021/03/11 DOTA
Python爬取三国演义的实现方法
2016/09/12 Python
基于Python中numpy数组的合并实例讲解
2018/04/04 Python
对pycharm代码整体左移和右移缩进快捷键的介绍
2018/07/16 Python
python梯度下降法的简单示例
2018/08/31 Python
python绘制多个曲线的折线图
2020/03/23 Python
python之线程通过信号pyqtSignal刷新ui的方法
2019/01/11 Python
python实现实时视频流播放代码实例
2020/01/11 Python
Tech21美国/加拿大:英国NO.1防摔保护壳品牌
2018/01/20 全球购物
Banana Republic英国官网:香蕉共和国,GAP集团旗下偏贵族风
2018/04/24 全球购物
分解成质因数(如435234=251*17*17*3*2,据说是华为笔试题)
2014/07/16 面试题
网上商城创业计划书范文
2014/01/31 职场文书
法院先进个人事迹材料
2014/05/04 职场文书
小学生安全教育广播稿
2014/10/20 职场文书
《秦兵马俑》教学反思
2016/02/24 职场文书
Spring Boot 启动、停止、重启、状态脚本
2021/06/26 Java/Android
Python办公自动化解决world文件批量转换
2021/09/15 Python