3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JavaScriptのStringについて part2

Last updated at Posted at 2022-09-17

初めに

前回で漏れた一部の文字列メソッドと、JSONも一緒にまとめてみました。

今回の参考資料です。

(「字符串的扩展」の1~4まで)

Methods

String.prototype.padStart()

目標の文字列が指定の長さに達していないとき、指定の長さまで先頭にほかの文字列を追加する。

// Syntax
str.padStart(targetLength[, padString])

下はMDNの例です。

// console.log('123'.padStart(5, '0')); // '00123'

const fullNumber = '012345678910';
const lastFourDigits = fullNumber.slice(-4);
let maskedNumber = lastFourDigits.padStart(fullNumber.length, '*');
console.log(maskedNumber); // ********8910

少しもの足りなさを感じて正規表現で練習をしました。

function maskedNum(str, unmaskedLength) {
  if (/^\d+$/.test(str)) {
    return '*'.repeat(str.length - unmaskedLength) + str.slice(-(unmaskedLength));
  }
  return 'Invalid input';
}
console.log(maskedNum('012345678910', 4)) // ********8910
console.log(maskedNum('a12345678910', 4)) // Invalid input

String.prototype.padEnd()

padStart()とほぼ同じ仕組みです。長さが足りるまで末尾から文字列を追加する。

// Syntax
str.padEnd(targetLength[, padString])

...でも何か柔軟性が足りないので別の練習になってしまいました。

// console.log('abcdef'.padEnd(10, '.')); // abcdef....

function bannerString(str, targetLength) {
  if ((str.length - 1) <= (targetLength - 3)) {
    return str + '.'.repeat(3);
  }
  return str.slice(0, targetLength - 3) + '.'.repeat(3);
}

let input = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
console.log(bannerString(input, 30));
// Lorem ipsum dolor sit amet,...
console.log(bannerString(input, 60));
// Lorem ipsum dolor sit amet, consectetur adipiscing elit....
console.log(bannerString(input, 90));
// Lorem ipsum dolor sit amet, consectetur adipiscing elit....

String.prototype.trimStart() & String.prototype.trimEnd() & String.prototype.trim()

先頭/末尾/両端の改行・空白文字を削除する。

// Syntax
str.trimStart() // start from left
str.trimEnd() // end up right
str.trim() // trim head and tail
let str = '     Lorem  123 ';
console.log(str.length);
// 16
console.log(str.trimStart(), str.trimStart().length);
// Lorem  123  11
console.log(str.trimEnd(), str.trimEnd().length);
//      Lorem  123 15
console.log(str.trim(), str.trim().length);
// Lorem  123 10
let str = '\t\f\r\n\v\u0020\u00a0\u3000\u2028\u2029Lorem\t\f\r\n\v\u0020\u00a0\u3000\u2028\u2029';
console.log(str.length);
// 25
console.log(str.trimStart().length);
// 15
console.log(str.trimEnd().length);
// 15
console.log(str.trim().length);
// 5

改行・空白文字について前の文章では少しまとめました。

Iterator

普通のforループではCode Unit単位で文字列を処理するので、サロゲートペアなど二つのCode Unitの組み合わせや、0xffffを超えたUnicodeを正しく処理できません。
ここでCode Point単位で文字列を識別するfor...ofArray.from()をしたいと思います。(ほかにメソッドが分かればまた更新します。)

for...of

let str = '🍎🍇🍎🥕🍒';
for (let item of str) {
  console.log(item.codePointAt(0));
}
// 127822
// 127815
// 127822
// 129365
// 127826

Array.from()

function countCodePoints(str, target) {
  let count = 0;
  Array.from(str, (x) => {
    if (x === target) count += 1;
  })
  return count;
}
console.log(countCodePoints(str, '🍎')); // 2

JSON

データ交換のための通用フォーマットオブジェクト。(自分の解釈です。実際はもっと複雑。)
JSON文字列はオブジェクト、配列、数値、文字列、論理値そしてnullを直列化(serialized)/整列化(marshalled)されるオブジェクトと呼ばれる。

Methods

JSON.stringify()は引数をJSONフォーマットのテキスト(String)に転換し、
JSON.parse()JSONテキストからJavaScriptが対応する値に転換する。

// Syntax
JSON.stringify(value[, replacer[, space]])
// note
value: Object, Array, Number, String, boolean, null
replacer: Array of properties / function replacer(key, value)
space: space characters for formatting (space <= 10)

