シンボル構文

【JavaScript】 定義済みシンボルの使い方

更新日:2021/01/11

JavaScriptにはシンボルという機能があり、その機能を使ってあらかじめいくつかのシンボルが定義されています。
これらのシンボルはウェルノウン(well known)シンボルと呼ばれています。
ここでは、そのウェルノウンシンボルについてお伝えします。

 

ウェルノウンシンボルはSymbolオブジェクトのプロパティとして定義されている

ウェルノウンシンボルはSymbolオブジェクトのプロパティとして定義されています。
JavaScriptにはグローバルシンボルという機能がありますが、これとは別のものです。

ウェルノウンシンボルはイメージ的に、次のように定義されています。

Symbol{
   iterator : Symbol(),
   asyncIterator : Symbol(),
   ・
   ・
   ・
}

 

ウェルノウンシンボル一覧

 

各ウェルノウンシンボルの使い方

Symbol.asyncIterator

Symbol.asyncIteratorは、オブジェクトに非同期イテレータを実装するために使用します。


function btnWait(n){
 
    return new Promise( resolve => {
        const btn = document.getElementById("btn");
        
        btn.addEventListener("click",function clickEvent(){
            btn.removeEventListener("click",clickEvent);
            resolve(n);
        });
    });
}
const obj = {
    *[Symbol.asyncIterator]() {
        yield btnWait(1);
    }
}
async function fun1(){
    for await( let data of obj){
        console.log( data ); // 結果: 1
    }
}
fun1();

詳しくは【JavaScript】 非同期イテレータと非同期ジェネレーターについて理解しようを見てください。

Symbol.hasInstance

Symbol.hasInstanceは、instanceof演算子の結果を制御します。

具体的には、引数としてオブジェクトを受け取り真偽値を返す関数を指定します。


const obj1 = {};
const obj2 = {};

const obj3 ={
    [Symbol.hasInstance](instance) {
        return instance === obj1;
    }
};

console.log(obj1 instanceof obj3); // true
console.log(obj2 instanceof obj3); // false

Symbol.isConcatSpreadable

オブジェクトがArray.prototype.concat()の引数として渡されたとき、オブジェクトの[Symbol.isConcatSpreadable]プロパティを参照して、オブジェクトのプロパティを展開するかどうかを決定します。

true: 展開する
false: 展開しない

Array.prototype.concat()は、元となる配列に引数で指定した配列を追加するメソッドです。

初期状態のオブジェクトは、[Symbol.isConcatSpreadable]プロパティが定義されていません。
そのため既定の動作が行われますが、オブジェクトが配列の場合と、それ以外では動作が異なります。

配列(Array)オブジェクトの規定動作

配列(Array)オブジェクトは、規定値がtrueとみなされるため、展開されます。


const Arry = [ "A" , "B" , "C" ];
const Arry2 = [ "E" , "F" , "G" ];

console.log( Arry2[Symbol.isConcatSpreadable] ); // undefined

console.log( Arry.concat(Arry2) ); // 結果:  [ "A", "B", "C", "E", "F", "G" ]

[Symbol.isConcatSpreadable]falseをセットすると、展開されずそのまま追加されます。


Arry2[Symbol.isConcatSpreadable] = false;

console.log( Arry.concat(Arry2) ); // 結果:  [ "A", "B", "C", [ "E", "F", "G" ] ]

配列以外のオブジェクトの規定動作

その他のオブジェクトは、規定値がfalseとみなされるため、そのまま追加されます。


const Arry = [ "A" , "B" , "C" ];
const obj = {
    data1:"f",
    data2: "e",
}

console.log( obj[Symbol.isConcatSpreadable] ); // undefined

console.log( Arry.concat(obj) ); // 結果: [ "A", "B", "C", { data1: "f", data2: "e"} ]

[Symbol.isConcatSpreadable]trueをセットすると、展開されますが配列としてみなすことができないプロパティは無視されます。


obj[Symbol.isConcatSpreadable] = true;

console.log( Arry.concat(obj) ); // 結果: [ "A", "B", "C" ] ← objのプロパティが無視された!

