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();
我不确定我是否理解你的意思。
经过下面的一些评论之后,关于“使用和不使用标签功能运行它的含义”的混淆是:
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
定义为吸气剂。因此,当您这样做时:
let { id, html } = thread;
您实际上正在调用id
中的代码,并且您获得了该函数。因此,每次在字符串中使用id
时,它都会使用具有相同片段的相同函数。
解决方案?重构代码,以使id
不再是吸气剂。
当您使用从对象解构函数时,该函数不再知道上下文。您可以通过在构造函数中绑定函数来解决此问题:
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时都会覆盖该片段,并且转换为原始值时仅使用最后一个值。
当不使用标记功能时,每个值都会在模板字符串中的每个位置转换为原始值。但是,当将其与标记函数一起使用时,会将每个值作为标记函数的参数,并且直到在标记函数中将其转换为原始值时,才会转换为原始值。因此,您只会得到片段的最后一个值。