javascript - 标记模板字符串和闭包的问题

标签 javascript closures template-strings

在标记模板文字中调用的

Symbol.toPrimitive 方法将失去对闭包的访问权限。

要重现,只需将提供的代码片段粘贴到开发控制台中,使用或不使用标签功能运行它。任何相关文章都将受到高度赞赏。

附注如果您能告诉我如何以及在何处调试 js 代码(包括 node.js),我也将不胜感激。我对词法环境、执行上下文和调用堆栈感兴趣。

const isEmptyString = /^\s*$/;

class Thread {
  constructor() {
    this.scope = {
      current: '/test|0::0'
    };

    this.context = {
      current: '/test|0'
    };

    this.html = (strings, ...interpolations) => {
      var output = '';
      var prevMode = this._mode;

      this._mode = 'html';

      var {
        length
      } = interpolations;
      output += strings[0]

      for (let i = 0; i < length; ++i) {
        output += String(interpolations[i]) + strings[i + 1];
      }

      this._mode = prevMode;
      return output;
    };
  }


  get id() {
    var fragment;

    const scope = this.scope.current;
    const context = this.context.current;

    return Object.defineProperties(function self(newFragment) {
      fragment = newFragment;
      return self;
    }, {
      scope: {
        get() {
          return scope
        }
      },
      context: {
        get() {
          return context
        }
      },
      fragment: {
        get() {
          return fragment
        }
      },

      [Symbol.toPrimitive]: {
        value: hint => {
          console.log('::', fragment, '::');
          const isFragmentDefined = !isEmptyString.test(fragment);

          const quote = isFragmentDefined ? '\'' : '';
          const suffix = isFragmentDefined ? `::${fragment}` : '';

          if (isFragmentDefined) fragment = '';

          switch (true) {
            case this._mode === 'html':
              return `node=${quote}${scope}${suffix}${quote}`;
            case this._mode === 'css':
              return `${context}${suffix}`.replace(invalidCSS, char => `\\${char}`);

            default:
              return `${scope}${suffix}`;
          }
        }
      }
    });
  }
}

let thread = new Thread();



async function article() {
  let {
    id,
    html
  } = thread;

  let links = html `
    <ul>
      <li ${id('C-first-id')}></li>
      <li ${id('C-second-id')}></li>
      <li ${id('C-third-id')}></li>
      <li ${id('C-fourth-id')}></li>
    </ul>
  `;

  return html `
    <article>
      <h1 ${id('B-first-id')}>Some header</h1>
      <p ${id('B-second-id')}>Lorem ipsum...</p>
      <p ${id('B-third-id')}>Lorem ipsum...</p>
      <p ${id('B-fourth-id')}>Lorem ipsum...</p>

      <section>
        ${links}
      </section>
    </article>
  `;
}

async function content() {
  let {
    id,
    html
  } = thread;

  return html `
    <main>
      <div>
        <h1 ${id('A-first-id')}>Last article</h1>
        
        
        <div>
          <a href='#' ${id('A-second-id')}>More articles like this</a>
          ${await article()}
          <a href='#' ${id('A-third-id')}>Something else...</a>
          <a href='#' ${id('A-fourth-id')}>Something else...</a>
        </div>
      </div>
    </main>
  `;
}

content();

最佳答案

我不确定我是否理解你的意思。

<小时/>

在下面的一些评论之后,人们对“使用和不使用标签功能运行它”的含义感到困惑:

let { id, html } = thread;

console.log("Without tag function", `${id("A-first-id")}${id("A-second-id")}${id("A-third-id")}`);
console.log("With tag function", html`${id("A-first-id")}${id("A-second-id")}${id("A-third-id")}`);

结果是:

Without tag function /test|0::0::A-first-id/test|0::0::A-second-id/test|0::0::A-third-id
With tag function node='/test|0::0::A-third-id'node=/test|0::0node=/test|0::0

区别在于,如果没有标签功能,它会按预期工作,并且结果中会出现“A-first-id”、“A-second-id”和“A-third-id”。使用tag-function时,仅存在“A-third-id”(且格式也不同)。

问题是为什么“A-first-id”和“A-second-id”在与标签功能一起使用时会丢失。

<小时/>

但是我注意到每次调用id时都会覆盖fragment,并且稍后会调用Symbol.toPrimitive中的代码。这就是为什么您只获得最后一个字符串 "[ABC]-fourth-id" 并使用 if (isFragmentDefined)fragment = '';

"use strict";

