使用 babel 插件来打造真正的“ 私有” 属性大家都知道 JavaScript 的对象属性默认是可以被从外部访问和修改的, 也就是说, JavaScript 本身不存在完全“ 私有” 的对象属性。 例如: class Point {
constructor(x,y){
this._x=x;
this._y=y;
}
getlength(){
const{
_x,
_y
}=this;
returnMath.sqrt(_x*_x+_y*_y);
}
}
let p = new Point(3, 4);
console.log(p._x, p._y, p.length);
3, 4, 5 1 2 3 4 5 6 7 8 9 10 11 12 13 class Point {
constructor(x,y){
this._x=x;
this._y=y;
}
getlength(){
const{
_x,
_y
}=this;
returnMath.sqrt(_x*_x+_y*_y);
}
}
let p = new Point(3, 4);
console.log(p._x, p._y, p.length);
3, 4, 5 在上面的代码里, 我们约定俗成地用下划线开头来表示私有变量。 我们希望 _x、 _y 不被外部访问, 然而, 这只是我们一厢情愿, 使用者还是可以访问到这两个变量。 在这里, 我们不讨论 ES 的 private 标准提案, 而是讨论如何使用工具来将约定变成真正的私有。 使用 Symbol 来构造私有数据ES6 提供了一个新的数据类型叫做 Symbol, Symbol 有许多用途, 其中一个用途是可以用来生成唯一 key, 用作属性标识, 我们利用它可以实现真正的私有属性:
const [_x, _y] = [Symbol(‘_x’), Symbol(‘_y’)];
class Point {
constructor(x,y){
this[_x] =x;
this[_y] =y;
}
getlength(){
constx=this[_x],
y=this[_y];
returnMath.sqrt(x*x+y*y);
}
}
let p = new Point(3, 4);
console.log(p._x, p._y, p.length);
undefined, undefined, 5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
const [_x, _y] = [Symbol(‘_x’), Symbol(‘_y’)];
class Point {
constructor(x,y){
this[_x] =x;
this[_y] =y;
}
getlength(){
constx=this[_x],
y=this[_y];
returnMath.sqrt(x*x+y*y);
}
}
let p = new Point(3, 4);
console.log(p._x, p._y, p.length);
undefined, undefined, 5 我们改写上一版的代码, 用 Symbol 的 _x、 _y 代替字符串来作为 key, 这样, 外部 p 访问 _x、 _y 属性就访问不到了, 这样我们就真正实现了对象数据的私有。 上面这种用法并不复杂, 但是, 如果我们每次定义对象都这么去写还是显得麻烦。 因此, 我们可以考虑让编译器去做这件事情, 自动将下划线开头的属性编译成私有属性。 使用 Babel 插件来实现属性的默认私有在这里, 我们可以开发 Babel 的插件来实现。 Babel 的原理在博客之前的文章中有介绍。 还有使用 Babel 插件来进行测试覆盖度检查的例子。 如果对于 Babel 不熟悉的同学, 可以回顾一下之前的文章。 首先, 我们分析一下要处理的 AST 部分。 ES6 的 class 有两种 node 类型, 一种是 ClassDeclaration, 另一种是 ClassExpression。 它们比较类似, 但是在一些细节上又有区别。 比如 ReturnStatement 之后可以跟 ClassExpression 但是不能跟 ClassDeclaration。 ClassDeclaration 与 ClassExpressionClassDeclaration class Foo { …
}
classExpression
const Bar = class MyClass { …
}
1 2 3 4 5 6 7 8 9 ClassDeclaration class Foo { …
}
classExpression
const Bar = class MyClass { …
}
对这两种 node, 如果其中有下划线开头的属性, 可以分别编译成如下形式:
const Foo = function () {
[…fields] = […Symbol(…)] class Foo { …
}
returnFoo;
}();
const Bar = function () {
[…fields] = […Symbol(…)]
returnclassMyClass{…
}
}();
1 2 3 4 5 6 7 8 9 10 11 12 13 14
const Foo = function () {
[…fields] = […Symbol(…)] class Foo { …
}
returnFoo;
}();
const Bar = function () {
[…fields] = […Symbol(…)]
returnclassMyClass{…
}
}();
此外, 还需要考虑 ES Modules 的情况:
export class Foo { …
}
1 2 3
export class Foo { …
}
对应为:
export const Foo = function () { …
}();
1 2 3
export const Foo = function () { …
}();
上面的形式没有问题。 但是如果:
export default class Foo { …
}
1 2 3
export default class Foo { …
}
对应为:
export default
const Foo = function () { …
}();
1 2 3
export default
const Foo = function () { …
}();
编译会报错。 因此要进行修改, 对应成:
const Foo = function () { …
}();
export default Foo;
1 2 3 4
const Foo = function () { …
}();
export default Foo;
由于 Class 允许存在嵌套, 因此, 我们需要使用堆栈结构, 在 AST 的 enter 的时候创建存放当前 Class 的 scope 下的私有属性列表。 堆栈还有一个作用, 就是如果堆栈为空, 那么当前作用域不在 Class 内部, 不用进行编译转换。 ClassDeclaration: {
exit(path) {
letexpr=transformWrapClass(path.node);
if (!expr) return;
if (path.parentPath.node.type ===’ExportDefaultDeclaration’) {
处理
exportdefault的特殊情况path.parentPath.insertAfter(t.exportDefaultDeclaration(t.identifier(path.node.id.name)));
path.parentPath.replaceWith(expr);
}else{
替换掉当前pathpath.replaceWith(expr);
}
path.skip();
},
enter(path,state) {
创建存放私有变量标识符的堆栈stack.push({
variables: new Set()
});
}
}, ClassExpression: {
exit(path) {
letexpr=transformWrapClass(path.node);
if (!expr) return;
ClassExpression可以直接
exportdefaultpath.replaceWith(expr);
path.skip();
},
enter(path,state) {
stack.push({
variables: new Set()
});
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 ClassDeclaration: {
exit(path) {
letexpr=transformWrapClass(path.node);
if (!expr) return;
if (path.parentPath.node.type ===’ExportDefaultDeclaration’) {
处理
exportdefault的特殊情况path.parentPath.insertAfter(t.exportDefaultDeclaration(t.identifier(path.node.id.name)));
path.parentPath.replaceWith(expr);
}else{
替换掉当前pathpath.replaceWith(expr);
}
path.skip();
},
enter(path,state) {
创建存放私有变量标识符的堆栈stack.push({
variables: new Set()
});
}
}, ClassExpression: {
exit(path) {
letexpr=transformWrapClass(path.node);
if (!expr) return;
ClassExpression可以直接
exportdefaultpath.replaceWith(expr);
path.skip();
},
enter(path,state) {
stack.push({
variables: new Set()
});
}
}
接下来, 我们处理具体的 Identifier: Identifier(path, state) {
if (stack.length 0) return;
不在class作用域内, 直接返回
if (/^__.*__$/.test(path.node.name)) return;
系统保留属性, 比如__proto__
letnode = path.node,
parentNode = path.parentPath.node,
meta = stack[stack.length – 1];
letregExp = newRegExp(state.opts.pattern || ‘^_’);
给属性名增加前缀后缀, 避免内部使用时出现重名比如应当允许
let_x = this._x;
letsymbolName = ‘$’ + node.name + ‘$’;
if (parentNode && parentNode.type === ‘MemberExpression’ && parentNode.object.type === ‘ThisExpression’ && !parentNode.computed && regExp.test(node.name)) {
private对于私有属性的读写this._x, 直接替换成this[_x] 并且记录下当前变量标识符, 添加到栈顶的Set中去node.name =symbolName;
meta.variables.add(node.name);
parentNode.computed=true;
}elseif (parentNode&&parentNode.type ===’MemberExpression’&&parentNode.object.type ===’Super’&&!parentNode.computed&®Exp.test(node.name)) {
使用super._x访问父元素的属性, 进行一个变换node.name =symbolName;
parentNode.computed=true;
letexpr=transformPropertyToSymbol(node.name);
path.replaceWith(expr);
path.skip();
}elseif (parentNode&&parentNode.type ===’ClassMethod’&®Exp.test(node.name)) {
处理class的方法和getter、 setter名带下划线的情况。 node.name = symbolName;
meta.variables.add(node.name);
parentNode.computed = true;
}
}, 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 Identifier(path, state) {
if (stack.length 0) return;
不在class作用域内, 直接返回
if (/ ^ __ . * __$ /.test(path.node.name)) return;
系统保留属性, 比如__proto__
letnode = path.node,
parentNode = path.parentPath.node,
meta = stack[stack.length – 1];
letregExp = newRegExp(state.opts.pattern || ‘^_’);
给属性名增加前缀后缀, 避免内部使用时出现重名比如应当允许
let_x = this._x;
letsymbolName = ‘$’ + node.name + ‘$’;
if (parentNode && parentNode.type === ‘MemberExpression’ && parentNode.object.type === ‘ThisExpression’ && !parentNode.computed && regExp.test(node.name)) {
private对于私有属性的读写this._x, 直接替换成this[_x] 并且记录下当前变量标识符, 添加到栈顶的Set中去node.name =symbolName;
meta.variables.add(node.name);
parentNode.computed=true;
}elseif (parentNode&&parentNode.type ===’MemberExpression’&&parentNode.object.type ===’Super’&&!parentNode.computed&®Exp.test(node.name)) {
使用super._x访问父元素的属性, 进行一个变换node.name =symbolName;
parentNode.computed=true;
letexpr=transformPropertyToSymbol(node.name);
path.replaceWith(expr);
path.skip();
}elseif (parentNode&&parentNode.type ===’ClassMethod’&®Exp.test(node.name)) {
处理class的方法和getter、 setter名带下划线的情况。 node.name = symbolName;
meta.variables.add(node.name);
parentNode.computed = true;
}
}, Protected 的属性和 super._x 操作对于对象方法带下划线的情况, 和 this 带下划线不同, 我们是可以使用 super.属性名 来访问的。 比如: class Foo {
constructor(x){
this._x=x;
}
这是一个protected的属性, 在派生类中可以通过super._X访问get_X(){
returnthis._x;
}
}
class Bar extends Foo {
constructor(x,y){
super(x);
this._y=y;
}
getXY(){
return [super._X,this._y];
}
}
let bar = new Bar(3, 4);
console.log(bar.XY);
[3, 4] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class Foo {
constructor(x){
this._x=x;
}
这是一个protected的属性, 在派生类中可以通过super._X访问get_X(){
returnthis._x;
}
}
class Bar extends Foo {
constructor(x,y){
super(x);
this._y=y;
}
getXY(){
return [super._X,this._y];
}
}
let bar = new Bar(3, 4);
console.log(bar.XY);
[3, 4] 在这里, 我们需要对 super._X 进行处理, 如果直接编译:
const Foo = function () {
const[$_x$,$_X$]= [Symbol(‘$_x$’),Symbol(‘$_X$’)];
classFoo{
constructor(x){
this[$_x$] =x;
}
这是一个protected的属性, 在派生类中可以通过super._X访问get[$_X$](){
returnthis[$_x$];
}
}
returnFoo;
}();
const Bar = function () {
const[$_y$,$_X$]= [Symbol(‘$_y$’),Symbol(‘$_X$’)];
classBarextendsFoo{
constructor(x,y){
super(x);
this[$_y$] =y;
}
getXY(){
return [super[$_X$],this[$_y$]];
}
}
returnBar;
}();
let bar = new Bar(3, 4);
console.log(bar.XY);
[undefined, 4] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
const Foo = function () {
const[$_x$,$_X$]= [Symbol(‘$_x$’),Symbol(‘$_X$’)];
classFoo{
constructor(x){
this[$_x$] =x;
}
这是一个protected的属性, 在派生类中可以通过super._X访问get[$_X$](){
returnthis[$_x$];
}
}
returnFoo;
}();
const Bar = function () {
const[$_y$,$_X$]= [Symbol(‘$_y$’),Symbol(‘$_X$’)];
classBarextendsFoo{
constructor(x,y){
super(x);
this[$_y$] =y;
}
getXY(){
return [super[$_X$],this[$_y$]];
}
}
returnBar;
}();
let bar = new Bar(3, 4);
console.log(bar.XY);
[undefined, 4] 由于每个 Symbol 都是唯一的, 所以 Bar 的 Symbol(‘$_X$’) 和 Foo 的并不相同, 这样也就获取不到 super[$_X$] 实际的值了。 因此, 在这里, 我们编译的时候不能直接这样转成 Symbol, 而是要通过反射机制去处理:
const Foo = function () {
const[$_x$,$_X$]= [Symbol(‘$_x$’),Symbol(‘$_X$’)];
classFoo{
constructor(x){
this[$_x$] =x;
}
这是一个protected的属性, 在派生类中可以通过super._X访问get[$_X$](){
returnthis[$_x$];
}
}
returnFoo;
}();
const Bar = function () {
const[$_y$]= [Symbol(‘$_y$’)];
classBarextendsFoo{
constructor(x,y){
super(x);
this[$_y$] =y;
}
getXY(){
return [super[Object.getOwnPropertySymbols(this.__proto__.__proto__).filter(s=>String(s) ===”Symbol($_X$)”)[0]],this[$_y$]];
}
}
returnBar;
}();
let bar = new Bar(3, 4);
console.log(bar.XY);
[3, 4] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
const Foo = function () {
const[$_x$,$_X$]= [Symbol(‘$_x$’),Symbol(‘$_X$’)];
classFoo{
constructor(x){
this[$_x$] =x;
}
这是一个protected的属性, 在派生类中可以通过super._X访问get[$_X$](){
returnthis[$_x$];
}
}
returnFoo;
}();
const Bar = function () {
const[$_y$]= [Symbol(‘$_y$’)];
classBarextendsFoo{
constructor(x,y){
super(x);
this[$_y$] =y;
}
getXY(){
return [super[Object.getOwnPropertySymbols(this.__proto__.__proto__).filter(s=>String(s) ===”Symbol($_X$)”)[0]],this[$_y$]];
}
}
returnBar;
}();
let bar = new Bar(3, 4);
console.log(bar.XY);
[3, 4] 上面的 super 里的 key 有一大串, 是: Object.getOwnPropertySymbols(this.__proto__.__proto__).filter(s => String(s) === “Symbol($_X$)”)[0] 1 2 Object.getOwnPropertySymbols(this.__proto__.__proto__).filter(s = > String(s) === “Symbol($_X$)”)[0] 这里通过 Object.getOwnPropertySymbols(this.__proto__.__proto__) 反射出父类的 Symbol, 然后通过字符串匹配到对应的 key。 于是, 我们确定了转换方法, 那么接下来就只是实现具体的转换细节了:
function transformCreateSymbols() {
letmeta=stack.pop(),
variableNames=Array.from(meta.variables);
noprivatevariables
if (variableNames.length 0) return;
letidentifiers=variableNames.map(id=>t.identifier(id));
letpattern=t.arrayPattern(identifiers);
letsymbols=variableNames.map(id=>t.callExpression(t.identifier(‘Symbol’), [t.stringLiteral(id)]));
symbols=t.arrayExpression(symbols);
returnt.variableDeclaration(‘const’, [t.variableDeclarator(pattern,symbols)]);
}
function transformWrapClass(cls) {
letsymbols=transformCreateSymbols();
if (!symbols) return;
if (cls.type ===’ClassDeclaration’) {
letexpr=t.callExpression(t.functionExpression(null, [],t.blockStatement([symbols,cls,t.returnStatement(t.identifier(cls.id.name))])), []);
returnt.variableDeclaration(‘const’, [t.variableDeclarator(t.identifier(cls.id.name),expr)]);
}elseif (cls.type ===’ClassExpression’) {
returnt.callExpression(t.functionExpression(null, [],t.blockStatement([symbols,t.returnStatement(cls)])), []);
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
function transformCreateSymbols() {
letmeta=stack.pop(),
variableNames=Array.from(meta.variables);
noprivatevariables
if (variableNames.length 0) return;
letidentifiers=variableNames.map(id=>t.identifier(id));
letpattern=t.arrayPattern(identifiers);
letsymbols=variableNames.map(id=>t.callExpression(t.identifier(‘Symbol’), [t.stringLiteral(id)]));
symbols=t.arrayExpression(symbols);
returnt.variableDeclaration(‘const’, [t.variableDeclarator(pattern,symbols)]);
}
function transformWrapClass(cls) {
letsymbols=transformCreateSymbols();
if (!symbols) return;
if (cls.type ===’ClassDeclaration’) {
letexpr=t.callExpression(t.functionExpression(null, [],t.blockStatement([symbols,cls,t.returnStatement(t.identifier(cls.id.name))])), []);
returnt.variableDeclaration(‘const’, [t.variableDeclarator(t.identifier(cls.id.name),expr)]);
}elseif (cls.type ===’ClassExpression’) {
returnt.callExpression(t.functionExpression(null, [],t.blockStatement([symbols,t.returnStatement(cls)])), []);
}
}
上面的方法将 ClassDeclaration 和 ClassExpression 处理完成。 接下来是处理 super 属性的部分:
function transformPropertyToSymbol(name) {
letexpr=t.callExpression(t.memberExpression(t.identifier(‘Object’),t.identifier(‘getOwnPropertySymbols’)), [t.memberExpression(t.memberExpression(t.thisExpression(),t.identifier(‘__proto__’)),t.identifier(‘__proto__’))]);
expr=t.callExpression(t.memberExpression(expr,t.identifier(‘filter’)), [t.arrowFunctionExpression([t.identifier(‘s’)],t.binaryExpression(‘===’,t.callExpression(t.identifier(‘String’), [t.identifier(‘s’)]),t.stringLiteral(`Symbol(${name})`)))]);
expr=t.memberExpression(expr,t.numericLiteral(0),true);
returnexpr;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
function transformPropertyToSymbol(name) {
letexpr=t.callExpression(t.memberExpression(t.identifier(‘Object’),t.identifier(‘getOwnPropertySymbols’)), [t.memberExpression(t.memberExpression(t.thisExpression(),t.identifier(‘__proto__’)),t.identifier(‘__proto__’))]);
expr=t.callExpression(t.memberExpression(expr,t.identifier(‘filter’)), [t.arrowFunctionExpression([t.identifier(‘s’)],t.binaryExpression(‘===’,t.callExpression(t.identifier(‘String’), [t.identifier(‘s’)]),t.stringLiteral(` Symbol ( $ { name } ) `)))]);
expr=t.memberExpression(expr,t.numericLiteral(0),true);
returnexpr;
}
上面代码虽然繁琐, 但都并不复杂, 只是 AST 树的构建而已。 最终, 我们形成完整的插件代码。 有兴趣的同学可以关注这个 GitHub repo。 要使用的话, 直接安装: npm i babel – plugin – transform – private–save – dev 1 npm i babel – plugin – transform – private–save – dev然后配置一下: {
“plugins”: [
[“transform-private”, {
“pattern”:”^_”
}],
]
}
1 2 3 4 5 6 7 {
“plugins”: [
[“transform-private”, {
“pattern”:”^_”
}],
]
}
其中配置的 pattern 参数可以修改私有变量的匹配正则表达式, 默认是 `”^_” 也就是以下划线开头,可以改成别的模式。以上就是今天的全部内容,代码比较多,但是关键点就这些,其他就是构建 AST 树的过程。如有任何问题,欢迎讨论。
评论前必须登录!
注册