数値とlengthプロパティを持つ配列のようなオブジェクトは展開されます。


const obj2 = {
    0:"d",
    1:"e",
    99:"f",
    abc:"g",
    length: 3,
    [Symbol.isConcatSpreadable]: true,
}
console.log( Arry.concat(obj2) ); //  結果:  [ "A", "B", "C", "d", "e", <1 empty slot> ]

上の例はプロパティの99とabcが展開されていません。
また、結果の最後がemptyとなっています。

これはlengthプロパティの値を元に、展開するデータが選択されたからです。

Symbol.iterator

Symbol.iteratorは、オブジェクトにイテレータを実装するために使用します。


const obj = {
    [Symbol.iterator]() {
        yield 1;
    }
}
async function fun1(){
    for( let data of obj){
        console.log( data ); // 結果: 1
    }
}
fun1();

詳しくは【JavaScript】 Iterator(イテレーター)とは?避けて通りたいけど説明してみるを見てください。

Symbol.match

Symbol.matchは、正規表現のマッチング処理の変更と正規表現機能の無効化の二つの意味があります。

正規表現のマッチング処理の変更

オブジェクトがString.prototype.match()の引数として指定されたとき、オブジェクトの[Symbol.match]プロパティが呼び出されます。

このとき[Symbol.match]は、正規表現のマッチング処理をおこなう関数を指定します。

マッチングアルゴリズムを変更する

この例は、[Symbol.match]が受け取る引数と、返す値を確認しています。


const reg = /^abc/;
reg[Symbol.match] = function (...arg){
    console.log( arg ); // [ "abcdefg" ]
    
          // 既定の[Symbol.match]を呼び出す
    const result = RegExp.prototype[Symbol.match].call(this, ...arg );
    
    console.log( result ); // { 0: "abc" , groups: undefined ... }
    return result;
};

const result = "abcdefg".match( reg ); // reg[Symbol.match]が呼び出される
                                         
console.log( result ); //  { 0: "abc" , groups: undefined ... }

また実用的かどうかわかりませんが、通常のオブジェクトに正規表現マッチングを組み込むこともできます。


const reg = {
    regData: /^abc/,
    [Symbol.match](...arg){
        console.log( arg );
        return RegExp.prototype[Symbol.match].call(this.regData, ...arg );
    }
};

const result = "abcdefg".match( reg ); // [ "abcdefg" ]
console.log( result ); //  { 0: "abc" , groups: undefined ... }

※...argはスプレッドまたはレスト構文です。
※call()についてはこちら

正規表現機能の無効化

[Symbol.match]にfalseをセットすると、引数としてRegExpを受け付けないString.prototype.includes()などのメソッドで、RegExpを文字列として扱うようになります。

String.prototype.includes()の引数にRegExpオブジェクトを指定すると、TypeErrorになる。


const reg = /^abc/;
const result1 = "***/^abc/***".includes( reg ); // TypeError:

[Symbol.match] = falseにすることで、RegExpオブジェクトが文字列として扱われる。


reg[Symbol.match] = false;

const result2 = "***/^abc/***".includes( reg );
console.log( result2 ); // true

Symbol.matchAll

オブジェクトがString.prototype.matchAll()の引数として指定されたとき、オブジェクトの[Symbol.matchAll]が参照されます。

[Symbol.matchAll]には、正規表現で一致した文字列を順番に返すイテレータを返す関数を指定します。


const reg = /\d/g;

reg[Symbol.matchAll] = function* (str){
          // 既定の[Symbol.matchAll]を呼び出す
    const result = RegExp.prototype[Symbol.matchAll].call(this,str);
    for( s of result ){
        yield "****" + s + "*****";
    }
};
for( res of "abc1def2ghi3".matchAll(reg)){
    console.log( res );
      // 結果: ****1*****
      //       ****2*****
      //       ****3*****
}

※上の例はイテレータジェネレーター関数で実装しています。

Symbol.replace

オブジェクトをString.prototype.replace()の引数として渡したとき、オブジェクトの[Symbol.replace]プロパティに定義されている関数が実行されます。

関数は、実際に置き換えをおこなう必要があります。