JSON.parse(text, reviver)
// note
text: JSON-string
reviver: function replacer(key, value)
  • NaNInfinityなど文字列できない数値は強制的にnullに転換される。
  • 関数プロパティ、シンボルキーと値、undefinedを格納しているプロパティはスキップされる。

JSON.stringify() - value

let jsonArr = JSON.stringify(['1', 2, 'true', false, null, NaN, Infinity]);
console.log(jsonArr);
// ["1",2,"true",false,null,null,null]
console.log(JSON.parse(jsonArr));
// [
//   '1', 2,
//   'true', false,
//   null, null,
//   null
// ]
let jsonObj = JSON.stringify({
  a: '1',
  b: 2,
  c: 'true',
  d: false,
  e: null,
  f: NaN,
  g: Infinity,
  h: {
    a: '5'
  },
  i: [4, 5, 6],
  myMethod() {
    console.log('myMethod');
  },
  [Symbol('id')]: 123,
  j: Symbol('foo'),
  k: undefined
})
console.log(jsonObj);
// {"a":"1","b":2,"c":"true","d":false,"e":null,"f":null,"g":null,"h":{"a":"5"},"i":[4, 5, 6]}
console.log(JSON.parse(jsonObj));
/*
{
  a: '1',
  b: 2,
  c: 'true',
  d: false,
  e: null,
  f: null,
  g: null,
  h: { a: '5' },
  i: [ 4, 5, 6 ]
}
*/

JSON.stringify()はオブジェクトラッパー関数から返したオブジェクトでもJSON文字列に転換できる。JSON文字列にしたときラッパーが捨てられ、再びJSON.parse()で転換すると対応する値になります。

// object wrapper => json => corresponding value
// console.log(typeof (new Number(5)));// object
let num = JSON.stringify(new Number(5));
console.log(num);
// 5 // string
console.log(JSON.parse(num));
// 5 // number

// console.log(typeof (new String('abc'))); // object
let str = JSON.stringify(new String('abc'));
console.log(str);
// "abc" // string with double quotes
console.log(JSON.parse(str));
// abc // string

JSON.stringify() - replacer

循環構造(circular structure)/循環参照(circular references)のオブジェクトをそのままJSONに転換したらエラーが出てきます。

let room = {
  number: 10
}
let guests = {
  title: 'Party',
  participants: ['Taro', 'Jiro']
};
guests.place = room;
room.occupiedBy = guests;
console.log(guests.place, room.occupiedBy);
/*
<ref *1> {
  number: 10,
  occupiedBy: {
    title: 'Party',
    participants: [ 'Taro', 'Jiro' ],
    place: [Circular *1]
  }
} <ref *1> {
  title: 'Party',
  participants: [ 'Taro', 'Jiro' ],
  place: { number: 10, occupiedBy: [Circular *1] }
}
*/
console.log(JSON.stringify(guests));
// TypeError: Converting circular structure to JSON

JSON.stringifyの第2引数に循環参照を避けたプロパティの配列を渡したら問題なくJSONにしてくれます(ネストされたプロパティも適用される)。

let room = {
  number: 10
};
let guests = {
  title: 'Party',
  participants: [{ name: 'Taro' }, { name: 'Jiro' }],
  place: room
};
room.occupiedBy = guests;

console.log(JSON.stringify(guests, ['title', 'participants']));
// {"title":"Party","participants":[{},{}]}
console.log(JSON.stringify(guests, ['title', 'participants', 'name', 'place', 'number']));
// {"title":"Party","participants":[{"name":"Taro"},{"name":"Jiro"}],"place":{"number":10}}

console.log(JSON.stringify(guests, ['title', 'number', 'name', 'place', 'participants']));
// {"title":"Party","place":{"number":10},"participants":[{"name":"Taro"},{"name":"Jiro"}]}

(第2引数に配列入れたJSON内部の順番は配列によって変わる。)

循環参照のプロパティが衝突しない限りうまくいきますが。

console.log(JSON.stringify(guests, ['occupiedBy']));
// {}
console.log(JSON.stringify(guests, ['title', 'occupiedBy']));
// {"title":"Party"}
console.log(JSON.stringify(guests, ['place', 'occupiedBy']));
// TypeError: Converting circular structure to JSON

そして循環参照を避けるにはundefinedJSONに無視させましょう。

console.log(JSON.stringify(guests, (key, value) => {
  return (key === 'occupiedBy') ? undefined : value;
}));
// {"title":"Party","participants":[{"name":"Taro"},{"name":"Jiro"}],"place":{"number":10}}

