首页 / 文章

闭包、私有变量与#:在TypeScript和JavaScript中各自的保护作用

TypeScript中的private属性是在编译时进行检查的,而哈希字段则在运行时得到强制约束,闭包则会捕获词法作用域。下面分别介绍它们的功能。

发布者:REACTAPP.TOP阅读约 7 分钟1514 词

来源: Closures vs Private in TypeScript: If We Have private and #, Why Do We Still Need Closures?

TypeScript拥有private关键字,而JavaScript则使用#,那么一个合理的问题是:为何仍然需要闭包来保护变量?在了解JavaScript运行时的行为之前,答案似乎很显然,但深入了解后就会发现其中更有意思的细节。我们先来探讨闭包。首先,TypeScript的private关键字究竟起什么作用?

TypeScript的private

以一个包含一个私有属性的类为例:

class User {
	private secret: string = "hidden";
}

现在创建一个实例,并从类外部读取该属性:

const me = new User();
console.log(me.secret);

关键句就是这一句:

console.log(me.secret);

编译器 tsc 会因错误而拒绝它,因此该变量看起来是被保护的。问题在于 private 主要只是类型系统的一个特性。当 TypeScript 转换为 JavaScript 时,该关键字并不会作为运行时的隐私机制保留下来。这段代码如下:

class User {
    private secret: string = "hidden";
}

可大致表示为:

class User {
    constructor() {
        this.secret = "hidden";
    }
}

此时 JavaScript 并不知道 secret 应该是私有的,因为它只是一种普通属性。因此会出现以下情况:

const me = new User();
console.log(me.secret);

可打印:

hidden

这种区别很重要。private 告诉编译器的是,使用该类型的代码不得修改该属性。但它并未告知运行时该属性必须完全无法访问,这两者是截然不同的承诺。

# 能带来什么变化

TypeScript 还支持 JavaScript 原生的私有类字段:

class User {
    #secret: string = "hidden";
}

现在尝试从外部读取该字段会出现不同的错误:

