初めに
前回で漏れた一部の文字列メソッドと、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...of
とArray.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)
-
NaN
、Infinity
など文字列できない数値は強制的に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
そして循環参照を避けるにはundefined
でJSON
に無視させましょう。
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)))
物足りないと思いながら今の実力を出し切ったって感じがします。。
次回はテンプレートリテラルと、気になるところをまとめていきたいと思います。