こちらの例を参考にして書いたんですが。replacerの第2引数valueを出力してよく見たら、順番でプロパティにアクセスし、ネストされた部分まで全部取り出してくれました。これで第1引数keyに配列を入れて特定のプロパティだけ取り出してくれる、また、ネストのプロパティまで指定されないと無視されるのが何となくわかりました。

JSON.stringify() - spaces

見やすさのためJSON.stringify()では第3引数に指定の数値(最大10まで)か、文字列(長さが10まで)を用いてインデントに使われます。

let user = {
  name: 'Taro',
  age: 26,
  roles: {
    isAdmin: false,
    isEditor: true
  }
};
console.log(JSON.stringify(user));
// {"name":"Taro","age":26,"roles":{"isAdmin":false,"isEditor":true}}
console.log(JSON.stringify(user, null, 2));
/*
{
  "name": "Taro",
  "age": 26,
  "roles": {
    "isAdmin": false,
    "isEditor": true
  }
}
*/

toJSON()

オブジェクトのtoString()のような位置づけです。JSON.stringify()利用すると呼び出されます。

let room = {
  number: 23,
};
let meetup = {
  title: "Conference",
  room
};
console.log(JSON.stringify(room)); // {"number":23}
console.log(JSON.stringify(meetup)); // {"title":"Conference","room":{"number":23}}

let room = {
  number: 23,
  toJSON() {
    return this.number;
  }
};
let meetup = {
  title: "Conference",
  room
};
console.log(JSON.stringify(room)); // 23
console.log(JSON.stringify(meetup)); // {"title":"Conference","room":23}

JSON.parse() - str

JSON文字列です。

× '{name: "Taro"}'
 '{"name": "Taro"}'
× '{"surname": 'Yamada'}'
 '{"surname": "Yamada"}'
× '{'isAdmin': false}'
 '{"isAdmin": false}'
× '{"birthday": new Date(1995, 6, 6)}'
 '{"birthday":"1995-07-05T16:00:00.000Z"}'

× '{"friends": "[0, 1, 2, 3]"}' // value is string
 '{"friends": [0, 1, 2, 3]}'
× '{"family": "{"father": "...", "mother": "..."}"}'
 '{"family": {"father": "...", "mother": "..."}}'

JSON.parse() - reviver

reviver()(key, value)二つのパラメータを持つ関数です。
reviver()JSON文字列に操作することができます。

let str = '{"name": "Taro", "birthday": "1995-07-05T16:00:00.000Z"}';
let person = JSON.parse(str, (key, value) => {
  if (key === 'birthday') return new Date(value);
  return value;
});
console.log(`${person.name}'s birthday is ${person.birthday.toDateString()}`);
// Taro's birthday is Thu Jul 06 1995

replacer()と同じくreviver()もネストされたプロパティへ動作します。

Practice

こちらの例を書いてみました。

let room = {
  number: 10
};
let guests = {
  title: 'Party',
  participants: [{ name: 'Taro' }, { name: 'Jiro' }],
  place: room
};
room.occupiedBy = guests;
guests.self = guests;

console.log(JSON.stringify(guests, (key, value) => {
  // console.log(key)
  return (key !== '' && value === guests) ? undefined : value;
}))
// {"title":"Party","participants":[{"name":"Taro"},{"name":"Jiro"}],"place":{"number":10}}

下はkeyの出力結果です。


title
participants
0
name
1
name
place
number
occupiedBy
self

最初のkey""{"": guests})、そして順番でネストされたプロパティまで取り出しました。

Newline/terminator in JSON

改行・空白文字がどう処理されるのか気になります。
JSON文字列のような構造(double quotes)包んだ改行・空白文字をパースしようとしたらエラーになる。

console.log(JSON.parse('"foo"')); // foo 
console.log(JSON.parse('"\u0009"'));
// SyntaxError: Unexpected token    in JSON at position 1

プロパティバリューにすると問題なく処理してくれました。

const json = JSON.stringify({
  horizontalTab: '\u0009',
  verticalTab: '\u000b',
  formFeed: '\u000c',
  carriageReturn: '\u000d',
  lineFeed: '\u000a',
  whiteSpace: '\u0020',
  nonBreakingSpace: '\u00a0',
  ideographicSpace: '\u3000',
  lineSeparator: '\u2028',
  paragraphSeparator: '\u2029',
  reverseSolidus: '\u005c'
});
console.log(json);
/*
{"horizontalTab":"\t","verticalTab":"\u000b","formFeed":"\f","carriageReturn":"\
r","lineFeed":"\n","whiteSpace":" ","nonBreakingSpace":" ","ideographicSpace":"
 ","lineSeparator":"","paragraphSeparator":"","reverseSolidus":"\\"}
*/
console.log(JSON.parse(json));
// {
//   horizontalTab: '\t',
//   verticalTab: '\x0B',
//   formFeed: '\f',
//   carriageReturn: '\r',
//   lineFeed: '\n',
//   whiteSpace: ' ',
//   nonBreakingSpace: ' ',
//   ideographicSpace: ' ',
//   lineSeparator: '',
//   paragraphSeparator: '',
//   reverseSolidus: '\\'
// }

