Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

浅谈 class 私有变量 #11

Open
KarthusLorin opened this issue Jun 30, 2020 · 0 comments
Open

浅谈 class 私有变量 #11

KarthusLorin opened this issue Jun 30, 2020 · 0 comments

Comments

@KarthusLorin
Copy link
Owner

class 的前世今生

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

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

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

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

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

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

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

有人会问,为什么是 klass 而不是 class ?当然是因为 class 是 JS 中的保留关键字,直接用 class 会报错。

就这样凑合着过了好多年,直到 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 中并没有该功能。

来看下下面这个代码:

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 了(尤大:真香)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant