浅谈 class 私有变量

class 的前世此生

在 es6 以前,虽然 JS 和 Java 一样都是 OOP (面向对象)语言,可是在 JS 中,只有对象而没有类的概念。前端

在 JS 中,生成实例对象的传统方法是经过构造函数,以下所示:vue

function A (x) {
    this.x = x
}

// 在原型链上挂载原型方法
A.prototype.showX = function () {
    return this.x
}

// 生成对象实例
let a = new A(1)
// 调用原型方法
a.showX()	// 1
复制代码

对比传统 OOP 语言中的类写法,这种写法让许多学过其余 OOP 语言的 JS 初学者感到困惑。git

为了实如今 JS 中写 Java 的心愿,当时有人将构造函数写法封装成了相似于 Java 中类的写法的 klass 语法糖 。es6

有人会问,为何是 klass 而不是 class ?固然是由于 class 是 JS 中的保留关键字,直接用 class 会报错。github

就这样凑合着过了好多年,直到 es6 发布,在 es6 中, klass 终于备胎转正,摇身一变变成了 class ,终于从官方角度实现了梦想。闭包

以前的代码转换成 class 是这样的:模块化

class A {
    // 构造函数,至关于以前的函数A
    constructor(x) {
        this.x = x
    }
    
    // 至关于挂载在原型链上的原型方法
    showX () {
        return this.x
    }
}

// 生成对象实例
let a = new A(1)
// 调用原型方法
a.showX()	// 1
复制代码

能够发现, class 的写法更接近传统 OOP 语言。函数

class 的不足

看起来, es6 中 class 的出现拉近了 JS 和传统 OOP 语言的距离。可是,就如以前所说的 klass 同样,它仅仅是一个语法糖罢了,不能实现传统 OOP 语言同样的功能。在其中,比较大的一个痛点就是私有变量问题。性能

何为私有变量?私有变量就是只能在类内部访问的变量,外部没法访问的变量。在开发中,不少变量或方法你不想其余人访问,能够定义为私有变量,防止被其余人使用。在 Java 中,可使用 private 实现私有变量,可是惋惜的是, JS 中并无该功能。ui

来看下下面这个代码:

class A {
    constructor(x) {
        this.x = x
    }
	
    // 想要经过该方法来暴露x
    showX () {
        return this.x
    }
}

let a = new A(1)

// 直接访问x成功
a.x	// 1
复制代码

能够看到,虽然本意是经过方法 showX 来暴露 x 的值,可是能够直接经过 a.x 来直接访问 x 的值。

很明显,这影响了代码的封装性。要知道,这些属性都是可使用 for...in 来遍历出来的。

因此,实现 class 的私有变量功能是颇有必要的。

实现 class 私有变量

虽然, class 自己没有提供私有变量的功能,可是,咱们能够经过经过一些方式来实现相似于私有变量的功能。

约定命名

首先,是目前使用最广的方式:约定命名,又称为:本身骗本身或者潜规则

该方式很简单,就是团队自行约定一种表明着私有变量的命名方式,通常是在私有变量的名称前加上一个下划线。代码以下:

class A {
    constructor(x) {
        // _x 是一个私有变量
        this._x = x
    }

    showX () {
        return this._x
    }
}

let a = new A(1)

// _x 依然能够被使用
a._x		// 1
a.showX()	//1
复制代码

能够发现,该方法最大的优势是简单、方便,因此不少团队都采用了这种方式。

可是,该方式并无从本质上解决问题,若是使用 for...in 依然能够遍历出所谓的私有变量,能够说是治标不治本。

不过,该方式有一点值得确定,那就是经过约定规范来方便他人阅读代码。

闭包

闭包在不少时候被拿来解决模块化问题,显而易见,私有变量本质上也是一种模块化问题,因此,咱们也可使用闭包来解决私有变量的问题。

咱们在构造函数中定义一个局部变量,而后经过方法引用,该变量就成为了真正的私有变量。

