かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript 配列のコピー

JavaScript の配列は参照なので、破壊的な変更を加えてしまうと副作用を発生させるので取り扱いには注意 (危機管理) が必要です。
副作用を発生させないために配列操作を行う時はコピーを行うことが多いですが、配列が入れ子だったり、配列の中にオブジェクトを入れたりということも多いと思うので、改めて浅いコピー (shallow copy) と深いコピー (deep copy) について試したのでメモ

配列は参照

別の変数に代入してもコピーされるわけではない (同じデータを参照しているだけ)

const array = [
  {name: 'Shamiko'},
  {name: 'Chiyoda Momo'},
];

const copyArray = array;
// 配列を代入した変数と元の配列は同一 (同じデータを参照している)
console.log(copyArray === array);
// => true
copyArray.push({name: 'Hinatsuki Mikan'});
copyArray[0].name = 'Ririsu';
console.log(copyArray);
// => [{name: 'Ririsu'}, {name: 'Chiyoda Momo'}, {name: 'Hinatsuki Mikan'}]
console.log(array);
// => [{name: 'Ririsu'}, {name: 'Chiyoda Momo'}, {name: 'Hinatsuki Mikan'}]

slice() でコピー

const array = [
  {name: 'Shamiko'},
  {name: 'Chiyoda Momo'},
];

const copyArray = array.slice();
// コピーされているので内容は同じでも同一ではなくなっている
console.log(copyArray === array);
// => false
copyArray.push({name: 'Hinatsuki Mikan'});
copyArray[0].name = 'Ririsu';
console.log(copyArray);
// => [{name: 'Ririsu'}, {name: 'Chiyoda Momo'}, {name: 'Hinatsuki Mikan'}]
console.log(array);
// => [{name: 'Ririsu'}, {name: 'Chiyoda Momo'}]

Array.slice()shallow copy
ref. Array.prototype.slice() - JavaScript | MDN

スプレッド構文 ( […Array] ) でコピー

const array = [
  {name: 'Shamiko'},
  {name: 'Chiyoda Momo'},
];

const copyArray = [...array];
console.log(copyArray === array);
// => false
copyArray.push({name: 'Hinatsuki Mikan'});
copyArray[0].name = 'Ririsu';
console.log(copyArray);
// =>[{name: 'Ririsu'}, {name: 'Chiyoda Momo'}, {name: 'Hinatsuki Mikan'}]
console.log(array);
// => [{name: 'Ririsu'}, {name: 'Chiyoda Momo'}]

スプレッド構文 [...Array]shallow copy
ref.

Array.from() でコピー

const array = [
  {name: 'Shamiko'},
  {name: 'Chiyoda Momo'},
];

const copyArray = Array.from(array);
console.log(copyArray === array);
// => false
copyArray.push({name: 'Hinatsuki Mikan'});
copyArray[0].name = 'Ririsu';
console.log(copyArray);
// =>[{name: 'Ririsu'}, {name: 'Chiyoda Momo'}, {name: 'Hinatsuki Mikan'}]
console.log(array);
// => [{name: 'Ririsu'}, {name: 'Chiyoda Momo'}]

Array.from()shallow copy ref. Array.from() - JavaScript | MDN

Object.assign() でコピー

const array = [
  {name: 'Shamiko'},
  {name: 'Chiyoda Momo'},
];

const copyArray = Object.assign([], array);
console.log(copyArray === array);
// => false
copyArray.push({name: 'Hinatsuki Mikan'});
copyArray[0].name = 'Ririsu';
console.log(copyArray);
// =>[{name: 'Ririsu'}, {name: 'Chiyoda Momo'}, {name: 'Hinatsuki Mikan'}]
console.log(array);
// => [{name: 'Ririsu'}, {name: 'Chiyoda Momo'}]

Object.assign()shallow copy
ref. Object.assign() - JavaScript | MDN

JSON.stringify() で文字列に変換してコピー

JSON.stringify() で文字列に変換して、JSON.parse() で再び配列に戻す方法

const array = [
  {name: 'Shamiko'},
  {name: 'Chiyoda Momo'},
];

const copyArray = JSON.parse(JSON.stringify(array));
console.log(copyArray === array);
// => false
copyArray.push({name: 'Hinatsuki Mikan'});
copyArray[0].name = 'Ririsu';
console.log(copyArray);
// =>[{name: 'Ririsu'}, {name: 'Chiyoda Momo'}, {name: 'Hinatsuki Mikan'}]
console.log(array);
// => [{name: 'Shamiko'}, {name: 'Chiyoda Momo'}]

JSON.stringify() で一度文字列にしてしまえば、deep copy ができる

※ 但し、JSON.stringify()でのコピーは、Date オブジェクトや function など 文字列化すると壊れるオブジェクトが含まれていると正しくコピーできない
const array = [
  new Date(),
  () => { console.log('function') },
];

