34–2. You Don’t Know JS: ES6 & Beyond Chapter 2: Syntax 2/3
【所要時間】
6時間14分(2018年8月8,9,10日)
【概要】
ES6の構文について
【要約・学んだこと】
Repeated Assignments
object destructing formは、source property(どんなvalue typeも保持する)を複数回リスト化できる。
var { a: X, a: Y } = { a: 1 };
X; // 1
Y; // 1
それはsub-object/array propertyをdestructure することと、sub-object/arrayのvalue自身を捉えることができる。
var { a: { x: X, x: Y }, a } = { a: { x: 1 } };
X; // 1
Y; // 1
a; // { x: 1 }
( { a: X, a: Y, a: [ Z ] } = { a: [ 1 ] } );
X.push( 2 );
Y[0] = 10;
X; // [10,2]
Y; // [10,2]
Z; // 1
destructingについて気をつけること。これまで議論してきたように、それは1つの行にすべてのdestructuring assignmentをリスト化したくなるかもしれない。しかし、適切な字下げをして複数行にdestructuring assignment patternを広げる方がいい考えだ。JSONやobject literal valueと同様に読みやすくするためだ。
// harder to read:
var { a: { b: [ c, d ], e: { f } }, g } = obj;
// better:
var {
a: {
b: [ c, d ],
e: { f }
},
g
} = obj;
destructuringの目的は、タイプ数を少なくするのではなく、declarative readabilityだ。
Destructuring Assignment Expressions
object または array destructuringのassignment expressionは、完全な右辺object/array valueをcompletion valueとして持つ。
var o = { a:1, b:2, c:3 },
a, b, c, p;
p = { a, b, c } = o;
console.log( a, b, c ); // 1 2 3
p === o; // true
p はa, b, c valueの1つではなく、 o object referenceを割り当てられた。array destructuringについても同様だ。
var o = [1,2,3],
a, b, c, p;
p = [ a, b, c ] = o;
console.log( a, b, c ); // 1 2 3
p === o; // true
object/array valueをcompletionとして渡すことによって、destructuring assignment expressionを連結することができる。
var o = { a:1, b:2, c:3 },
p = [4,5,6],
a, b, c, x, y, z;
( {a} = {b,c} = o );
[x,y] = [z] = p;
console.log( a, b, c ); // 1 2 3
console.log( x, y, z ); // 4 5 4
Too Many, Too Few, Just Enough
array destructuring assignmentとobject destructuring assignmentは、どちらも存在する全てのvalうえを割り当てる必要はない。
var [,b] = foo();
var { x, z } = bar();
console.log( b, x, z ); // 2 4 6
foo()から返されたvalue 1と3は、bar()のvalue 5と同じく捨てられる。
同様に、destructuring/decomposingしているvalueに存在するよりも多くのvalueを割り当てようとすると、期待通りundefinedのフォールバックが得られる。
var [,,c,d] = foo();
var { w, z } = bar();
console.log( c, z ); // 3 6
console.log( d, w ); // undefined undefined
この動作は先ほど述べた”undefinedは消える”という原則を対照的に従っている。
このチャプターの … operatorで調べ、あるarray valueを別々のvalueに広げて使われることが出来ることを見た。そして時々、valueのセットをarrayにまとめるというように、逆の使い方もできる。
function declarationでのgather/restの使い方に加えて、 … はdestructuring assignmentで同じ動作を実行することができる。
var a = [2,3,4];
var b = [ 1, ...a, 5 ];
console.log( b ); // [1,2,3,4,5]
ここで …a は a を広げている。なぜならarray[ .. ] value positionに現れるからだ。もし …a がarray destructuring positionに現れるなら、集める動作を実行する。
var a = [2,3,4];
var [ b, ...c ] = a;
console.log( b, c ); // 2 [3,4]
var [ .. ] = adestrucuring assignmentは[ .. ]の中に記述されたパターンを割り当てるように a を広げる。最初の部分は、a(2)の最初のvalueにbを名付ける。しかし、 … cは残りのvalue 3, 4をarrayに集め、cを呼ぶ。
Default Value Assignment
destructuringのフォームはどちらも、デフォルトfunction argument valueと同様の= syntax を使って、assignmentにデフォルトvalue オプションを提供できる。
var [ a = 3, b = 6, c = 9, d = 12 ] = foo();
var { x = 5, y = 10, z = 15, w = 20 } = bar();
console.log( a, b, c, d ); // 1 2 3 12
console.log( x, y, z, w ); // 4 5 6 20
デフォルト value assignment と alternative assignment expression syntax を組み合わせることができる。
var { x, y, z, w: WW = 20 } = bar();
console.log( x, y, z, WW ); // 4 5 6 20
destructuringのデフォルトvalueとしてobject か arrayを使うなら、自分自身や他のディベロッパーを混乱させないように気をつけなければならない。非常に理解が難しいコードも作れる。
var x = 200, y = 300, z = 100;
var o1 = { x: { y: 42 }, z: { y: z } };
( { y: x = { y: y } } = o1 );
( { z: y = { y: z } } = o1 );
( { x: z = { y: x } } = o1 );
x, y, zが最終的にどのvalueになったかわかりますか?
console.log( x.y, y.y, z.y ); // 300 100 42
destructuringは素晴らしい。そしてとても有用だが、場合によっては怪我を引き起こす。
Nested Destructuring
もしdestructuringしているvalueがnested objectかarrayを持っていたら、それらもdestructureできる。
var a1 = [ 1, [2, 3, 4], 5 ];
var o1 = { x: { y: { z: 6 } } };
var [ a, [ b, c, d ], e ] = a1;
var { x: { y: { z: w } } } = o1;
console.log( a, b, c, d, e ); // 1 2 3 4 5
console.log( w ); // 6
nested destrucruingはobject namespaceを平坦化する簡単な方法だ。
var App = {
model: {
User: function(){ .. }
}
};
// instead of:
// var User = App.model.User;
var { model: { User } } = App;
Destructuring Parameters
function foo(x) {
console.log( x );
}
foo( 42 );
このassignmentは少し隠されている。foo(42)が実行されるときに42がx(parameter)に割り当てられる。parameter/argument ペアがassignmentなら、それはdestructureされるかもしれないassignmentであることを理由としている。
parameterのarray destructuring
function foo( [ x, y ] ) {
console.log( x, y );
}
foo( [ 1, 2 ] ); // 1 2
foo( [ 1 ] ); // 1 undefined
foo( [] ); // undefined undefined
parameterが動作するdestructuring
function foo( { x, y } ) {
console.log( x, y );
}
foo( { y: 1, x: 2 } ); // 2 1
foo( { y: 42 } ); // undefined 42
foo( {} ); // undefined undefined
このテクニックはobjectのpropertyが同じ名前の破壊されたparameterにマップされるargument(長期間要求されているJSの機能)の近似だ。これはまた、自由にoptional parameter(どのポジションでも)を取得できるということだ。これは期待通りに、動作した”parameter”から離れることがわかる。
先に議論した全てのdestructuring variationは、nested destructuring、default valueを含むparameter destructuringで使うことができる。
Destructuringはまたデフォルトのparameter valueや、rest/gather parameter機能とうまく組み合わせることができる。
次の図を考えてみよう。
function f1([ x=2, y=3, z ]) { .. }
function f2([ x, y, ...z], w) { .. }
function f3([ x, y, ...z], ...w) { .. }
function f4({ x: X, y }) { .. }
function f5({ x: X = 10, y = 20 }) { .. }
function f6({ x = 10 } = {}, { y } = { y: 10 }) { .. }
このsnippetの1つの例を見てみよう。
function f3([ x, y, ...z], ...w) {
console.log( x, y, z, w );
}
f3( [] ); // undefined undefined [] []
f3( [1,2,3,4], 5, 6 ); // 1 2 [3,4] [5,6]
ここでは2つの … operatorがある。どちらも arrayのz, w valueを集るが、 … z は最初のarray argumentのに残っている残りのvalueから集め、一方で…w は最初の次に残されたmain argumentの残りの部分から集まる。
Destructuring Defaults + Parameter Defaults
1つ特に注意する点がある。destructing defaul valueとfunction parameter default valueの違いだ。
function f6({ x = 10 } = {}, { y } = { y: 10 }) {
console.log( x, y );
}
f6(); // 10 10
一目見ると、xとy parameter両方にdefault value 10が宣言されているようにみえる。しかし、2つは違う。これらの2つの異なるアプローチは、場合によって異なる動作をする。そして違いはわずかだ。
f6( {}, {} ); // 10 undefined
最初のargument’s objectに同じ名前のpropertyが渡されなければ、parameter x がデフォルトで10になることは明らかだ。
しかし、{ y: 10 } valueはfunction parameter default valueとしてのobjectで、destructuring default valueではない。したがって、2番目のargumentが全くわたされない場合、undefinedとして渡される場合にのみ適用される。
前のsnippetでは2番目のargument {} を渡した。そのためデフォルト{ y: 10 } valueは使われず、 { y } destructuringが渡された{} 空のobject valueに対して発生する。
次は { y } = { y: 10 }
と{ x = 10 } = {}
を比べる。
xのフォームの使用方法については、最初のfunction argumentが省略されているか、undefinedなら、{} 空のempty object のデフォルトが適用される。デフォルトの{}か、渡したもののどちらかが、つまり最初のargument positionにあるvalueはなんでも、{x = 10}で破棄される。そしてx propertyが発見されるならチェックし、発見されない(またはundefined)ならば、デフォルトvalueの10がxの named parameter に適用される。
コードで確認しよう。
function f6({ x = 10 } = {}, { y } = { y: 10 }) {
console.log( x, y );
}
f6(); // 10 10
f6( undefined, undefined ); // 10 10
f6( {}, undefined ); // 10 10
f6( {}, {} ); // 10 undefined
f6( undefined, {} ); // 10 undefined
f6( { x: 2 }, { y: 3 } ); // 2 3
一般に、x parameterのデフォルト動作は、yと比較して、より望ましく、合理的なケースであると思われる。なぜ{ x = 10} = {} フォームが{y} = {y : 10} フォームと異なるかを理解するのが重要。
Nested Defaults: Destructured and Restructured
最初は把握するのが難しいかもしれないが、nested object’s propertyのデフォルトをセットするための面白いイディオムがある。object destructuringとrestructuringを一緒に使う。
nested object structure内の一連のデフォルトを考えてみよう。
// taken from: http://es-discourse.com/t/partial-default-arguments/120/7
var defaults = {
options: {
remove: true,
enable: false,
instance: {}
},
log: {
warn: true,
error: true
}
};
configと呼ばれるobjectがあるとして、これらのobjectのいくつかは適用されるが、おそらく全てではない。そして欠落している場所全てのデフォルトをこのobjectに設定したいが、すでに存在する特定の設定を上書きしない。
var config = {
options: {
remove: false,
instance: null
}
};
もちろんマニュアルでもでき、過去にはやっていただろう。
config.options = config.options || {};
config.options.remove = (config.options.remove !== undefined) ?
config.options.remove : defaults.options.remove;
config.options.enable = (config.options.enable !== undefined) ?
config.options.enable : defaults.options.enable;
...
このタスクへの割り当て上書きアプローチを好む人もいるかもしれない。ES6のobject.assign(..) utilityを使い、defaultから最初のpropertyにクローンするように誘惑されるかもしれない。そして、configからクローンされたpropertyで上書きされるだろう。
config = Object.assign( {}, defaults, config );
Object.assign(..)はshallowで、defaults.optionsをコピーするときに、それのobject referenceをコピーするという意味で、object の propertyをconfig.options objectに深くクローン化しない。Object.assign(..)は、期待しているdeep クローンを得るために、objectのツリーを全てのレベルで適用される必要がある。(再帰的に並べ替えられる)
Note:
多くのJS utility library/frameworkはそれら自体のオプションをobjectのdeep cloningに供給するが、それらのアプローチはここで議論する範囲を超えている。
デフォルトでES6 object destructuringが役立つか調べよう。
config.options = config.options || {};
config.log = config.log || {};
({
options: {
remove: config.options.remove = defaults.options.remove,
enable: config.options.enable = defaults.options.enable,
instance: config.options.instance = defaults.options.instance
} = {},
log: {
warn: config.log.warn = defaults.log.warn,
error: config.log.error = defaults.log.error
} = {}
} = config);
Object.assign(..) の false promise ほどよくはないが、(それはshallowだけ) fair bitによるマニュアルアプローチよりは良い。しかし、残念ながら冗長で反復的だ。
前回のsnippetのアプローチは、property === undefined をチェックし、assignmentの決定を行うために、destructuringとデフォルトメカニズムをハックするので、動作する。それはconfigを destructuringしているという点ではトリック(スニペット最後の = config を参照)だが、config.options.enable assignment referenceを使用して、全てのdestructured valueをconfigに再割り当てする。
何かよくできるかどうか見てみよう。
以下のトリックはdestructuringされている全てのさまざまpropertyが一位に命名されていることを知っているなら、最も効果的だ。そうでなければ、まだそれを行えるが、よくはない。段階的にdestructuringをするか、一位のローカルvariableを一時的なエイリアスとして作成する必要がある。
もし全てのpropertyをtop-level variableにdestructureするなら、すぐに元のnested object structureを再構成することができる。
しかし、それらの一時的なvariableはscopeを汚染するだろう。そのため、block scope(このチャプター前半の”Block-Scoped Declarations”参照)を、一時的な{} 囲みblockで使用する。
// merge `defaults` into `config`
{
// destructure (with default value assignments)
let {
options: {
remove = defaults.options.remove,
enable = defaults.options.enable,
instance = defaults.options.instance
} = {},
log: {
warn = defaults.log.warn,
error = defaults.log.error
} = {}
} = config;
// restructure
config = {
options: { remove, enable, instance },
log: { warn, error }
};
}
Note:
一般的な{} blockとlet declarationの代わりに、arrow IIFEをscope enclosureに使い達成することもできる。destructuring assignment/defaultは、parameter listにあり、restructuringはfunction bodyのreturn statementにある。
restructuring部分の { warn, error } syntaxが新しく見えるかもしれない。これは”concise property”と呼ばれ、次のセクションで説明する。
Object Literal Extensions
ES6は{ .. } object literalにいくつかの重要で便利な拡張をする。
Concise Properties
このフォームのdeclaring object literalは知っているはず。
var x = 2, y = 3,
o = {
x: x,
y: y
};
lexical identifierと同じ名前のpropertyを定義する必要があるなら、 x: x からxに短縮できる。
var x = 2, y = 3,
o = {
x,
y
};
Concise Methods
今のpropertyと同じで、object literal内のpropertyに関連づけられたfunctionも、便宜上concise formをしている。
古いやり方
var o = {
x: function(){
// ..
},
y: function(){
// ..
}
}
ES6以降
var o = {
x() {
// ..
},
y() {
// ..
}
}
Warning:
x() { .. } が x: function(){ .. } のただの省略に見える一方で、concise methodは旧式ではなかった特別な動作を持つ。具体的には superを許可する。(詳しくはこのチャプターであとで出てくる”Object super
" 参照)
generatorもconcise method フォームを持つ。
var o = {
*foo() { .. }
};
Concisely Unnamed
手軽な省略が魅力的な一方、気をつける点がある。まずはES6以前のコードを調べてみよう。concise methodを使うために、リファクタをしようとするかもしれない。
function runSomething(o) {
var x = Math.random(),
y = Math.random();
return o.something( x, y );
}
runSomething( {
something: function something(x,y) {
if (x > y) {
// recursively call with `x`
// and `y` swapped
return something( y, x );
}
return y - x;
}
} );
これは明らかによくないコードだ。ただ2つの乱数を作り、より大きなものから小さいものを引き算する。しかし、重要なのはここでやったことではない、どうやって定義したかだ。object literalとfunction definitionにフォーカスすると
runSomething( {
something: function something(x,y) {
// ..
}
} );
どうしてsomething:とfunction something 両方がいるのか。実際に異なる目的で必要とされている。property somethingはo.something(..)をどうやって呼べるかで、いわゆる公共名のようなものだ。しかし、2つ目の something は、再起を目的としたその内部自身のfunctionを参照するためのlexical nameだ。
どうしてreturn something(y,x) 行がfunctionを参照するためにsomethingが必要なのか。return o.something(y,x)、もしくはそれのソートのsomethingといった objectにlexical nameがない。
これは実際にobject literalがidentifying nameを持つ時に一般的なやり方だ。
var controller = {
makeRequest: function(..){
// ..
controller.makeRequest(..);
}
};
おそらくこれはいい考えではない。name controllerはいつも疑問のobjectを指すと想定している。しかし、それはよくないかもしれない。makeRequest(..) functionは外側のコードをコントロールしないので、強制的にそうすることができない。
他の人はthisを使ってこれらを定義するのを好む。
var controller = {
makeRequest: function(..){
// ..
this.makeRequest(..);
}
};
いつもcontroller.makeRequest(..)としてmethodを呼び出すなら、動作するだろう。しかし、もしこのようにするなら、thisの結びを持っている。
btn.addEventListener( "click", controller.makeRequest, false );
もちろんイベントに結ぶためのhandler referenceとしてcontroller.makeRequest.bind(controller)を渡すことで解決できる。しかし、これはあまりよろしくない。
this.makeRequest(..)の内部コールがnested functionから作成される必要がある場合はどうだろう。this が危険と結ぶのを他に持っている。下記のようなhacky var self= this で解決することが多いだろう。
var controller = {
makeRequest: function(..){
var self = this;
btn.addEventListener( "click", function(){
// ..
self.makeRequest(..);
}, false );
}
};
さらによくない。
Note:
this binding についてさらに詳しくは、Chapters 1–2 of the this & Object Prototypes title of this series 参照。
ではconcise methodが何をできるか。something() methodの定義を思い出そう。
runSomething( {
something: function something(x,y) {
// ..
}
} );
2つ目のsomethingは、いつもfunction自体を指すとても便利なlexical identifierを供給し、再帰、event binding/unbindingなどのための完全な参照を与える。thisや信頼できないobject referenceを使用したりとして混乱させることがない。
ここではES6のconcise methodフォームへのfunction referenceをリファクタしようとしている。
runSomething( {
something(x,y) {
if (x > y) {
return something( y, x );
}
return y - x;
}
} );
このコードが壊れることを除き、一目瞭然だ。return something(..) コールはsomething identifierを探さない。そしてReferenceErrorを得る。
上記のES6 snippertは次のように解釈される。
runSomething( {
something: function(x,y){
if (x > y) {
return something( y, x );
}
return y - x;
}
} );
concise method definitionは something: function(x,y) を意味する。頼っていた2つ目のsomthing がどうやって省略されるのか。つまり、concise methodはanonymous function expressionを意味する。
Note:
=> のarrow functionが解決策と思ったかもしれないが、それらもまたanonymous function expressionと同様に不十分だ。
something(x,y) concise methodはanonymousにはならない。ES6 function ネームの推論ルールについては、”Function Names”参照。それは再起のために助けてくれないが、デバッグには少なくとも役立つ。
concise methodは短くて便利だが、再起やイベントのbinding/unbindingの解除が必要ない場合のみ使用すること。さもなければ、function something(..) method definitionを機能させる。
メソッドの多くはconcise method definitionの恩恵を受けるだろう。名前のつけられないハザードにのみ注意が必要。
ES5 Getter/Setter
ES5はgetter/setter literal formを定義したが、主に新しいsyntaxを処理するためにtranspilerが不足するため、そこまで使われていなかったようだ。ES6の新しい機能ではないが、おそらくES6になってからとても使いやすくなったので、そのフォームを軽く説明する。
var o = {
__id: 10,
get id() { return this.__id++; },
set id(v) { this.__id = v; }
}
o.id; // 10
o.id; // 11
o.id = 20;
o.id; // 20
// and:
o.__id; // 21
o.__id; // 21 -- still!
これらのgetter、setter literal formはclassにも存在する。チャプター3参照。
Warning:
明らかではないが、setter literalは1つだけのdeclared parameterを持たなければいけない。それを省略するか、他のものをリスト化するのは不正な構文だ。この1つの要求されたparameterは、destructuringと、default(set id({ id: v = 0}) { .. } のような)に使うことができるが、gather/rest … は( set id (…v) { .. }を許可されない。
Computed Property Names
おそらく下記のsnippetのようなシチュエーションにいるだろう。ここではある種のexpressionに由来するproperty nameが1つ以上あるので、object literalに入れることができない。
var prefix = "user_";
var o = {
baz: function(..){ .. }
};
o[ prefix + "foo" ] = function(..){ .. };
o[ prefix + "bar" ] = function(..){ .. };
..
ES6はsyntaxを計算するexpressionに指定することを許可するobject literal definitionに加える。その結果は割り当てられたproperty nameになる。
var prefix = "user_";
var o = {
baz: function(..){ .. },
[ prefix + "foo" ]: function(..){ .. },
[ prefix + "bar" ]: function(..){ .. }
..
};
有効なexpressionは、object literal definitionのproperty nameの位置にある[ .. ]の内部に現れる。
おそらく、property nameの最も一般的な使い方は、Symbolになる。
var o = {
[Symbol.toStringTag]: "really cool thing",
..
};
Symbol.toStringTagは特別なbuilt-in valueだ。それは[ .. ]syntaxで評価し、特別なproperty nameに本当にクールなvalueを割り当てることができる。
var o = {
["f" + "oo"]() { .. } // computed concise method
*["b" + "ar"]() { .. } // computed concise generator
};
Setting [[Prototype]]
ときどきobject literalを宣言すると同時に、objectの[[Prototype]]を割り当てることが役に立つ。下記のextensionは長らくJSエンジニアにとってスタンダードではなかったが、ES6から標準化された。
var o1 = {
// ..
};
var o2 = {
__proto__: o1,
// ..
};
o2はnormal object literalで宣言されたが、[[Prototype]]は01にもまたリンクしている。ここでの The__proto__ property nameはstring “__proto__”にもまたなる。しかし、それはcomputed property nameにはならない。
__proto__は議論の余地がある。長年独自のextensionとしてJSにあったが、最終的に標準化された。控えめに言うとES6では控えめに見ている。多くのディベロッパーはこれ以上使うべきでないと感じている。実際に、ES6のAnnex Bは互換性の理由で標準化する必要があると感じられたJSのリストのセクションだ。
Warning:
object literal definitionのキーとして__proto__を少し支持しているが、それをo.__proto__ のように object property formで使うことは支持していない。そのフォームはgetterでsetter(互換性の理由から)だが、明らかにより良いオプションがある。詳しくは this & Object Prototypes title of this series 参照。
existing objectに[[Prototype]]をセットすること、ES6 utility Object.setPrototypeOf(..)を使うことができる。
var o1 = {
// ..
};var o2 = {
// ..
};Object.setPrototypeOf( o2, o1 );
Note:
chapter 6 で改めてObjectについては議論する。”Object.setPrototypeOf(..) static function” はObject.setPrototypeOf(..)に追加詳細を提供する。また、o2 prototypicallyをo1に関わる他のフォームの”object.assign(..)” Static function” も参照。
Object super
superは典型的にclassにのみ関係するものだと考えられている。しかし、JSのclassless-objects-with-prototypes natueのため、superは同じように効果的で、ほとんど同じ動作で、単純なobject’s concise methodである。
var o1 = {
foo() {
console.log( "o1:foo" );
}
};var o2 = {
foo() {
super.foo();
console.log( "o2:foo" );
}
};Object.setPrototypeOf( o2, o1 );o2.foo(); // o1:foo
// o2:foo
Warning:
superはconcise method内でのみ許可され、regular function expression propertyではない。また、super.XXX form(property/method accessのために)のみでも許可されるが、super() formではされない。
o2.foo() methodのsuper referenceは、o2に静的にロックされ、特にo2の[[Prototype]]にロックされる。ここのsuperは基本的にObject.getPrototypeOf(o2)である。もちろんo1に解決され、o1foo()を発見し呼び出す。
superについてさらに詳しくは "Classes" in Chapter 3. 参照。
Template Literals
多くのディベロッパーは、templateは多くのテンプレートエンジン(Mustache, Handlebarなど)で提供される繰り返し使え、レンダリングできるテキストの一部として考えている。ES6のテンプレートの仕様は、際レンダリング可能なinline templateを宣言する方法のように、同じことを暗示する。
そのため、interpolated string literal(またはinterpoliteral(補間文字))と呼ばれるべき名前に変える。
すでに ” や ’ の区切り文字をstring literalを宣言している。そしてこれらはsmart stringではないことも知っており、内容がinterpolation expression(補完)に解析されることがわかる。
しかし、ES6ではstring literalの新しいtypeを導入し、区切り文字として` back tickを使っている。これらのstring literalは、基本的なstring interpolation expressionを埋め込むことができ、自動的に解析、評価される。
ES6以前は
var name = "Kyle";
var greeting = "Hello " + name + "!";
console.log( greeting ); // "Hello Kyle!"
console.log( typeof greeting ); // "string"
ES6では
var name = "Kyle";
var greeting = `Hello ${name}!`;
console.log( greeting ); // "Hello Kyle!"
console.log( typeof greeting ); // "string"
` .. `
を文字列の周りに使い、string literalとして解釈される。しかし、${} のexpressionは、ただちにinlineだと分析、評価される。これをinterpolationと呼ぶ。
Warning:
typeof greeting == “string” は、これらの実在を特別なtemplate valueとして考えるのが、なぜ重要でないか。それは評価されていないフォームを何かに割り当てて再利用することはできないからだ。` .. ` string literalはinlineで自動的に評価されると言う点で、IIFEによく似ている。`..` string literalの結果は、単にstringにすぎない。
interpolated string literalの素晴らしい点は、複数業に分割することが許可されていることだ。
var text =
`Now is the time for all good men
to come to the aid of their
country!`;
console.log( text );
// Now is the time for all good men
// to come to the aid of their
// country!
interpolated string literalの開業は、string valueに保存された。
literal valueにexplicit escape sequencesが現れない限り、\r carriage return 文字のvalue(code point U+000D)か、\r\n carriage return + シーケンスのvalue(code point U+000D と U+000A)は、どちらも\n line feed character (cod point U+000A)に標準化される。
この正規化はまれであり、JSファイルにテキストをコピペする場合のみ発生する。
Interpolated Expressions
有効なexpressionはfunction call, inline function, interpolated string literalを含むinterpolated string literalの ${ .. }内に現れることができる。
function upper(s) {
return s.toUpperCase();
}
var who = "reader";
var text =
`A very ${upper( "warm" )} welcome
to all of you ${upper( `${who}s` )}!`;
console.log( text );
// A very WARM welcome
// to all of you READERS!
ここでは `${who}s`の中にinterpolated string literalを使う方が、who + “s”に対して、who variableと”s” stringを組み合わせるより少し便利だ。nested interpolated string literalは役に立つ。しかし、そのようなことを頻繁にやっていたり、深いlevelのnestingにしていることがわかったら、注意が必要。
そうであれば、string valueの生産には、いくつかの抽象化の恩恵を受けることができる。
Warning:
このような新しく発見された力でコードを読みやすくするには、注意が必要。default value expressionと、destructuring assignment expressionのように、やるべきことを意味しない場合があるからだ。コードがチームメンバーや自分自身よりも巧妙になるような新しいES6のトリックでは、絶対に外に出てはいけない。
Expression Scope
expressionのvariableを解決するために使われるscopeについて。interpolated string literalは、IIFEと同じ分類だ。scopeの振る舞いも同様に説明されているように考えることがわかる。
function foo(str) {
var name = "foo";
console.log( str );
}
function bar() {
var name = "bar";
foo( `Hello from ${name}!` );
}
var name = "global";
bar(); // "Hello from bar!"
bar() functionの内部で` .. ` string literalが表現されているとき、scopeはそれがvariable “bar” の bar() の name variableを発見し、利用することができる。global name でも foo(..)のnameでもない。言い換えると、interpolated string literalはただのlexically scopeが現れる場所で、dynamically scopeではない。
Tagged Template Literals
tagged string literalについて。
これはES6が提供する有用なトリックの1つ。奇妙で実用的でない部分もあるだろうが、tagged string literalは有用だろう。
function foo(strings, ...values) {
console.log( strings );
console.log( values );
}
var desc = "awesome";
foo`Everything is ${desc}!`;
// [ "Everything is ", "!"]
// [ "awesome" ]
まずは見慣れない foo` Everything… ` ; について。
これは( .. ) を必要としない特別なfunctionの1つ。 ` .. ` string literalの前にある foo は、コールされるべきfunctionだ。実際に、functionの結果のexpressionはなんでも良い。functionが他のfunctionをコールするものでも良い。
function bar() {
return function foo(strings, ...values) {
console.log( strings );
console.log( values );
}
}
var desc = "awesome";
bar()`Everything is ${desc}!`;
// [ "Everything is ", "!"]
// [ "awesome" ]
しかし、string literalのtagとして呼び出されたときにfoo(..) functionに渡されるのは何か。
stringsと呼ばれる最初のargumentは、全てplain stringsのarrayだ。(interpolated expressionの間にある要素すべて)
strings arrayから2つのvalueを得る。 “Everything is” と “!” だ。
便宜上、後続の全てのargumentを… gather/rest operatorを使ってvalueというarrayに集める。strings parameterの後にここの名前付きparameterとして残すこともできる。
value arrayに集められたargumentは、string literalで発見されたすでに評価されたinterpolation expressionの結果だ。そのため、valueのelementは”awsome”のみだ。
もしstringsのvalueを接合するならば、valueの中のvalueはセパレーターとして、これら2つのarrayを考えて良い。そして、全て集めたいなら、interpolated string valueを得ることができる。
tagged string literalはinterpolation expressionが評価された後だが、最後のstringがコンパイルされる前のプロセスのようなもので、literalからstringを作る制御を強化する。
string literal tag function (前のsnippetのfoo(..) )は、適切なstring valueを計算し、返すので、tagged string literalをuntagged string literalのようなvalueとして使うことができる。
function tag(strings, ...values) {
return strings.reduce( function(s,v,idx){
return s + (idx > 0 ? values[idx-1] : "") + v;
}, "" );
}
var desc = "awesome";
var text = tag`Everything is ${desc}!`;
console.log( text ); // Everything is awesome!
このsnippetでは、tag(..)はpass-through operationで、いかなる特殊な修正も実行しないが、loop overとsplice/interleave stringにだけreduce(..)を使い、valuesはuntagged string literalが実行したのと同じ方法で返す。
実践的な使い方は、ここでの議論の範疇を超えている先進的なものがあるが、numberをUSドルとしてフォーマットするといった単純な考えがある。
function dollabillsyall(strings, ...values) {
return strings.reduce( function(s,v,idx){
if (idx > 0) {
if (typeof values[idx-1] == "number") {
// look, also using interpolated
// string literals!
s += `$${values[idx-1].toFixed( 2 )}`;
}
else {
s += values[idx-1];
}
}
return s + v;
}, "" );
}
var amt1 = 11.99,
amt2 = amt1 * 1.08,
name = "Kyle";
var text = dollabillsyall
`Thanks for your purchase, ${name}! Your
product cost was ${amt1}, which with tax
comes out to ${amt2}.`
console.log( text );
// Thanks for your purchase, Kyle! Your
// product cost was $11.99, which with tax
// comes out to $12.95.
もしnumber valueがvalue arrayにあれば、それの前に”$”を置いて、小数点第二位以下はtoFixed(2)でフォーマットする。それ以外は、value pass-throughを変更しない。
Raw Strings
前のsnippetで、tag functionはstringsと呼ぶ最初のargumentを受け取る。これはarrayだ。しかし、全てのstringsの生で未処理のバージョンの追加データが含まれる。これらのraw string valueは .raw propertyを使ってアクセスできる。
function showraw(strings, ...values) {
console.log( strings );
console.log( strings.raw );
}
showraw`Hello\nWorld`;
// [ "Hello
// World" ]
// [ "Hello\nWorld" ]
valueのraw バージョンは、raw escaped \n シーケンス(\とnは別の文字)を保持するが、処理されたバージョンはそれを単一の改行文字と考える。しかし、前述のline-ending normalizationはvalueに適用される。
ES6はString.raw(..)といった、string literal tagとして使われることができるbuilt-in functionがある。それはシンプルにstrings valueのrawバージョンを渡す。
console.log( `Hello\nWorld` );
// Hello
// World
console.log( String.raw`Hello\nWorld` );
// Hello\nWorld
String.raw`Hello\nWorld`.length;
// 12
string literal tagの他の用途は、国際化、ローカル化などの特別な処理が含まれていた。
続く。