class A {
    constructor (x) {
        let _x = x
        this.showX = function () {
            return _x
        }
    }
}

let a = new A(1)
// 没法访问
a._x		// undefined
// 能够访问
a.showX()	// 1
复制代码

该方法最大的优势就是从本质解决了私有变量的问题。

可是有个很大的问题,在这种状况下,引用私有变量的方法不能定义在原型链上,只能定义在构造函数中,也就是实例上。这致使了两个缺点:

  1. 增长了额外的性能开销
  2. 构造函数包含了方法,较为臃肿,对后续维护形成了必定的麻烦(不少时候,看到代码写成一坨就不想看 -_-)

进阶版闭包

进阶版闭包方式能够基本完美解决上面的那个问题:既然在构造函数内部定义闭包那么麻烦,那我放在 class 外面不就能够了吗?

咱们能够经过 IIFE (当即执行函数表达式) 创建一个闭包,在其中创建一个变量以及 class ,经过 class 引用变量实现私有变量。

代码以下:

// 利用闭包生成IIFE,返回类A
const A = (function() {
    // 定义私有变量_x
    let _x

    class A {
        constructor (x) {
            // 初始化私有变量_x
            _x = x
        }

        showX () {
            return _x
        }
    }

    return A
})()

let a = new A(1)

// 没法访问
a._x		// undefined
// 能够访问
a.showX()	//1
复制代码

能够发现,该方法完美解决了以前闭包的问题,只不过写法相对复杂一些,另外,还须要额外建立 IIFE ,有一点额外的性能开销。

注1:该方式也能够不使用 IIFE ,能够直接将私有变量置于全局,可是这不利于封装性,因此,我在这里采用了 IIFE 的方式。

注2:对于 IIFE 是不是个闭包,在 You-Dont-Know-JS 这本书中有过争议,有兴趣的同窗能够前去了解一下,在此再也不赘述。

Symbol

这种方式利用的是 Symbol 的惟一性—— 敌人最大的优点是知道我方key值,我把key值弄成惟一的,敌人不就没法访问了吗?人质是此次任务的关键,当敌人再也不拥有人质时,任务也就完成了

代码以下:

// 定义symbol
const _x = Symbol('x')

class A {
    constructor (x) {
        // 利用symbol声明私有变量
        this[_x] = x
    }
    showX () {
        return this[_x]
    }
}

let a = new A(1)

// 自行定义一个相同的Symbol
const x = Symbol('x')
// 没法访问
a[x]		// undefined
// 能够访问
a.showX()	//1
复制代码

从结果来看,完美地实现了 class 私有变量。

我的认为,这是目前最完美的实现私有变量的方式,惟一的缺点就是 Symbol 不太经常使用,不少同窗不熟悉。

私有属性提案

针对 es6 中的 class 没有私有属性的问题,产生了一个提案——在属性名以前加上 # ,用于表示私有属性。

class A {
    #x = 0
    constructor (x) {
        #x = x
    }
    showX () {
        return this.#x
    }
}
复制代码

不少同窗会有一个问题,私有属性提案为何不使用 private 而使用 #是人性的扭曲仍是道德的沦丧? 这一点和编译器性能有关(其实我我的认为还有一大缘由是向 Python 靠拢,毕竟从 es6 以来, JS 一直向着 Python 发展),有兴趣的同窗能够去了解了解。

不过该提案仅仅仍是提案罢了,并无进入标准,因此依然没法使用。

最后

若是上述全部方法全都知足不了你,还有一个终极方法—— TypeScript 。使用 TS ,让你享受在 JS 中写 Java 的快感!区区私有变量,不在话下。

就今年的发展趋势来看, TS 已经成为前端必备的技能之一,连以前 diss 过 TS 的尤大都已经开始用 TS 重写 Vue 了(尤大:真香)。

最后的最后

又到了平常的求 star 环节,若是你们以为这篇文章还不错的话,不如给我点个 star 再走呗~

Github