0
0

More than 1 year has passed since last update.

JavaScriptのStringについて part3

Posted at

初めに

今回はテンプレートリテラルの使い方をまとめていきたいと思います。

Template literals(Template strings)

Basic usage

すでに使い慣れたと思ったけど、実際調べたら別の使い方や応用もあるので色々とメモしたいです。
HTMLのタグとタグの間空白があっても画面に表示されないのであまり気にしませんでしたが、意識しないうちにそれがバグの原因にあったかもしれないので、trim()で空白を排除するのがいいと思います。

let str = `
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim()
console.log(str)
/*
<ul>
  <li>first</li>
  <li>second</li>
</ul>
*/

テンプレートリテラルにはどんな値が入れられても最終的に文字列になる。

let obj = {
  x: 1,
  y: 2
};
let re = {
  x: `${obj.x}`,
  y: `${obj.y}`
}
console.log(re)
// { x: '1', y: '2' } // value is string

テンプレートリテラルの中でもテンプレートリテラルを用いられる。(入れ子関数みたいなものです。)

let userName = [
  { first: 'Taro', last: 'Yamada' },
  { first: 'Jiro', last: 'Yamada' }
];
let temp = (address) => `
<table>
  ${address.map((x) => `
  <tr><td>${x.first}</td></tr>
  <tr><td>${x.last}</td></tr>
  `).join('').trim()}
</table>
`.trim();
console.log(temp(userName))
/*
<table>
  <tr><td>Taro</td></tr>
  <tr><td>Yamada</td></tr>

  <tr><td>Jiro</td></tr>
  <tr><td>Yamada</td></tr>
</table>
*/

下のように、暗黙の''よりテンプレートリテラルの方が自由に微調整できるし安全です。例えばAPIから貰ったURLの行頭や末尾に/が含んだら組み立て時、毎回splice()などのメソッドで正しい文字列を抽出しなければならない。また、結合演算子+がもたらした型変換の問題もなくなります。(JSONファイルから取り出しや宣言で型を固定しているなら問題にならないが。)

const base = 'http://example.com/resouces/';
const path = '/example.js';

function getResource(baseURL, pathName) {
  return `${baseURL.replace(/\/$/, '')}${pathName}`
}
console.log(getResource(base, path));
// http://example.com/resouces/example.js

Tagged template(Tagged function)

タグつきテンプレート関数です。まだ経験の浅い自分に初めてみた使い方ですが、今回も頭を抱えながら自分なりにまとめてみました。

タグ付きテンプレートの内部はどうやって実装しているかは初めてみたときさっぱりでした。いくつの参考文章を読み書きながら大まかにまとめると、

  • タグ付きテンプレートは普通の文字列の部分とパラメータ(引数)を分けている。
  • 実装のイメージはreduce()のように最終的に一つの文字列に結合する。(forループでも実現できる。)

まずは普通のテンプレートとタグ付きテンプレートの違いから比べてみたいと思います。一般の使い方では関数の引数としてテンプレート入れて出力させたら、

function fullName(str) {
  console.log(str)
}
fullName(`First name is ${'Taro'}, last name is ${'Yamada'}`);
// First name is Taro, last name is Yamada

タグ付きテンプレートなら

function fullName(first, ...last) {
  console.log(arguments);
  console.log(first);
  console.log(last);
}
fullName`First name is ${'Taro'}, last name is ${'Yamada'}.`
/*
[Arguments] {
  '0': [ 'First name is ', ', last name is ', '.' ],
  '1': 'Taro',
  '2': 'Yamada'
}
*/
// ['First name is ', ', last name is ', '.']
// ['Taro', 'Yamada']

// note: /First name is /${'Taro'}/, last name is /${'Yamada'}/./

上のように、タグ付きテンプレートは${}が区切りになって、普通の文字列と${}の引数が分けられている。
ではどのように結合して出力するのでしょうか。

// reduce((previousValue, currentValue, currentIndex))
function stringRaw(strings, ...values) {
  console.log(arguments)
  return strings.reduce((first, str, i) => {
    console.log([first, values[i - 1], str]);
    return first + values[i - 1] + str;
  });
}
// console.log(stringRaw`First name is ${'Taro'}, last name is ${'Yamada'}.`)
// [Arguments] {
//   '0': [ 'First name is ', ', last name is ', '.' ],
//   '1': 'Taro',
//   '2': 'Yamada'
// }
// ['First name is ', 'Taro', ', last name is ']
// ['First name is Taro, last name is ', 'Yamada', '.']
// First name is Taro, last name is Yamada.

タグ付きテンプレートは文字列と引数を分けて配列にするから、[Arguments]のように、stringsは普通の文字列の配列、...valuesは残りの引数を展開する。(引数の個数が固定している場合はvalue1, value2などでパラメータ設置してもいいです。)

そしてreduce()の特性を用いて、(first, str, i)という設置で、
一回目のreduce()first(指定しなければstrings[0]がデフォルト)、引数の配列[0]と、strings[1]が結合し、
/First name is /${'Taro'}/, last name is /、となる。これは二回目のreduce()firstになります。
それで二回目はfirst、引数の配列[1]、strings[2]が結合、
/First name is ${'Taro'}, last name is /${'Yamada'}/./
最終的に、First name is Taro, last name is Yamada.となりました。

forループでもタグ付きテンプレートの実装ができます。

// First name is Taro, last name is Yamada.
let raw = function (strings, ...values) {
  let output = '';
  let i;
  for (i = 0; i < values.length; i++) {
    output += strings[i] + values[i];
    console.log(output)
  }

  output += strings[i]
  return output;
}
console.log(raw`First name is ${'Taro'}, last name is ${'Yamada'}.`)
// First name is Taro
// First name is Taro, last name is Yamada
// First name is Taro, last name is Yamada.

reduce()で結合の原理が分かればforループでも簡単に実現できます。ただforreduce()と少し違って、stringsの最後の要素をループ外でoutputと結合しなければならない。これはタグ付きテンプレートが文字列への切り取り方と関わっている。タグ付きテンプレートの最後が${}じゃなく単なる空白''でも切り取るのです。不思議に思ったが、最初は手抜きして.を書いてなかったんでこれに気づきました。つまりforループはstrings最後の要素をカバーできないのです。(<= values.lengthにしてカバーしたら今度はvalues[]undefinedになる。)

そしてタグ付きテンプレートではなく、普通の関数の入れ方に戻すと、

console.log(raw(['First name is ', ', last name is ', '.'], 'Taro', 'Yamada'))
// First name is Taro
// First name is Taro, last name is Yamada
// First name is Taro, last name is Yamada.

もし複雑な処理をする必要ないならString.raw``でも簡単にできる。

console.log(String.raw`First name is ${'Taro'}, last name is ${'Yamada'}.`)
// First name is Taro, last name is Yamada.

String.raw``\n\uなどのエスケープを無効化して生の文字列に形成してくれるメソッドです。もともとエスケープできず問題を起こすが今は修正されたそうです。

構造と使い方が分かったら、タグ付きテンプレートはメリットの多いメソッドです。例えばユーザ(inputから取り出したテキストとか)からの悪意のあるコードもろ過しすることができます。

function saferHTML(strings, ...values) {
  let result = ''
  let i
  for (i = 0; i < values.length; i++) {
    let value = String.raw`${values[i]}`
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
    result += strings[i] + value
  }
  return (result + strings[i]);
}

let sender = '<script>alert("abc")</script>'; // malicious code
let message = saferHTML`<p>${sender} has sent you a message.</p>`;
console.log(message)
// <p>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>

encodeURIComponent

エスケープされないもの:A-Z a-z 0-9 - _ . ! ~ * ' ( )

encodeURIComponent()URI(Uniform Resource Identifier)構成要素として特定の文字をUTF-8エンコーディングのようにエスケープシーケンスに転換するメソッドです。例えばURLには特定の符号文字? = / &、または漢字などUTF-8にエンコードしないと、訪問先のパスとして識別できないということです。

さきのようにString.raw``URLの処理をしたら、

const input = 'A&B';
const escapedURL = String.raw`https://example.com/search?q=${input}&sort=desc`;
console.log(escapedURL)
// https://example.com/search?q=A&B&sort=desc

