前言
自从 ES6 加入了类的基础语法以来,Javascript 中的 class 已经 5 年没有更新了。早在 2019 年,class-fields 的一系列提案就已经进入了 stage 3,其中就包括给 class 加上 public / private 成员字段,而私有成员由于使用了诡异的语法并且引入天坑无数而备受争议。
浏览器版本帝谷歌已经在 Chrome 84 中实现了私有成员等 class 的新特性。如果这一提案最终入选,相信各大 JS 引擎也只能跟进。到时候私有成员 等 class 新特性恐怕要取代 JS 判等问题成为面试题新宠。
那不如趁现在,了解一下 class 私有成员, 体验一下它如何在面试题中埋坑吧。
基本语法
语法简介
这个看起来像注释一样的 #
号,就是用来声明私有成员和私有方法的。加 #
号是特意为了和普通属性区分开来,普通属性的存取可能需要访问对象的原型链,而私有成员完全使用另外一套检索体系。为了不影响普通属性的访问优化,提案决定在语法层面使用特殊前缀来表示私有成员。
class A {
name = 'abc'; // public field
#value = 100; // private field
// private method
#doubleValue() {
return this.#value * 2;
}
// private getter
get #t() { return 1;}
getValue() {
return this.#value + this.#doubleValue() + this.#t;
}
}
class B {
// 静态私有
static #p = 1;
// 用类名访问或 this 都可以
static getP() {return B.#p;}
}
JS 未引入语义的符号已无几,低头看看键盘就剩下@#
了,而 @
被装饰器语法用掉了,大佬们一拍大腿, #
就你了。大家都知道下划线 _
一般用作君子协定式私有成员和方法名,如果你把 #
当作 _
看,似乎也没那么糟糕,吧?
class A {
name = 'abc'; // public field
_value = 100; // 约定下划线开头为私有
// 君子协定私有方法
_doubleValue() {
return this.#value * 2;
}
getValue() {
return this._value + this._doubleValue();
}
}
const a = new A();
a._value = 200; // 不好意思我是小人
不存在类似 computed property 的 [] 访问方式
私有成员不是对象的 property,所以固然不能用索引名访问。
const name = 'aaa';
class Foo {
#a = 1;
getA() {return this['#a']; } // undefined,你只是把 '#a' 当 key 找 property
[name]() {
// 这个可以有
}
#[name] = 2; // 这个不行
[#name] = 2; // 目前来说也不行
}
特性
私有性
提问,私有成员的私有是对谁私有?
答,对类私有。这里的私有指的是访问控制,即,类内部能访问,而类外部不能访问,而非拥有者私有(只有拥有者可以读取)。
class Foo {
#p;
constructor(p) { this.#p = p; }
getP() {return this.#p; }
}
const f = new Foo(1);
f.getP(); // 1
f.#p; // error: 私有成员只能在类内部使用
为了辅助理解 对类私有,我们需要另一个例子。说,我们有一个矩形类,规定如果两个矩形长宽都相等我们即认为相等,但是我们又不希望暴露长宽属性,所以打算用私有成员描述长宽。
class Rectangle {
#width
#height
constructor(width, height) {
this.#width = width;
this.#height = height;
}
area() {
return this.#width * this.#height;
}
compare(other) {
return this.#width === other.#width &&
this.#height === other.#height;
}
}
const a = new Rectangle(10, 5);
const b = new Rectangle(10, 6);
a.compare(b); // no error, returns false
喏,对象 a 其实是可以访问对象 b 的 private
成员的,并非拥有者私有制。#width
被严格限制在 class Rectangle 类的两个花括弧内访问,并且访问前引擎会检查对象是否是当前类的实例。如果不是会怎么样?
const a = new Rectangle(10, 5);
const b = new Triangle(5, 5, 5);
a.compare(b); // Boom!
你会得到一个运行时错误, Cannot read private member from an object whose class did not declare it.
综上,所谓的 private 其实是相对于程序员而言。项目中多人分工合作,我写类来你调用,我声明的公开方法你可以用,私有的东西你别碰。当我需要重构我的类的时候,只要保证对外的行为一致就行,而内部的东西我可以随意增删。
vs 闭包,vs TypeScript Private
闭包是利用作用域来实现隐藏和私有的,加上工厂函数确实可以模拟私有成员,但是这相当于摒弃了继承和原型链带来的优势。类的体系中上下文其实是 this
, 闭包没法跟 class 一起协作把私有域共享给整个 class 而不暴露到 class 外部,而且闭包实现的私有是拥有者私有制,你无法用闭包私有实现上述的 compare 函数。
Typescript 中的 private 是编译阶段的 private,它的缺点是运行时对私有成员是不知情的,甚至你如果愿意作死,一招 as any
就可以让你破除这道屏障,底裤都可以扒出来。我不是说 TS 中的私有成员不好,相反其实我更支持一般的项目用 TS 的 private 就够了,其他静态语言还能通过反射之类的操作拿到私有成员呢。我是说你要知道,它确实就是这么一回事。
缺点
ES class 的私有成员其实带来了很多心智负担,普通的属性如果不存在顶多返回一个 undefined,而私有成员直接给你抛运行时异常。
有人说,你上 Typescript 啊,TS 也支持了 ES class 的私有成员。上 TS 的话, TS 本身的 private 绝大部分情况下就够用了,为什么劳驾 ES class 私有成员?
真的觉得运行时私有很重要?那先练练手?
private 试炼
1. Duck Type?
class A {
#p = 1;
}
class B {
#p = 2;
getP(){return this.#p;}
}
const a = new A();
const b = new B();
b.getP.apply(a); //报错么?
2. instance of 能否保障??
class C {
#p = 1;
getP() {
if(this instanceof C) {
return this.#p;
}
}
}
const c = new C();
const d = Object.create(c);
d.getP(); // Boom 不 Boom ??
3. class 是 prototype 的语法糖,所以,这种操作呢?
class D {
#p = 1;
}
D.prototype.getP = function() { return this.#p; };
const d = new D();
d.getp();
4.搭配继承服用呢?
class Parent {
#p = 1;
q = 1;
getP() { return this.#p; }
getQ() { return this.q; }
}
class Son extends Parent{
#p = 2;
q = 2;
SON_getP() { return this.#p; }
SON_getQ() { return this.q; }
}
const son = new Son();
console.log(son.getP()); // 1 or 2?
console.log(son.getQ()); // 1 or 2?
console.log(son.SON_getP()); // 1 or 2?
console.log(son.SON_getQ()); // 1 or 2?
5. 内联 class
class Outer {
#p = 1;
get m() {
const outer = this;
return new class Inner {
getP() {return outer.#p;}
}
}
}
const o = new Outer();
o.m.getP(); // error?
这个呢?
class Outer {
#p = 1;
get m() {
const outer = this;
return new class Inner {
#p = 2;
getP() {return outer.#p;}
}
}
}
const o = new Outer();
o.m.getP(); // error?
6. 配合 Proxy
// 先构造一个 logging handler 拦截所有的 property 访问并 log 记录
const logging = {
get: function (target, prop, receiver) {
console.log(`accessing prop: ${prop}`);
return Reflect.get(...arguments);
},
};
const proxy1 = new Proxy({a:1, b: 2, c() {return this.a + this.b}}, logging);
proxy1.a; // 打印 accessing prop: a
proxy1.b; // 打印 accessing prop: b
proxy1.c(); // 没问题
// 目前一切正常, 直到。。。
class S {
#p = 1;
getP() {return this.#p;}
}
const s = new S();
const proxy2 = new Proxy(s, logging);
proxy2.getP(); //你猜?
本文使用 mdnice 排版