淺談 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

相關文章
相關標籤/搜索