class Thread {
  constructor() {
    this.html = (strings, ...interpolations) => {
      var output = '';
      var {
        length
      } = interpolations;
      output += strings[0]

      for (let i = 0; i < length; ++i) {
        output += String(interpolations[i]) + strings[i + 1];
      }

      return output;
    };
  }


  get id() {
    var fragment;

    return Object.defineProperties(function self(newFragment) {
      console.log("fragment new '%s' old '%s'", newFragment, fragment);
      fragment = newFragment; // overwrite fragment
      return self;
    }, {
      [Symbol.toPrimitive]: {
        value: hint => {
          // this is called later, fragment is the last value
          console.log("toPrimitive", fragment);
          return fragment;
        }
      }
    });
  }
}

let thread = new Thread();

async function content() {
  let {
    id,
    html
  } = thread;

  return html `
    ${id('A-first-id')}
    ${id('A-second-id')}
    ${id('A-third-id')}
    ${id('A-fourth-id')}
  `;
}

content().then(x => console.log(x));

运行上面的代码,你会得到:

fragment new 'A-first-id' old 'undefined'
fragment new 'A-second-id' old 'A-first-id'
fragment new 'A-third-id' old 'A-second-id'
fragment new 'A-fourth-id' old 'A-third-id'
toPrimitive A-fourth-id
toPrimitive A-fourth-id
toPrimitive A-fourth-id
toPrimitive A-fourth-id

  A-fourth-id
  A-fourth-id
  A-fourth-id
  A-fourth-id

因此,首先,字符串中每次出现时都会调用 id 中的代码,每次都会覆盖 fragment 。之后,调用toPrimitive,它只有最后一个片段集:“A-fourth-id”

我很确定这不是您想要的。

我认为你想要:

fragment new 'A-first-id' old 'undefined'
fragment new 'A-second-id' old 'A-first-id'
fragment new 'A-third-id' old 'A-second-id'
fragment new 'A-fourth-id' old 'A-third-id'
toPrimitive A-first-id
toPrimitive A-second-id
toPrimitive A-third-id
toPrimitive A-fourth-id

  A-first-id
  A-second-id
  A-third-id
  A-fourth-id

真正的错误是...

当我再次查看代码并试图解释为什么片段被覆盖时,我突然意识到:你将 id 定义为 getter。所以当你这样做时:

let { id, html } = thread;

你实际上是在调用id中的代码,并且你得到了这个函数。因此,每次您在字符串中使用 id 时,它都会对相同的片段使用相同的函数。

解决方案?重构您的代码,使 id 不再是 getter。

当您从对象中解构函数时,函数不再了解上下文。您可以通过在构造函数中绑定(bind)函数来解决这个问题:

class MyClass {
  constructor() {


    // Bind this to some functions
    for (const name of ['one', 'two'])
      this[name] = this[name].bind(this);
  }
  one(value) {
    return this.two(value).toString(16);
  }
  two(value) {
    return value * 2;
  }
}

const my = new MyClass();
const {one, two} = my;
console.log(one(1000)); // Works since `one` was bound in the constructor 

调试:

更新

模板字符串的标签函数只是将参数传递给函数的语法糖。

let { id, html } = thread;

// A tag function is just syntactic sugar:
html`${id("A-first-id")}${id("A-second-id")}${id("A-third-id")}`;

// for this:    
html(["", "", "", ""], id("A-first-id"), id("A-second-id"), id("A-third-id"));

如果没有语法糖,很明显,每次调用 id 时都会覆盖片段,并且在转换为原始值时只会使用最后一个值。

当您不使用标记函数时,每个值都会在模板字符串中的每个位置转换为原始值。但是,当您将其与标记函数一起使用时,您会将每个值作为标记函数的参数,并且只有在标记函数中对其进行转换后才会发生到原始值的转换。因此,您只能获得片段的最后一个值。

关于javascript - 标记模板字符串和闭包的问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59905736/

相关文章:

javascript - 更改路线不会在新页面中滚动到顶部

JavaScript - 仅使用 addEventListener 单击缩略图来更改预览图像

swift - 如何在本地闭包中调用非转义闭包?

iOS Swift : Closures (Callbacks) versus Delegates, 什么时候使用哪个?

javascript - 使用模板字符串和 && 运算符 react 条件类名

javascript - 奇怪的 js 评估/模板字符串行为

javascript - ES6 标记模板实用性

javascript - 无法读取未定义的属性 'isLoggedIn'

javascript - "test".reverse() 这可能吗?

javascript - setTimeout 中的多个任务未按预期工作