const copyArray = JSON.parse(JSON.stringify(array));
console.log(copyArray);
// => [ '2019-11-29T10:47:09.941Z', null ]
console.log(array);
// => [ 2019-11-29T10:47:09.941Z, [Function] ]

文字列化した際に、オブジェクトが壊れてしまう

lodash#cloneDeep でコピー

lodash ライブラリに含まれる cloneDeep メソッドを使ってコピー

const _ = require('lodash');
const array = [
  {name: 'Shamiko'},
  {name: 'Chiyoda Momo'},
  new Date(),
  {func() {console.log('foo');}
];

const copyArray = _.cloneDeep(array);
// => false
copyArray.push({name: 'Hinatsuki Mikan'});
copyArray[0].name = 'Ririsu';
console.log(copyArray);
// => [{name: 'Ririsu'}, {name: 'Chiyoda Momo'}, 2019-11-29T10:57:00.595Z, {func: [Function: func]}, {name: 'Hinatsuki Mikan'}]
console.log(arg);
// => [{name: 'Shamiko'}, {name: 'Chiyoda Momo'}, 2019-11-29T10:57:00.595Z, {func: [Function: func]}]

copyArray[2].getFullYear(); // => 2019
copyArray[3].func(); // => "foo"

lodash#cloneDeepdeep copy で、オブジェクトもコピーされる

まとめ

  1. JavaScriptの配列をコピーできる方法は、ほぼ shallow copy なので、入れ子になっているデータの取り扱いには注意 (危機管理) が必要
  2. 文字列化できるデータしか入ってないことが確実な場合は、JSON.parse(JSON.stringify(Array))deep copy することが可能
  3. function などが含まれる配列を deep copy したい場合は、再帰などを使った独自の関数を作る必要がある
  4. ライブラリを使えるなら deep copylodash#cloneDeep を使うのが簡単

JavaScript たのしい!


[参考]

Node.jsデザインパターン 第2版

Node.jsデザインパターン 第2版

まちカドまぞく 3巻 (まんがタイムKRコミックス)

まちカドまぞく 3巻 (まんがタイムKRコミックス)

まちカドまぞくの3巻まじ尊さの塊なのでシャミ子の二期お願いしますッ

Ruby メソッドの検索順

初めてのRubyやり終えたのでメモがてら

継承 (親クラス と 子孫クラス)

Ruby のクラスはは 1つだけ親クラスを持つ
多言語とかだと継承って呼ぶ事が多いけど、Rubyも継承って言って良いのかがチョットわからない

同じメソッドがある場合、子孫の持つメソッドが優先される

class Super
  def foo
    p 'Super foo'
  end

   def bar
    p 'Super bar'
  end
end

class Sub < Super
  def foo
    p 'Sub foo'
  end
end

sub = Sub.new
sub.foo # => "Sub foo"
# 子孫クラスで上書きされてないメソッドは親クラスのものが直接呼び出される
sub.bar # => "Super bar"

どのクラスが持っているメソッドか確認する

method(:メソッド名).owner でシンボル形式で渡したメソッドを持っているクラスを確認することができる

p sub.method(:bar).owner
# => Super
p sub.method(:foo).owner
# => Sub
p sub.method(:class).owner
# => Kernel

Module

Module インスタンス化できない機能の集まり (ModuleクラスはClassクラスの親)
Ruby ではクラスは1つしか継承できないが、Module はいくつでも継承?/読込みできる
Module をクラスに追加することを Mix-in と呼ぶらしい

include

include で追加した Module は自身と親の間に挿入されるイメージ
Module に定義されたメソッドは、親クラスのメソッドより優先される

class Super
  def foo
    p 'Super foo'
  end

   def bar
    p 'Super bar'
  end
end

module Mod
  def foo
    p 'foo-foo'
  end

  def bar
    p 'bar-bar'
  end
end

class Sub < Super
  # Module を Mix-in
  include Mod

  def foo
    p 'Sub foo'
  end
end

sub = Sub.new

# 自身の持つメソッドは includeした Module に上書きされない
sub.foo # => "Sub foo"
p sub.method(:foo).owner
# => Sub

# 親クラスの持つメソッドは Module が上書きする
sub.bar # => "bar-bar"
p sub.method(:bar).owner
# => Mod

prepend

Ruby 2.0 で追加された機能
prepend で追加した Module のメソッドは自身の持つメソッドより優先される

module Mod
  def foo
    p 'foo-foo'
  end
end

class Sub
  prepend Mod

  def foo
    p 'Sub foo'
  end
end

sub = Sub.new
sub.foo # => "foo-foo"
p sub.method(:foo).owner
# => Mod

あくまで Mix-in されたクラスのメソッドより優先されるだけなので、親クラスに Module が prepend されていても、子孫クラスのに同名のメソッドがあれば、自身(子孫クラス)のメソッドが呼び出される

module Mod
  def foo
    p 'foo-foo'
  end
  
  def bar
    p 'bar-bar'
  end
end

class Super
  prepend Mod
  
  def foo
    p 'Super foo'
  end
end

class Sub < Super
  def foo
    p 'Sub foo'
  end
end

sub = Sub.new
sub.foo # => "Sub foo"
# 親クラスに Mix-in されたメソッドも呼び出せる
sub.bar # => "bar-bar"

継承順の確認

クラス名.ancestors でModule, 親クラスの継承順を配列で確認することができる

module ModA
end

module ModB
end

module ModC
end

class Super
  include ModA
end

class Sub < Super
  include ModB
  prepend ModC
end

p Sub.ancestors
# => [ModC, Sub, ModB, Super, ModA, Object, Kernel, BasicObject]

ancestors で確認できる配列の先頭から順にメソッドが検索されていくイメージで良さそう。

Module が同じ名前のメソッドを持っている場合、後に追加されたものが優先される

include でも prepend でも同様に後に Mix-in された Module のメソッドが優先される

module ModA
  def hi
    p 'A'
  end
end

module ModB
  def hi
    p 'B'
  end
end

class X
  include ModA
  include ModB
end

X.new.hi # => "B"
p X.ancestors
# => [X, ModB, ModA, Object, Kernel, BasicObject]

class Y
  include ModB
  include ModA
end

Y.new.hi # => "A"
p Y.ancestors
# => [Y, ModA, ModB, Object, Kernel, BasicObject]

class A
  prepend ModA
  prepend ModB
end

A.new.hi # => "B"
p A.ancestors
# => [ModB, ModA, A, Object, Kernel, BasicObject]

class B
  prepend ModB
  prepend ModA
end

B.new.hi # => "A"
p B.ancestors
# => [ModA, ModB, B, Object, Kernel, BasicObject]

特異メソッド ( singleton method )

特異メソッドは、クラスではなくインスタンスに直接生えているメソッド
def オブジェクト.メソッド名 の形で定義する

特異メソッドはクラスが持つメソッドより優先される

module Mod
  def foo
    p 'Mod Foo'
  end
end

class Foo
  prepend Mod

  def foo
    p 'foo'
  end
end

foo = Foo.new

# 特異メソッドを定義
def foo.foo
  p 'FOO FOO'
end

foo.foo # => "FOO FOO"

# 特異メソッドはインスタンスに生えているので `クラス名.ancestors` では確認できない
p Foo.ancestors
# => [Mod, Foo, Object, Kernel, BasicObject]

# `インスタンス.singleton_class.ancestors` で自身のオブジェクトを含んだリストを見ることができる
p foo.singleton_class.ancestors
# => [#<Class:#<Foo:Object ID>>, Mod, Foo, Object, Kernel, BasicObject]

extend

extend は特異クラスに Mix-in をする
extend で追加した Module のメソッドはインスタンス自身の持っている特異メソッドは上書きしない
特異メソッド版の include

module ModA
  def foo; p 'Mod Foo' end

  def bar; p 'Mod Bar' end
end

module ModB
  def bar; p 'B' end
end

module ModC
  def bar; p 'C' end
end

class Foo
  prepend ModB
  include ModC

  def foo
    p 'foo'
  end
  
  def bar
    p 'bar'
  end
end

foo = Foo.new

# Mod を特異クラスに Mix-in
foo.extend Mod

# 特異メソッドを追加
def foo.foo
  p 'FOO FOO'
end

# 自身の特異メソッドは extend した Module より優先される
foo.foo # => "FOO FOO"
# extend した Module のメソッドはクラスの持つインスタンスメソッドより優先される
foo.bar # => "Mod Bar"

# extend で追加した Module は自身のインスタンの次に追加されている
p foo.singleton_class.ancestors
# => [#<Class:#<Foo:Object ID>>, ModA, ModB, Foo, ModC, Object, Kernel, BasicObject]

まとめ

Ruby のメソッドの検索は下記の順で遡って検索する

  1. 特異メソッド
  2. extendインスタンスに追加された Module のメソッド (特異メソッド)
  3. prepend で追加された Module のメソッド
  4. クラス自身の持つインスタンスメソッド
  5. include で追加された Module のメソッド
  6. クラス内にメソッドが見つからない場合は親クラスを探索
  7. クラスの親を遡って一番上のObject クラスまで探索してメソッドが見つからなければ BasicObject#method_missing が呼び出され NoMethodError が発生

メソッドの順番を確認したくなったら クラス名.ancestorsインスタンス.singleton_class.ancestors で継承順のリストを確認できる
どのクラスがメソッドを持っているか確認したい時は インスタンス.method(:メソッド名).owner で確認することができる

JavaScript の prototype 継承っぽくてすんなり理解できた!


[参考]

プログラミング言語 Ruby

プログラミング言語 Ruby

JavaScript 複数の要素をまとめて追加したい

例えばTODOリストのデータの初期化などループで要素を追加するような処理では、DOMへの要素の追加はレンダリングコストが高いので、なるべくまとめて行いたいです。

jQuery でイベントをもつ複数の要素を丸っとDOMに追加する

jQuery時代は文字列結合でひとまとめにしたものをDOMに追加して、イベントは document.on でイベントを発火させるターゲットを指定していれば、後から追加したテキストでもイベントを発火させることができました。

const data = [
  {text: 'TASK 1', status: 0},
  {text: 'TASK 2', status: 1},
  //...
];

const initTodoList = (data) => {
  const todoList = document.getElementById('#todoList');
  const listText = data.reduce((html, item) => {
    const className = item.status? 'complete' : 'incomplete';
    return html += `<li class="${className}">
      <label>${item.text}</label>
      <button class="doneBtn">DONE</button>
    </li>`;
  }, '');
  todoList.inerHTML = listText;
};

document.on('click.completeItem', '.doneBtn', onComplete);
initTodoList();

JavaScript (VanillaJS) でイベントをもつ複数の要素を丸っとDOMに追加する

jQueryを使わない場合は document.addEvetListener で取れるイベントから event.target を遡って該当するイベントを発火させる事もできますが、少し処理が複雑になってしまいます。
各要素に addEventListener でイベントを付けると、テキスト化してDOMに追加することができませんので、DOMElmentのまま追加するには appendChild() を使いますが、これはリストなど複数の要素を渡すことができません。( jQuery こういう時便利でしたね… )

アンチパターン

ループ内で都度 DOM に追加するとレンダリングコストが高くなる

const data = [...];

const createItem = ({text, status}) => {
  const item = document.createElement('li');
  const doneBtn = document.createElement('button');

  item.className = status? 'complete' : 'incomplete';
  item.innerHTML = `<label>${text}</label>`;

  doneBtn.className = 'doneBtn';
  doneBtn.textContent = 'DONE';
  doneBtn.addEventListener('click', onComplete);

  item.appendChild(doneBtn);
  return item;
};

const initTodoList = (data) => {
  const todoList = document.getElementById('#todoList');
  const listItems = data.map((item) => {
    const itemDOM = createItem(item);
    // 都度追加するのはレンダリングコストが高い
    todoList.appendChild(listItems);
    return itemDOM;
  });

  // これはエラーになるのでループ内で都度DOMに追加する必要がある
  // todoList.appendChild(listItems);
  // => TypeError: Failed to execute 'appendChild' on 'Node': parameter 1 is not of type 'Node'.
};
initTodoList();

document.createDocumentFragment を使う

Document.createDocumentFragment()
DocumentFragment は DOM ノードです。メインの DOM ツリーの一部にはなりません。通常の使い方は、文書フラグメントを生成し、その文書フラグメントに要素を追加して、その文書フラグメントを DOM ツリーへ追加します。 DOM ツリー内では、文書フラグメントはすべての子要素によって置き換えられます。

文書フラグメントはメモリ内にあり、メインの DOM ツリーの一部ではないため、文書フラグメントに子要素を追加してもページのリフロー (要素の位置と大きさを決定するための計算) が行われません。そのため文書フラグメントを利用することによって、パフォーマンスの改善が見込まれます。
cf. Document.createDocumentFragment() - Web API | MDN

DocumentFragment は後でまるっと追加したい要素を溜めておける透明な袋みたいなものっぽい。

const initTodoList = (data) => {
  const fragment = document.createDocumentFragment();
  data.map((item) => {
    const itemDOM = createItem(item);
    fragment.appendChild(itemDOM);
  });
  document.getElementById('#todoList')
    .appendChild(fragment);
};

DocumentFragment は React で言う所の <></> のようなものみたいで、描画されるDOMに追加しても、描画されず DocumentFragment 内に溜められていた要素が丸っと直接 appendChild するDOM直下に追加され描画されました!

所感

createDocumentFragment めっちゃ便利〜って調べたら結構昔からあった機能なのですね。 jQuery時代の後はReactとか触っててVanilla JSでゴリゴリやる機会も無かったので全く知らなかったです… むしろこれがあったからこそ React の <Fragment></Fragment>/<></> ができたって訳だったのか〜


[参考]

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

初めてのJavaScript 第3版 ―ES2015以降の最新ウェブ開発

FRAGMENT (Special Edition)

FRAGMENT (Special Edition)