【问题标题】:Problem with tagged template strings and closure标记模板字符串和闭包的问题
【发布时间】:2020-05-11 07:54:21
【问题描述】:

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

要重现,只需将提供的代码 sn-p 粘贴到开发控制台中,使用和不使用标记功能运行它。任何相关文章都非常感谢。

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

【问题讨论】:

  • 你能解释一下它应该做什么以及当它无法访问闭包时哪里出错了吗?
  • 基本上,如果不详细说明,我使用标记模板文字来临时更改thread 实例上的_mode 属性。因此,通过html 方法中的String(interpolations[i]) 进行的后续字符串化可以对字符串应用适当的转换。由于_mode 是实例的属性,我必须通过箭头函数将html 函数绑定到实例。如果您在提供的示例中删除 tag-function,预期的输出应该与您可以获得的输出相同。

标签: javascript closures template-strings


【解决方案1】:

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


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

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”在与tag-function一起使用时会丢失。


但我注意到每次调用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 定义为吸气剂。所以当你这样做时:

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 

对于调试:

  • 浏览器:在 Google Chrome 中,按 F12,选择源选项卡。您可以设置断点。 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 都会覆盖片段,并且在转换为原始值时只会使用最后一个值。

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

【讨论】:

  • 是的,确实,id('fragment') 调用是在标记的模板字符串有机会对 id 进行字符串化之前执行的,所以我最终得到了最后一个 id。但。它适用于常规模板文字(id|id('fragment') 按顺序执行和字符串化)。
  • @Raised 如果您显示“有效”的代码,对您的帮助会容易得多。
  • 例如,如果您将 &lt;li ${id("C-first-id")}&gt;&lt;/li&gt; 更改为 &lt;li ${thread.id("C-first-id")}&gt;&lt;/li&gt; 它也可以工作,因为这将为每个字符串调用 getter。您的错误是因为 Thread.id 是一个吸气剂。执行let { id } = thread; 然后使用id 与使用thread.id 不同
  • 只需在模板文字文字之前删除html,您就有了可以工作的代码。 Id 是故意的。
  • 啊,这就是你的意思!如果没有标签函数,它可以工作,因为每个 id 都按顺序执行并转换为原始值。当您使用标记函数执行此操作时,调用函数只是语法糖。这些值会在以后进行转换,因此都有最新的片段,并且您的代码不起作用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-11-13
  • 1970-01-01
  • 1970-01-01
  • 2014-07-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多