标记模板字符串和闭包的问题
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 时都会覆盖片段,并且在转换为原始值时只会使用最后一个值。
当您不使用标记函数时,每个值都会在模板字符串的每个位置转换为原始值。但是,当您将它与标记函数一起使用时,您会将每个值作为标记函数的参数获取,并且只有在标记函数中将其转换后才会转换为原始值。因此,您只能获得片段的最后一个值。
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 时都会覆盖片段,并且在转换为原始值时只会使用最后一个值。
当您不使用标记函数时,每个值都会在模板字符串的每个位置转换为原始值。但是,当您将它与标记函数一起使用时,您会将每个值作为标记函数的参数获取,并且只有在标记函数中将其转换后才会转换为原始值。因此,您只能获得片段的最后一个值。