深入了解JavaScript 私有化

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 出现的理由之一就是解决私有化问题,但是为什么要用以及怎么用就需要去分析和思考了。

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

(0)

相关推荐

  • JavaScript私有变量实例详解

    本文实例讲述了JavaScript私有变量.分享给大家供大家参考,具体如下: 任何在函数中定义的变量,就是私有变量,因为这些变量在函数外部是无法访问到的.总的来说,私有变量包括函数的参数.局部变量和在函数内部定义的其他函数. function add(num1, num2){ var sum = num1 + num2; return sum; } 上面的例子中的 num1, num2, sum 就是函数的私有变量. 如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量,

  • 详解JavaScript 新语法之Class 的私有属性与私有方法

    译者按: 为什么偏要用 # 符号? 原文:JavaScript's new #private class fields •译者:Fundebug 本文采用意译,版权归原作者所有 proposal-class-fields与proposal-private-methods定义了 Class 的私有属性以及私有方法,这 2 个提案已经处于 Stage 3,这就意味着它们已经基本确定下来了,等待被加入到新的 ECMAScript 版本中.事实上,最新的 Chrome 已经支持了 Class 私有属性.

  • 浅谈js继承的实现及公有、私有、静态方法的书写

    今天没事的时候,研究了一下JS继承的实现,下面是html的源码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>JS类的继承的实现</title> <script type="text/JavaScript"> //定义父类及公有.私有.静态属性及方法 function parent(){ var pname

  • JS块级作用域和私有变量实例分析

    本文实例讲述了JS块级作用域和私有变量.分享给大家供大家参考,具体如下: 块级作用域 (function(){ //这里是块级作用域 })() 例如: (function(){ for(var i=0;i<5;i++){ alert(i);//0,1,2,3,4 } })(); alert(i);//error 上例中,定义了一个块级作用域,变量i在块级作用域中可见的,但是在块级作用域外部则无法访问. 这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数. 私有变

  • 深入了解JavaScript 私有化

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

  • 面向对象的Javascript之三(封装和信息隐藏)

    同时,我们知道在面向对象的高级语言中,创建包含私有成员的对象是最基本的特性之一,提供属性和方法对私有成员进行访问来隐藏内部的细节.虽然JS也是面向对象的,但没有内部机制可以直接表明一个成员是公有还是私有的.还是那句话,依靠JS的语言灵活性,我们可以创建公共.私有和特权成员,信息隐藏是我们要实现的目标,而封装是我们实现这个目标的方法.我们还是从一个示例来说明:创建一个类来存储图书数据,并实现可以在网页中显示这些数据. 1. 最简单的是完全暴露对象.使用构造函数创建一个类,其中所有的属性和方法在外部

  • Javascript模块化编程详解

    模块化编程是一种非常常见Javascript编程模式.它一般来说可以使得代码更易于理解,但是有许多优秀的实践还没有广为人知. 基础 我们首先简单地概述一下,自从三年前Eric Miraglia(YUI的开发者)第一次发表博客描述模块化模式以来的一些模块化模式.如果你已经对于这些模块化模式非常熟悉了,大可以直接跳过本节,从"进阶模式"开始阅读. 匿名闭包 这是一种让一切变为可能的基本结构,同时它也是Javascript最棒的特性.我们将简单地创建一个匿名函数并立即执行它.所有的代码将跑在

  • javaScript面向对象继承方法经典实现

    JavaScript的出现已经将近20多年了,但是对这个预言的褒贬还是众说纷纭.很多人都说JavaScript不能算是面向对象的变成语言.但是JavaScript的类型非常松散,也没有编译器.这样一来给了程序员很大的自由,也带来了一些缺陷. 虽然JavaScript不算是一门面向对象的语言.但是我们可以模仿着其他语言实现面向对象的方式来实现JavaScript的面向编程. 下面是JavaScript教程中非常经典的继承方法. 复制代码 代码如下: //定义一个Pet对象.通过这一个名称和数量的腿

  • functional继承模式 摘自javascript:the good parts

    这种模式的作用在于确保对象属性的真正私有化, 我们无法直接访问对象的状态, 只能通过特权方法来进行操作. 下面直接来个例子吧. 复制代码 代码如下: var person = function(cfg) { var that = {}; that.getName = function() { return cfg.name || 'unknow name'; }; // 性别 默认男性 that.getGender = function() { return cfg.gender || 'mal

  • JavaScript 匿名函数和闭包介绍

    匿名函数:没有名字的函数; 闭包:可访问一个函数作用域里的变量的函数; 一 匿名函数 // 普通函数 function box(){ // 函数名是box; return 'Lee'; } box(); // =>Lee; 调用函数; // 匿名函数 function(){ // 匿名函数,会报错; return 'Lee'; } // 通过表达式自我执行 (function(name){ console.log(name); // =>Lee; })("Lee"); //

  • 轻松学习Javascript闭包函数

    闭包函数是什么?在开始学习的闭包的时候,大家很能都比较难理解.就从他的官方解释来说,都是比较概念化的. 不过我们也还是从闭包的含义出发. 闭包是指函数有自由独立的变量.换句话说,定义在闭包中的函数可以"记忆"它创建时候的环境. 官方解释说完后,我们先来看一个简单计数的例子. var c = 0; function count(){ c++; } count();// 1 count();// 2 这个例子是利用了全局变量来实现,但是这里有个问题是,c这个变量也容易被其他方式所调用,这时

  • javascript设计模式 封装和信息隐藏(上)

    本文分上下两部分,上部讲基本模式(basic patterns):完全暴露法,下划线标记法和使用闭包:下部讲高级模式(Advanced Patterns),如何实现静态方法和属性,常量还有其他一些知识点. 封装是面向对象语言很基本也是很有用的特性,虽然javascript也可以称的上是面向对象语言,但他对封装的支持并不是很好,不像其他语言,只要使用private.protected就可以实现.但这并不是说就没有办法了,下面我就介绍下如何在javascript中实现封装. 一.基本模式(basic

  • JavaScript实现设计模式中的单例模式的一些技巧总结

    一.使用全局变量保存单例 这是最简单的实现方法 function Person(){ this.createTime=new Date(); } var instance=new Person(); function getInstance(){ return instance; } 加载该js时就创建一个Person对象,保存到instance全局变量中,每次使用都取这个对象.如果一次都没使用,那么创建的这个对象则浪费了,我们可以优化一下, var instance function getI

  • 在JavaScript中实现类的方式探讨

    在 javascript 中有很多方式来创建对象,所以创建对象的方式使用起来非常灵活.那么,到底哪一种方式是最恰当的对象创建方式呢?构造模式,原型模式还是对象原意模式(Object literal)呢? 但这些模式具体是怎么回事呢? 在开始讲解之前,让我们先清楚地介绍一下关于 javascript 基本知识. 有没有可能在 javascript 中实现面向对象编程的方式呢? 答案是可能的,javascript 是可以创建对象的!这种对象可以包含数据及能够操作数据的方法,甚至可以包含其他对象.它没

随机推荐