const me = new User();console.log(me.#secret);

这不仅仅是一个编译器警告。在运行时该字段也是私有的,因为#语法会创建真正的私有类字段。从类外部读取它是非法的:

console.log(me.#secret);

读取同名的普通属性也无法访问到它:

console.log(me.secret);

因为 #secret 并非名为 secret 的普通属性。人们可能会倾向于将其想象成一条字符串键:

me["#secret"]

但那样行不通。#并非命名规范,因为运行时确实存在对私有字段的概念。因此我们面对的是两回事。第一种主要由类型系统来强制规定:

private secret: string;

第二种则是基于JavaScript语义:

#secret: string;

闭包的适用场景

在私有类字段出现之前,开发者仍然需要那些无法从外部直接访问的数据,而该语言早已具备的最强大的工具之一就是闭包。以下是一个工厂函数:

function createUser() {
    let secret = "hidden";

它返回一个仅提供一个访问器的对象:

    return {
        getSecret() {
            return secret;
        }
    };
}const user = createUser();console.log(user.getSecret());

调用该访问器会输出:

hidden

注意缺少了什么。没有这样的属性访问方式:

user.secret

返回的对象甚至没有secret属性。该变量存在于调用createUser()时创建的词法作用域中,而返回的getSecret()函数会记住该作用域。这就是闭包的核心:一个函数即便在外部函数执行完毕之后,仍能继续访问其创建时的作用域中的变量。此次调用之后:

const user = createUser();

外部函数已经处理完成。通常情况下,人们会关注它的局部变量:

let secret = "hidden";

从而消失。然而 getSecret() 仍然能够访问到它,因为它捕获了 secret,名称也正是由此而来。

在不暴露的情况下保持私有状态

闭包可以存储私有状态而无需暴露任何变量。计数器就是一个很好的例子:

function myCounter() {
    let count = 0;
    return {
        increment() {
            count++;
        },        getCount() {
            return count;
        }
    };
}const counter = myCounter();counter.increment();
counter.increment();console.log(counter.getCount());

调用两次增量方法后再查询计数,输出结果为:

2

而这种方式则无法生效:

console.log(counter.count);

因为 count 并非计数器的属性,而是捕获的词法作用域中的变量。对象负责提供功能行为,而实际状态则保存在其他地方,这是一种基于词法作用域的封装方式。

# 真的就更好吗?

不一定。它们解决的是相关问题,并非相同的特性。以下是类版本:

class User {
    #secret = "hidden";
    getSecret() {
        return this.#secret;
    }
}

在其中,#secret 是对象所拥有的明确私有类状态,运行时能够感知到它。而在闭包版本中:

function createUser() {
    let secret = "hidden";
    return {
        getSecret() {
            return secret;
        }
    };
}

该变量根本不是对象属性,而是存在于周围的词法作用域中。两者的机制有本质区别。#字段表示该对象拥有私有字段,而闭包则表示该函数能够使用其所在作用域中的状态。这种差异会影响API的设计。

函数间共享的状态

闭包带来的好处不仅限于隐私保护:多个函数可以共享同一份状态。银行账户便体现了这一点:

function createBankAccount(initialBalance) {
    let balance = initialBalance;
    return {
        deposit(amount) {
            balance += amount;
        },        withdraw(amount) {
            balance -= amount;
        },        getBalance() {
            return balance;
        }
    };
}

使用方法如下:

const account = createBankAccount(1000);
account.deposit(500);console.log(account.getBalance());

结果如下:

1500

没有可读取的公共属性:

account.balance

deposit()、withdraw() 和 getBalance() 都使用同一个局部变量:

balance

这就是闭包在 JavaScript 中如此重要的原因之一。它们不仅是隐藏变量的技巧,更是实现跨调用保持状态的手段。

纠正初步认知

一种常见的总结是:闭包提供了隐藏的运行时机制,而 private 和 # 只是起到编译时警告的作用。这种说法接近事实但并不准确。TypeScript 的 private 主要用于编译时访问检查,而 JavaScript 的 #private 则在运行时强制实现隐私保护。闭包既不是另一种关键字,也并非纯粹的编译时概念——它们源于词法作用域机制,该机制使函数能够继续访问其所在环境中的变量。虽然这可以实现封装功能,但闭包的作用远不止隐私保护。以下是一个不涉及隐私保护的函数工厂示例:

function createMultiplier(x) {
    return function (y) {
        return x * y;
    };
}
const double = createMultiplier(2);console.log(double(10));

此处返回的函数会记住x,这就是闭包。同样的机制也被用于实现私有状态、工厂函数、回调函数、事件处理程序、记忆化等功能。

三者的概念模型

对于 TypeScript 中的private:

class User {
    private secret = "hidden";
}

可以理解为类型系统阻止你访问该成员。对于 JavaScript 的 #private 来说:

class User {
    #secret = "hidden";
}

可理解为运行时将该字段视为真正的私有成员。对于闭包而言:

function createUser() {
    let secret = "hidden";
    return {
        getSecret() {
            return secret;
        }
    };
}

可以将其视为一种能够记住其定义作用域中变量的函数。最后一点最为关键。既然有了 #,闭包就不再过时了,因为 #private 是专为私有类字段设计的特性,而闭包则是词法作用域与一等函数带来的结果,它们能实现许多与隐私无关的功能。

底层原理

还有一个细微之处需要注意。说 private 和 # 最终都会变成普通的 JavaScript 属性是不正确的。这在 TypeScript 的常规输出中是对的,但并不适用于原生的 #private 字段:

class User {
    #secret = "hidden";
}

这使用了运行时特性。根据编译目标的不同,TypeScript 可能会保留该语法,或为较旧的 JavaScript 降级处理,但语言语义上仍要求具备私有字段的功能。因此不要将 private 和 #private 放在同一类别中:它们在 TypeScript 中看起来相似,但实现方式不同,而闭包则是完全不同的机制。

那么还需要闭包吗?

没错,比起探讨为何即便有 private 和 # 仍需闭包,一个更好的问题是每种机制能解决什么问题。它们各自满足不同的需求:private 用于限制类型检查器内的访问,#private 则让运行时本身来管控私有字段,而闭包则能让函数继续使用其创建时的作用域。一旦弄清这一点,闭包就不再只是处理私有变量的旧有临时方案,而是展现出其作为 JavaScript 编程模型基础构建块的真正作用。学习新特性并不会让旧概念变得无关紧要,它只会让你能提出更精准的问题,即该机制究竟为何有用。对于闭包而言,其作用远不止于隐藏变量而已。