この例は、[Symbol.replace]に渡される引数と、実際に返すべき返す値を確認しています。


const reg = /\d/g;

reg[Symbol.replace] = function (...arg){

    console.log( arg ); // [ "abc1def2ghi3", "[Number]" ]
    
                 // 既定の[Symbol.replace]を呼び出す
    const result = RegExp.prototype[Symbol.replace].call( this , ...arg );
    console.log( result ); // abc[Number]def[Number]ghi[Number]
    return result;
};

const result = "abc1def2ghi3".replace( reg , "[Number]" );
console.log( result ); // abc[Number]def[Number]ghi[Number]

オブジェクトをString.prototype.search() の引数として渡したとき、オブジェクトの[Symbol.search]プロパティに定義されている関数が実行されます。

関数は、実際にマッチした箇所のインデックスを返す必要があります。

この例は、[Symbol.search]に渡される引数と、実際に返すべき返す値を確認しています。


const reg = /\d/;

reg[Symbol.search] = function (...arg){

    console.log( arg ); // [ "abc1def2ghi3" ]

                 // 既定の[Symbol.search]処理を呼び出す
    const result = RegExp.prototype[Symbol.search].call( this , ...arg );
    console.log( result ); // 3
    return result;
};

const result = "abc1def2ghi3".search( reg  );
console.log( result ); // 3

Symbol.species

Symbol.speciesは、コンストラクター関数を指定します。

一部の標準メソッドは、メソッド内部で新規オブジェクトを作成しています。
その際、[Symbol.species]にセットされたコンストラクター関数を使用します。

例えばArray.prototype.map()は、配列の要素に何らかの処理を加えた後、新規の配列に格納して返します。

その際、次のように空の配列を作成するのではなくて、オブジェクトの[Symbol.species]を使用します。

× const newArray = []; // const newArray = new Array() と同じ
 const newArray = new this[Symbol.species]();

実際にはArrayの[Symbol.species]はArrayコンストラクター関数がセットされているので、実質的にはconst newArray = [];と同じです。

Arrayから派生させたオブジェクトを例に、動作を確認してみます。


class myArray1 extends Array{
    constructor() {
        super();
        this.text ="オリジナルのオブジェクトです";
    }
    func1(){return "myArray1"}
}
const obj = new myArray1();
obj[0]=10;obj[1]=20;

console.log( obj ); 
    // 結果: { 0: 10 , 1: 20 , length: 2 , text: "オリジナルのオブジェクトです" }
console.log( obj.func1() ); // "myArray1"

const ary = obj.map( e=>e*2 );
console.log( ary );
    // 結果: { 0: 20 , 1: 40 , length: 2 , text: "オリジナルのオブジェクトです" }
console.log( ary.func1() ); // "myArray1"

Arrayから派生させたmyArray1は配列としての機能を持っているので、mapメソッドが使用できます。

mapメソッドの結果を見ると、myArray1固有のtextプロパティとfunc1メソッドを持っているため、mapメソッドの内部でArrayではなくてmyArray1が新規作成されているのがわかります。

次にArrayから派生させたmyArray2を作成し、myArray1の [Symbol.species]メソッドにmyArray2を返すコードを追加してみます。


class myArray1 extends Array{
    constructor() {
        super();
        this.text ="オリジナルのオブジェクトです";
    }
    func1(){return "myArray1"}
    static get [Symbol.species]() { return myArray2; }
}
class myArray2 extends Array{
    constructor() {
        super();
        this.text ="メソッド内部で作成されたオブジェクトです";
    }
    func1(){return "myArray2"}
}
const obj = new myArray1();
obj[0]=10;obj[1]=20;

console.log( obj ); 
    // 結果: { 0: 10 , 1: 20 , length: 2 , text: "オリジナルのオブジェクトです" }
console.log( obj.func1() ); // "myArray1"

const ary = obj.map( e=>e*2 );
console.log( ary );
    // 結果: { 0: 20 , 1: 40 , length: 2 , text: "メソッド内部で作成されたオブジェクトです" }
console.log( ary.func1() ); // "myArray2"

