It is well known that JavaScript object properties can be accessed and modified externally by default; that is, JavaScript itself has no completely “private” object properties. Such as:
class Point{
constructor(x, y){
this._x = x;
this._y = y;
}
get length() {const {_x, _y} = this;
return Math.sqrt(_x * _x + _y * _y); }}let p = new Point(3.4);
console.log(p._x, p._y, p.length); / / 3, 4, 5
Copy the code
In the above code, we conventionally begin with an underscore to indicate private variables. We want _x and _y not to be accessed externally. However, this is just wishful thinking. Users can still access these two variables.
We will not discuss the PRIVATE standard proposal for ES here, but rather how to use tools to make conventions truly private.
Use symbols to construct private data
ES6 provides a new data type called Symbol, which has a number of uses. One of the uses of Symbol is that it can be used to generate unique keys that can be used as attribute identifiers. We can use it to implement truly private attributes:
const [_x, _y] = [Symbol('_x'), Symbol('_y')];
class Point{
constructor(x, y){
this[_x] = x;
this[_y] = y;
}
get length() {const x = this[_x],
y = this[_y];
return Math.sqrt(x * x + y * y); }}let p = new Point(3.4);
console.log(p._x, p._y, p.length); //undefined, undefined, 5
Copy the code
We rewrite the previous version of the code, use Symbol _x, _y instead of string as key, so that external P access to _x, _Y attributes can not be accessed, so we really realize the object data private.
The above usage is not complicated, but it would still be troublesome if we wrote it this way every time we defined an object. Therefore, we can consider asking the compiler to do this and automatically compile properties that begin with an underscore to private properties.
Use the Babel plug-in to make properties private by default
Here, we can develop plug-ins for Babel to do this. The principles of Babel have been described in previous posts on this blog. There are also examples of using the Babel plug-in for test coverage checking. For those of you unfamiliar with Babel, review the previous article.
First, let’s examine the PART of the AST that we want to process. ES6 classes have two node types, the ClassDeclaration and ClassExpression. They are similar, but they differ in some details. For example, ReturnStatement can be followed by ClassExpression but not ClassDeclaration.
ClassDeclaration and ClassExpression
//ClassDeclaration
class Foo{
/ /...
}
//classExpression
const Bar = class MyClass{
/ /...
}
Copy the code
For both types of Node, if there are attributes beginning with an underscore, they can be compiled as follows:
const Foo = function(a){
[...fields] = [...Symbol(...)]
class Foo {
/ /...
}
returnFoo; } ();const Bar = function(a){
[...fields] = [...Symbol(...)]
return class MyClass{
/ /...
}
}();
Copy the code
In addition, we need to consider the case of ES Modules:
export class Foo{
/ /...
}
Copy the code
Corresponding to:
export const Foo = function(){
/ /...} ();Copy the code
There’s nothing wrong with the form above. But if:
export default class Foo{
/ /...
}
Copy the code
Corresponding to:
export default const Foo = function(){
/ /...} ();Copy the code
Compilation will report an error. Therefore, it should be modified, corresponding to:
const Foo = function(){
/ /...} ();export default Foo;
Copy the code
Since classes allow nesting, we need to use the stack structure to create a list of private properties under the scope of the current Class at the AST’s Enter. Another function of the stack is that if the stack is empty, the scope is not currently inside the Class and no compilation conversion is required.
ClassDeclaration: {
exit(path){
let expr = transformWrapClass(path.node);
if(! expr)return;
if(path.parentPath.node.type === 'ExportDefaultDeclaration') {// Handle the special case of export default
path.parentPath.insertAfter(t.exportDefaultDeclaration(
t.identifier(path.node.id.name)
));
path.parentPath.replaceWith(expr);
}else{
// Replace the current path
path.replaceWith(expr);
}
path.skip();
},
enter(path, state){
// Create a stack to store private variable identifiers
stack.push({
variables: new Set()}); }},ClassExpression: {
exit(path){
let expr = transformWrapClass(path.node);
if(! expr)return;
//ClassExpression can directly export default
path.replaceWith(expr);
path.skip();
},
enter(path, state){
stack.push({
variables: new Set()}); }}Copy the code
Next, we deal with specific identifiers:
Identifier(path, state) {
if(stack.length <= 0) return; // Not in class scope, returns directly
if($/ / ^ __. * __.test(path.node.name)) return; // The system reserves attributes, such as __proto__
let node = path.node,
parentNode = path.parentPath.node,
meta = stack[stack.length - 1];
let regExp = new RegExp(state.opts.pattern || '^ _');
// Add a suffix to the attribute name to avoid internal use of the same name
// let _x = this._x;
let symbolName = '$' + node.name + '$';
if(parentNode
&& parentNode.type === 'MemberExpression'
&& parentNode.object.type === 'ThisExpression'
&& !parentNode.computed
&& regExp.test(node.name)){ //private
// For private attributes read and write this._x, just replace this[_x]
// Add the current variable identifier to Set at the top of the stack
node.name = symbolName;
meta.variables.add(node.name);
parentNode.computed = true;
}else if(parentNode
&& parentNode.type === 'MemberExpression'
&& parentNode.object.type === 'Super'
&& !parentNode.computed
&& regExp.test(node.name)){
// Use super._x to access the attributes of the parent element and perform a transformation
node.name = symbolName;
parentNode.computed = true;
let expr = transformPropertyToSymbol(node.name);
path.replaceWith(expr);
path.skip();
}else if(parentNode
&& parentNode.type === 'ClassMethod'
&& regExp.test(node.name)){
// Handle class methods and getters and setters with underscores.
node.name = symbolName;
meta.variables.add(node.name);
parentNode.computed = true; }},Copy the code
Protected properties and the super._x operation
In the case of object methods underlined, unlike this underlined, we can use super. Property name to access. Such as:
class Foo{
constructor(x) {
this._x = x;
}
// This is a protected property, accessible in derived classes through super._x
get _X() {return this._x; }}class Bar extends Foo{
constructor(x, y){
super(x);
this._y = y;
}
get XY() {return [super._X, this._y]; }}let bar = new Bar(3.4);
console.log(bar.XY); / / [3, 4]
Copy the code
Here, we need to handle super._x, if compiled directly:
const Foo = function(){
const [$_x$, $_X$] = [Symbol('$_x$'), Symbol('$_X$')];
class Foo{
constructor(x) {
this[$_x$] = x;
}
// This is a protected property, accessible in derived classes through super._x
get [$_X$](){
return this[$_x$]; }}returnFoo; } ();const Bar = function(){
const [$_y$, $_X$] = [Symbol('$_y$'), Symbol('$_X$')];
class Bar extends Foo{
constructor(x, y){
super(x);
this[$_y$] = y;
}
get XY() {return [super[$_X$], this[$_y$]]; }}returnBar; } ();let bar = new Bar(3.4);
console.log(bar.XY); //[undefined, 4]
Copy the code
Since each Symbol is unique, Bar’s Symbol(‘ X_XX’) is not the same as Foo’s, and the actual value of super[X_XX] is not obtained.
In this case, we do not convert the Symbol to Symbol directly, but use reflection mechanism to handle:
const Foo = function(){
const [$_x$, $_X$] = [Symbol('$_x$'), Symbol('$_X$')];
class Foo{
constructor(x) {
this[$_x$] = x;
}
// This is a protected property, accessible in derived classes through super._x
get [$_X$](){
return this[$_x$]; }}returnFoo; } ();const Bar = function(){
const [$_y$] = [Symbol('$_y$')];
class Bar extends Foo{
constructor(x, y){
super(x);
this[$_y$] = y;
}
get XY() {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]
Copy the code
There is a long list of keys in super:
Object.getOwnPropertySymbols(this.__proto__.__proto__)
.filter(s= > String(s) === "Symbol($_X$)") [0]
Copy the code
Here by the Object. GetOwnPropertySymbols (this. __proto__. __proto__) reflects the Symbol of the parent, and then through the string matching to the corresponding key.
So, we define the transformation method, so it’s just a matter of implementing the transformation details:
function transformCreateSymbols(){
let meta = stack.pop(),
variableNames = Array.from(meta.variables);
//no private variables
if(variableNames.length <= 0) return;
let identifiers = variableNames.map(id= > t.identifier(id));
let pattern = t.arrayPattern(identifiers);
let symbols = variableNames.map(id= >
t.callExpression(t.identifier('Symbol'), [t.stringLiteral(id)]));
symbols = t.arrayExpression(symbols);
return t.variableDeclaration(
'const',
[t.variableDeclarator(pattern, symbols)]
);
}
function transformWrapClass(cls){
let symbols = transformCreateSymbols();
if(! symbols)return;
if(cls.type === 'ClassDeclaration') {let expr = t.callExpression(
t.functionExpression(null, [],
t.blockStatement(
[symbols,
cls,
t.returnStatement(
t.identifier(cls.id.name)
)]
)
), []
);
return t.variableDeclaration(
'const',
[t.variableDeclarator(
t.identifier(cls.id.name),
expr
)]
);
}else if(cls.type === 'ClassExpression') {return t.callExpression(
t.functionExpression(null, [], t.blockStatement( [symbols, t.returnStatement( cls )] ) ), [] ); }}Copy the code
The above method completes the ClassDeclaration and ClassExpression processing. Next comes the part that deals with the super attribute:
function transformPropertyToSymbol(name){
let expr = 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
);
return expr;
}
Copy the code
The above code is verbose, but not complex, just building an AST tree. Finally, we have the complete plug-in code. Those who are interested can follow this GitHub repo.
To use, install directly:
npm i babel-plugin-transform-private --save-dev
Copy the code
Then configure:
{
"plugins": [["transform-private", {
"pattern": "^ _"}}]]Copy the code
The pattern parameter can be configured to modify the matching regular expression of private variables. The default value is’ ^_ ‘, which starts with an underscore, and can be changed to another pattern.
That’s all for today, a lot of code, but that’s the key, and the rest is the process of building the AST tree. If you have any questions, welcome to discuss.