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

Problem with tagged template strings and closure

Symbol.toPrimitive 方法,在标记的模板文字中调用失去对闭包的访问。

要重现,只需将提供的代码片段粘贴到开发控制台,运行 它带有或不带有标记功能。非常感谢任何相关文章。

P.S。如果您能告诉我如何以及在何处调试 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();

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


在下面的一些评论后,对 "run it with and without tag-function" 的含义感到困惑:

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"。使用标记功能时,只有 "A-third-id" 存在(格式也不同)。

问题是为什么 "A-first-id" 和 "A-second-id" 在与标记函数一起使用时会丢失。


但我注意到你每次调用 id 时都会覆盖片段,并且稍后调用 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.

当您使用对象的函数解构时,该函数不再知道上下文。您可以通过在构造函数中绑定函数来解决这个问题:

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 

调试用:

  • 浏览器:在GoogleChrome中,按F12,选择source-tab。您可以设置断点。 Chrome example
  • 节点:参见node example

更新

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

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 时都会覆盖片段,并且在转换为原始值时只会使用最后一个值。

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