(ES2019からJSONの文字列やプロパティに\u2028\u2029を格納することができました。)
文字列の両端でなければ改行・空白文字が普通の空白(space)として処理できるが、
両端に置かれたらSyntaxErrorになってしまいます。これは暗黙の文字列''でも、テンプレートリテラルでもそうです。

let str = '"abc\u2028def\u2029ghi"';
console.log(JSON.parse(str));
// abc def ghi
str = `"abc\u2028def\u2029ghi"`
console.log(JSON.parse(str));
// abc def ghi

// newline/terminator at the head or tail
str = '"\u0009abc\u2028def\u2029ghi"';
console.log(JSON.parse(str));
// SyntaxError: Unexpected token    in JSON at position 1
str = '"abc\u2028def\u2029ghi\u000b"';
console.log(JSON.parse(str));
/*
SyntaxError: Unexpected token
                               in JSON at position 12
*/

str = `"\u0009abc\u2028def\u2029ghi"`
console.log(JSON.parse(str));
// SyntaxError: Unexpected token    in JSON at position 1
str = `"abc\u2028def\u2029ghi\u000b"`
console.log(JSON.parse(str));
/*
SyntaxError: Unexpected token
                               in JSON at position 12
*/

ユーザからもらったStringは改行・空白文字が混じっている状態かもしれないので、JSONにする前に両端の空白文字を切り捨てるメソッドtrim()、またはreplace()正規表現で検索して書き換える方法を書いてみました。

let json = '"\u2028abc\u2028def\u2029ghi\u2029"';

// trim()
function trimmedJsonString(json) {
  let str = json.slice(1, json.length - 1);
  return `"${str.trim()}"`;
}
console.log(JSON.parse(trimmedJsonString(json)));
// abc def ghi

// replace()
function replacedJsonString(json) {
  let str = json.slice(1, json.length - 1);
  return `"${str.replace(/^\s+|\s+$/, '')}"`;
}
console.log(JSON.parse(replacedJsonString(json)));
// abc def ghi

function replacedBySpace(json) {
  let str = json.slice(1, json.length - 1);
  return `"${str.replace(/\s+/g, ' ')}"`;
}
console.log(JSON.parse(replacedBySpace(json)));
//  abc def ghi

それぞれの改行・空白文字を保留したいときもあるかもしれませんが。

let json = '"\u2028\u2028abc\u2028def\u2029ghi\u2029\u2029"';
function escapedJsonString(json) {
  let str = json.slice(1, json.length - 1);
  return `"${str.
    replace(/\u2028/g, '\\u2028').
    replace(/\u2029/g, '\\u2029')}"
    `;
}
console.log(JSON.parse(escapedJsonString(json)));
//   abc def ghi

unicode in JSON

データ交換のためにスタンダードのJSONデータはUTF-8でエンコードされなければならない。たとえばU+D800-U+DFFFではサロゲートペアではない単独のコードポイントや、有効ではないペア(対応する文字が存在しない)はUTF-8のスタンダードに合わないです。
今のJavaScripJSON.stringify()はそれらをエスケープ無しでそのまま文字列として出力し、さらなる処理はしません。

// well-formed JSON.stringify
console.log(JSON.stringify('\ud834')); // "\ud834"
console.log(JSON.stringify('\udf06\ud834')); // "\udf06\ud834"
console.log(JSON.stringify('\ud834\udf06')); // "𝌆"
console.log(JSON.parse(JSON.stringify('\ud834\udf06'))); // 𝌆

console.log(JSON.stringify('\ud834') === '"\\ud834"'); // true
console.log(JSON.stringify('\udf06') === '"\\udf06"'); // true
  • サロゲートペア:U+10000-U+10FFFF
  • 上位サロゲート:U+D800-U+DBFF
  • 下位サロゲート:U+DC00-U+DFFF