console.log(encodeURIComponent(input))
// A%26B

String.raw``はエスケープ無しで生の文字列として処理してくれるだけです。

タグ付きテンプレート関数を使えばユーザからもらったinputテキストを関数内部で処理する。

function escapeURL(strings, ...values) {
  return strings.reduce((result, str, i) => {
    let value = String.raw`${values[i - 1]}`
    return result + encodeURIComponent(value) + str;
  });
}
const input = 'A&B';
const escapedURL = escapeURL`https://example.com/search?q=${input}&sort=desc`;
console.log(escapedURL)
// https://example.com/search?q=A%26B&sort=desc

forループで書き換えてもいいです。

function escapeURL(strings, ...values) {
  let output = '';
  let i;
  for (i = 0; i < values.length; i++) {
    let value = String.raw`${values[i]}`
    output += strings[i] + encodeURIComponent(value);
  }
  output += strings[i]
  return output;
}
const input = 'A&B';
const escapedURL = escapeURL`https://example.com/search?q=${input}&sort=desc`;
console.log(escapedURL)
// https://example.com/search?q=A%26B&sort=desc

HTMLはUTF-8でエンコードしているため、encodeURIComponent()の応用はURLのエスケープのほかにもたくさんあります。

自分はこれまでの勉強ではサロゲート、サロゲートペアなどという文字列の表示にまつわる問題と処理も少し理解しつつ実践してきましたが、今回テンプレートの勉強ではUTF-8UTF-16の認識はまだまだ足りないと感じて今後の課題にしておきたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0