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.