深入了解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实例教程(19) 使用HoTMetal(5)
Dec 23 Javascript
javaScript让文本框内的最后一个文字的后面获得焦点实现代码
Jan 06 Javascript
下载文件个别浏览器文件名乱码解决办法
Mar 19 Javascript
iframe父页面获取子页面参数的方法
Feb 21 Javascript
浅谈JS闭包中的循环绑定处理程序
Nov 09 Javascript
13 款最热门的 jQuery 图像 360 度旋转插件推荐
Dec 09 Javascript
Jquery使用css方法改变样式实例
May 18 Javascript
jquery选择器简述
Aug 31 Javascript
利用require.js与angular搭建spa应用的方法实例
Jul 19 Javascript
详解vue+axios给开发环境和生产环境配置不同的接口地址
Aug 16 Javascript
javascript 设计模式之享元模式原理与应用详解
Apr 08 Javascript
如何使用原生Js实现随机点名详解
Jan 06 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
强烈推荐:php.ini中文版(2)
2006/10/09 PHP
PHP隐形一句话后门,和ThinkPHP框架加密码程序(base64_decode)
2011/11/02 PHP
php实现压缩多个CSS与JS文件的方法
2014/11/11 PHP
php实现微信企业号支付个人的方法详解
2017/07/26 PHP
ThinkPHP框架整合微信支付之刷卡模式图文详解
2019/04/10 PHP
javascript parseInt 大改造
2009/09/27 Javascript
jQuery+ajax实现顶一下,踩一下效果
2010/07/17 Javascript
利用webqq协议使用python登录qq发消息源码参考
2013/04/08 Javascript
node.js中的console用法总结
2014/12/15 Javascript
基于豆瓣API+Angular开发的web App
2015/01/02 Javascript
js实现下拉框选择要显示图片的方法
2015/02/16 Javascript
JS基于面向对象实现的拖拽库实例
2015/09/24 Javascript
JS实现页面进入和返回定位到具体位置
2016/12/08 Javascript
ES6正则表达式的一些新功能总结
2017/05/09 Javascript
vue自定义底部导航栏Tabbar的实现代码
2018/09/03 Javascript
checkbox在vue中的用法小结
2018/11/13 Javascript
vue框架中props的typescript用法详解
2020/02/17 Javascript
[35:44]2014 DOTA2华西杯精英邀请赛 5 24 iG VS VG
2014/05/26 DOTA
[39:52]2018DOTA2亚洲邀请赛 4.3 突围赛 EG vs Newbee 第一场
2018/04/04 DOTA
[01:10:57]Liquid vs OG 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
python 查找文件夹下所有文件 实现代码
2009/07/01 Python
对python程序内存泄漏调试的记录
2018/06/11 Python
如何使用Python多线程测试并发漏洞
2019/12/18 Python
python在协程中增加任务实例操作
2021/02/28 Python
css3类选择器之结合元素选择器和多类选择器用法
2017/03/09 HTML / CSS
HTML5在手机端实现视频全屏展示方法
2020/11/23 HTML / CSS
FC-Moto西班牙:摩托车手最大的购物场所之一
2019/04/11 全球购物
Overload和Override的区别。Overloaded的方法是否可以改变返回值的类型
2013/10/30 面试题
金融专业毕业生推荐信
2013/11/26 职场文书
销售职业生涯规划范文
2014/03/14 职场文书
小学模范班主任事迹材料
2014/05/13 职场文书
企业党员一句话承诺
2014/05/30 职场文书
中职三好学生事迹材料
2014/08/24 职场文书
2015年度内部审计工作总结
2015/05/20 职场文书
导游词之峨眉山
2019/12/16 职场文书
javascript canvas实现雨滴效果
2021/06/09 Javascript