文档章节

JavaScript 的私有成员

边城
 边城
发布于 2018/08/13 16:44
字数 1573
阅读 25
收藏 0

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

笔者在专栏《JavaScript 全栈工程师养成记》的第四章讲到了原型 OOP 关系和继承 OOP 关系的关键区别。今天这里就研究一下 JavaScript 私有成员的问题。

首先挖个坑 —— 这是一段 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 全栈工程师养成记》,并跟着笔者一起思考、分析和解决软件开发过程中的若干问题。

© 著作权归作者所有

边城

边城

粉丝 91
博文 8
码字总数 18624
作品 1
绵阳
技术主管
私信 提问
加载中

评论(0)

JavaScript 私有成员

Class field declarations for JavaScript(JavaScript 类的字段声明)目前已经进入了 stage-3,其中包含一项 OOP 开发者都很关注的内容:Private fields。JavaScript 一直没有私有成员并不是...

边城__
2018/08/13
0
0
JavaScript面向对象之属性实现

作者:Truly 日期:2007.8.3 在我前面一篇文章《 在JavaScript中使用面向对象》中我们介绍了MSDN的一篇文章《使用面向对象的技术创建高级 Web 应用程序》,作者简单介绍了JavaScript面向对象...

Truly
2007/08/03
0
0
JavaScript的public、private和privileged模式

公共/私有变量和方法 通过一个简单的的例子,来展示如何使用JavaScript在类里面创建私有变量和方法 Summary 私有变量 在对象内部使用'var'关键字来声明,而且它只能被私有函数和特权方法访问...

dml124
2012/10/19
593
4
使用面向对象技术创建高级Web应用程序

最近,我面试了一位具有5年Web应用开发经验的软件开发人员。她有4年半的JavaScript编程经验,自认为自己具有非常优秀的JavaScript技能,可是,随后我很快发现,实际上她对JavaScript却知之甚...

时过境迁_
2013/06/20
1.2W
41
JavaScript 的黑暗时刻来了?

近日,横竖都是二的「#」字符在前端圈子点了一把熊熊烈火,燃烧起的浓烟让很多使用 JavaScript 的开发者懊恼其黑暗时刻已来临。 这其中真实的原因是 ECMA TC39 委员会(Technical Committee...

CSDN资讯
2018/10/12
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Java高级面试攻略:消息+缓存+高并发+高可用+架构+分布式+微服务

Java高级面试攻略:消息+缓存+高并发+高可用+架构+分布式+微服务,对每一个专题每一道列举出来的高频面试题,都会做一个面试官的心理分析以及题目的具体剖析,教你如何破解,攻克面试官! 攻...

眉目清俊
20分钟前
23
0
JavaScript 数组遍历方法的对比

前言 JavaScript 发展至今已经发展出多种数组的循环遍历的方法,不同的遍历方法运行起来那个比较快,不同循环方法使用在那些场景,下面将进行比较: 各种数组遍历的方法 for 语句 代码: var arr...

osc_fscujk71
29分钟前
11
0
Nginx和Tomcat配置https

Nginx配置htpps http{ #http节点中可以添加多个server节点 server{ #监听443端口 listen 443; #对应的域名,把baofeidyz.com改成你们自己的域名就可以了 ...

chinahufei
29分钟前
16
0
react调用方法

React里调用方法常见的方式有两种 (1)触发时通过回调函数触发 <Button type="primary" onClick={()=>this.updateBar()}>更新</Button> (2)直接通过方法名触发 <Button type="primary" o......

osc_vpogdtu8
30分钟前
26
0
Centos通过代理上网

阿里云ECS不能访问内外,购买NAT的除外,通过windows主机代理上网: 1.windows上安装代理工具,工具种类比较多,我这里使用的是privoxy 下载地址:http://www.privoxy.org/ 百度云盘(备用)...

osc_r94nrknb
31分钟前
11
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部