• 浅谈class私有变量


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

    最后的最后

    又到了日常的求 star 环节,如果大家觉得这篇文章还不错的话,不如给我点个 star 再走呗~

    Github

  • 相关阅读:
    十二周学习进度
    冲刺第十天
    19.Maven 的单模块之 Spring MVC + Spring + Spring Data JPA 项目(基于 IntelliJ IDEA)
    18. Maven 的单模块 / 多模块之 Spring MVC + Spring + Mybatis 项目讲解(重点)
    16.Java Web 项目环境搭建
    17.Maven 项目介绍
    15.Postfix Completion 的使用
    16.插件讲解
    14.Emmet 讲解
    13.文件代码模板讲解
  • 原文地址:https://www.cnblogs.com/karthuslorin/p/10189178.html
Copyright © 2020-2023  润新知