let str = '\ud834\udf06\ud834abc\ud834\udf06';
console.log(JSON.stringify(str, (key, value) => {
  return value.replace(/[\ud800-\udfff]/g, '')
}));
// "abc"

(思ったことのメモです。)
正規表現でサロゲート排除するって本当にいいのかな?有効なサロゲートペアならちゃんと転換出来てるし、特殊の符号文字も場合によって存在する必要があると思うけれど、なかなか正規表現で取り出すことができなかった。

コードポイントが問題です。正規表現では下位上位という誤った組み合わせを摘出して処理するのができるが、連なるコードポイント\ud834\udf06\ud834では\ud834\udf06は正しい組み合わせに対して\udf06\ud834は間違ったので排除すると、\ud834はペアになれなくてバグになるしかありません。

なので一度サロゲートへ処理するのに必要だと感じました。

// surrogate pair in string
let str = '\ud834\udf06\ud834abc\ud834\udf06';
function findSurrogatePair(str) {
  return Array.from(str, (item) => {
    return (item.codePointAt() > 65535) ?
      `0x${item.codePointAt().toString(16)}/` : item
  }).join('');
}

// JSON.stringify()
let json = JSON.stringify(str, (key, value) => {
  let fixedString = findSurrogatePair(value);
  return fixedString.replace(/[\ud800-\udfff]/g, '');
});
console.log(json);
// "0x1d306/abc0x1d306/"

// JSON.parse()
let jsonToString = JSON.parse(json, (key, value) => {
  return value.replace(/0x[a-f0-9]{5,6}\//g, (item) => {
    let str = item.slice(0, item.length - 1);
    return String.fromCodePoint(str);
  })
});
console.log(jsonToString);
// 𝌆abc𝌆

// surrogate pair in obj
let obj = {
  string1: '𝌆abc𝌆',
  string2: '\ud834\udf06\ud834abc\ud834\udf06'
}

let json = JSON.stringify(obj, (key, value) => {
  if (key !== '' && typeof value === 'string') {
    let fixedObj = findSurrogatePair(value);
    return fixedObj.replace(/[\ud800-\udfff]/g, '');
  }
  return value
});
console.log(json);
// {"string1":"0x1d306/abc0x1d306/","string2":"0x1d306/abc0x1d306/"}

let jsonToObj = JSON.parse(json, (key, value) => {
  if (key !== '' && typeof value === 'string') {
    return value.replace(/0x[a-f0-9]{5,6}\//g, (item) => {
      let str = item.slice(0, item.length - 1);
      return String.fromCodePoint(str);
    })
  }
  return value;
});
console.log(jsonToObj);
// {string1: '𝌆abc𝌆', string2: '𝌆abc𝌆'}

下はまとめて書き直した関数です。

// surrogate pair in string
function surrogateStringToJSON(str) {
  let fixedString = Array.from(str, (item) => {
    return (item.codePointAt() > 65535) ?
      `0x${item.codePointAt().toString(16)}/` : item
  }).join('').replace(/[\ud800-\udfff]/g, '');
  return JSON.stringify(fixedString);
}
console.log(surrogateStringToJSON(str))
// "0x1d306/abc0x1d306/"

function surrogateJSONToString(json) {
  let fixedJson = json.replace(/0x[a-f0-9]{5,6}\//g, (item) => {
    let str = item.slice(0, item.length - 1);
    return String.fromCodePoint(str);
  })
  return JSON.parse(fixedJson);
}
console.log(surrogateJSONToString(surrogateStringToJSON(str)))
// 𝌆abc𝌆

// surrogate pair in obj
function surrogateObjToJSON(obj) {
  return JSON.stringify(obj, (key, value) => {
    if (key !== '' && typeof value === 'string') {
      return Array.from(value, (item) => {
        return (item.codePointAt() > 65535) ?
          `0x${item.codePointAt().toString(16)}/` : item
      }).join('').replace(/[\ud800-\udfff]/g, '');
    }
    return value
  })
}
console.log(surrogateObjToJSON(obj))

function surrogateJSONToObj(json) {
  return JSON.parse(json, (key, value) => {
    if (key !== '' && typeof value === 'string') {
      return value.replace(/0x[a-f0-9]{5,6}\//g, (item) => {
        let str = item.slice(0, item.length - 1);
        return String.fromCodePoint(str);
      })
    }
    return value;
  })
}
console.log(surrogateJSONToObj(surrogateObjToJSON(obj)))

物足りないと思いながら今の実力を出し切ったって感じがします。。
次回はテンプレートリテラルと、気になるところをまとめていきたいと思います。

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?