mapメソッドの内部では、[Symbol.species]()が返したmyArray2を使って新規オブジェクトを作成します。
そのため、返されたオブジェクトがmyArray2になっています。

[Symbol.species]を参照するメソッド

Array.prototype.concat()
Array.prototype.filter()
Array.prototype.flat()
Array.prototype.flatMap()
Array.prototype.map()
Array.prototype.slice()
Array.prototype.splice()
Promise.prototype.finally()
Promise.prototype.then()
ArrayBuffer.prototype.slice()

※Arrayは、各TypedArrayオブジェクトも含みます。

Symbol.split

オブジェクトをString.prototype.split()の引数として渡したとき、オブジェクトの[Symbol.split]プロパティに定義されている関数が実行されます。

関数は、実際に区切り文字列で分割した結果を返す必要があります。

この例は、[Symbol.split]に渡される引数と、実際に返すべき返す値を確認しています。


const reg = /\d/;

reg[Symbol.split] = function (...arg){

    console.log( arg ); // [ "abc1def2ghi3", undefined ]

    // 既定の[Symbol.split]処理を呼び出す
    const result = RegExp.prototype[Symbol.split].call( this , ...arg );
    console.log( result ); // [ "abc", "def", "ghi", "" ]
    return result;
};

const result = "abc1def2ghi3".split( reg  );
console.log( result ); // [ "abc", "def", "ghi", "" ]

Symbol.toPrimitive

Symbol.toPrimitiveは、状況に合わせてオブジェクトの暗黙的な変換をおこなう関数を指定します。


const obj = function (data){
    this.data = data;
}
obj.prototype[Symbol.toPrimitive]=function (hint){
    console.log( hint + ":" + this.data);
    switch (hint){
        case "number":
            return this.data;
        case "string":
            return this.data.toString();
        default: // "default"
            return "データ(" + this.data + ")";
    }
};

const obj1 = new obj(10);

console.log(10 * obj1 ); // hint:"number" 結果: 100
console.log(`${obj1}`);   // hint:"string" 結果: "10"

関数は"number"、"string"、"default"のどれかの値を持つ引数を一つ受け取ります。
その値を見て、返す値を決定します。

引数の値は、コードの流れから決定されます。
数値が求められる時は"number"を、文字列が求められる時は"string"を、判断できないときは"default"になります。

+演算子は数値計算にも文字列連結にも使用されるため判断ができません。そのため"default"になります。


console.log(10 + obj1 ); //  hint:"default" 結果: "10(デフォルト)"

Symbol.toStringTag

Symbol.toStringTagは、オブジェクトのtoString()メソッドの出力結果の一部として使用されます。

出力結果の一部を変更する

toString()メソッドの出力結果は通常"[object Object]"です。
[Symbol.toStringTag] プロパティに文字列を指定すると、後半の"Object"を別の文字列に置き換えることができます。


   // 既定のtoString()結果
console.log( {}.toString() ); // "[object Object]"

  // toString()結果の一部を変更する
const obj = function (){}
obj.prototype[Symbol.toStringTag] = "***obj***";

console.log( (new obj).toString() ); // "[object ***obj***]"

出力結果の一部を動的に変更する

[Symbol.toStringTag]にゲッターを指定することで、出力結果の一部を動的に変更することもできます。


const obj = function (data){
    this.data = data;
}
Object.defineProperties(obj.prototype,
    {
        [Symbol.toStringTag]: {
            get:function (){return "obj:" + this.data; }
        }
   }
);

console.log( (new obj(10)).toString() );  // "[object obj:10]"
console.log( (new obj(100)).toString() ); // "[object obj:100]"

Symbol.unscopables

Symbol.unscopableswith構文で参照されるプロパティをオブジェクトから除外します。


const obj = {
    prop1:"a",
    prop2:"b",
    [Symbol.unscopables]:{
        prop1:true, // 除外する
        prop2:false // 除外しない
    }
};
let prop1 = "c";
let prop2 = "d";

with(obj){
    console.log( prop1 ); // "c" ←ローカル変数
    console.log( prop2 ); // "b" ←obj.prop2
}

更新日:2021/01/11

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。