NodeListを配列に変換するのにArray.applyを使うのはどうか
NodeList
やHTMLCollection
を配列(Array
)に変換する方法の一つに、Array.prototype.slice
を使う方法がある。
var array = Array.prototype.slice.call(document.getElementsByTagName('A'));
でも、最近の実装であればArray.apply
もその用途に使える。
var array = Array.apply(null, document.getElementsByTagName('A'));
ES3ではFunction.prototype.apply
の第二引数には配列(Array
)かarguments
しか取れなかったけど、ES5でその制限が緩和され、NodeList
などの array like オブジェクトも渡せるようになった。
なお、IE8以下ではどちらの方法でもエラーになるので注意(エラーメッセージは「JScript オブジェクトを指定してください」)。ループで処理するしかない。
var array = []; var nodes = document.getElementsByTagName('A'); for (var i = 0, l = nodes.length; i < l; ++i) { array[i] = nodes[i]; }
ちなみに、arguments
の配列化にArray.apply
は使えない。arguments
は要素が単一の数値だけになる可能性があり、その場合意図しない結果になる。
(function () { var args = Array.apply(null, arguments); // Array(5, 6, 7) つまり [5, 6, 7] console.log(args.shift()); // 5 })(5, 6, 7); (function () { var args = Array.apply(null, arguments); // Array(5) つまり length: 5 の配列 console.log(args.shift()); // undefined })(5);
lengthが1でないことを確かめてからなら使える(参考:slice vs [].apply · jsPerf)けど、Array.prototype.slice
を使う方がシンプル。
(function () { var args = arguments.length === 1 ? [arguments[0]] : Array.apply(null, arguments); console.log(args.shift()); // 5 })(5); (function () { var args = Array.prototype.slice.call(arguments); console.log(args.shift()); // 5 })(5);
おまけ:ES3実装およびIE8以下でFunction.prototype.apply
をES5仕様にするモンキーパッチ。
Array/Stringの汎用メソッドをこだわって定義する
FirefoxではArray.prototypeやString.prototypeにあるメソッドを簡単に転用できるように、コンストラクタ(Array, String)自身に汎用メソッド(generic method)が定義されています。
これにより、
Array.prototype.forEach.call(nodeList, body.appendChild, body)
はArray.forEach(nodeList, body.appendChild, body)
と書くことができるArray.prototype.slice.call(arguments, 1)
はArray.slice(arguments, 1)
と書くことができる
ところが、これはECMAScript標準ではないので、Firefox以外では定義されていません。Array | MDNに次のようなshimが用意されています。
/*globals define*/ // Assumes Array extras already present (one may use shims for these as well) (function () { 'use strict'; var i, // We could also build the array of methods with the following, but the // getOwnPropertyNames() method is non-shimable: // Object.getOwnPropertyNames(Array).filter(function (methodName) {return typeof Array[methodName] === 'function'}); methods = [ 'join', 'reverse', 'sort', 'push', 'pop', 'shift', 'unshift', 'splice', 'concat', 'slice', 'indexOf', 'lastIndexOf', 'forEach', 'map', 'reduce', 'reduceRight', 'filter', 'some', 'every', 'isArray' ], methodCount = methods.length, assignArrayGeneric = function (methodName) { var method = Array.prototype[methodName]; Array[methodName] = function (arg1) { return method.apply(arg1, Array.prototype.slice.call(arguments, 1)); }; }; for (i = 0; i < methodCount; i++) { assignArrayGeneric(methods[i]); } }());
isArrayは消さなければなりません。ミスだと思います。使う分にはこれで十分ですが、次の点が気になります。
- 定義される関数は無名関数(nameなし)なので、デバッグ時のコールスタックが嫌な感じになる
- 定義される関数のlengthプロパティが0なので、lengthプロパティを使う何か(例えばカリー化)に使えない
そこで、evalなりFunctionコンストラクタなりを使って、関数名と引数を合わせるように定義することを考えます。
var call = Function.prototype.call; function assignArrayGeneric(methodName) { var method = Array.prototype[methodName]; var args = generateArgsString(method.length + 1); Array[methodName] = new Function('call', 'method', 'return function ' + methodName + '(' + args + ') { return call.apply(method, arguments); };' )(call, method); } function generateArgsString(length) { var args = []; var i; for (i = 0; i < length; ++i) { args[i] = '$' + i; } return args.join(', '); }
引数の数は、this
相当の引数を受け取る分、+ 1
しています(Firefoxに合わせています)。これで、例えば Array.forEach
には function forEach($0, $1) { return call.apply(method, arguments); }
が定義されます(いわゆるクロージャにより、call
はFunction.prototype.call
、method
はArray.prototype.forEach
を指します)。関数名もlengthプロパティも、外部から使う分には十分になりました。
でも、欲が出てきて、今度は引数名が機械的な $0, $1
である点が気になりました。動的解析に対応したIDEで、$0, $1
よりは array, callbackfn
と表示された方が嬉しいです。
Functionコンストラクタを使うのもどうかと思うので、ES5の仕様書を見ながらそれぞれ書きました。
ご自由にお使いください。
breakできるforEach ―throwを使って―
思いつき。break文のように入れ子になった内側のループの中から外側のループも抜けられるように。
function Break(target) { this.target = target; } function forEach(array, callbackfn, thisArg) { try { Array.prototype.forEach.call(array, callbackfn, thisArg); } catch (e) { if (!(e instanceof Break) || (e.target && e.target !== callbackfn)) { throw e; } } }
使用例は以下の通り。
var array = [1, 2, 3, 4, 5, 6, 7, 8, 9]; forEach(array, function (n) { console.log(n); if (n === 5) { // 引数を指定しなければ直近のforEachが止まる throw new Break(); } }); forEach(array, function outer(i) { forEach(array, function inner(j) { console.log('%d * %d = %d', i, j, i * j); if (i === 5 && j === 5) { // 引数を指定するとその関数をコールバックとして渡したforEachが止まる throw new Break(outer); } }); });
戻り値 === false
で判断する方法は、reduceやfilterのような戻り値が意味を持つメソッドには使えない。一方、このthrow文の方法なら区別できる。でも、やっぱりここでthrow文は変な感じ。
someをforEach代わりに使うのも変な感じがする(と言いつつよくやるけど)。mapがあればforEachいらないよね、戻り値を無視すればいいだけなんだから、みたいな。
いっそのこと……
Array.prototype.forEach = Array.prototype.some;
Underscore.js、breaker変数をpublicにすればいいのに。
文字コードを使ってソート
Array.apply(null, Array(100)) .map(function () { return Math.random() * 1000 | 0; }) .map(String.fromCharCode) .sort() .map(function (c) { return c.charCodeAt(0); });
140文字に収まらなかった。
IEではフォーム内の要素にIDを付けてもグローバル変数として定義されない
- サンプルコード
<!DOCTYPE html> <title>TEST</title> <body> <input id="input1"> <form> <input id="input2"> </form> <pre><script> function w(text) { document.writeln(text); } try { w('input1: ' + input1); } catch (e) { w('input1 is undefined'); } try { w('input2: ' + input2); } catch (e) { w('input2 is undefined'); } </script></pre>
- 結果
-
input1 input2 IE 10 [object HTMLInputElement] undefined Firefox 18 [object HTMLInputElement] [object HTMLInputElement] Chrome 24 [object HTMLInputElement] [object HTMLInputElement] Opera 12.12 [object HTMLInputElement] undefined
Windows ストアアプリを作っているときにはまった。IntelliSenseで補完候補としてinput2が出てくるものの、実際に使うと Reference Error になる。
HTML Standard では 6.2.4 Named access on the Window object がこれに関しての説明だと思うんだけど、form内の要素については特に何も書いていないように見える。
msdnのどこかに書いてるのかな……。
slideshareのスライドをスクロールでめくるユーザースクリプト
version 1.2: speakerdeck, docs.google.comにも対応。
version 1.3: slideshareでめくり終えたときに表示されるスクリーンを考慮するように修正。
// ==UserScript== // @id slideshare_scroll // @name slideshare scroll // @version 1.3 // @namespace http://rikuba.com/ // @include http://slideshare.net/* // @include http://www.slideshare.net/* // @include http://speakerdeck.com/* // @include https://speakerdeck.com/* // @include https://docs.google.com/presentation/embed* // @run-at document-end // ==/UserScript== var siteinfos = [ { url: /^https?:\/\/speakerdeck\.com\//, container: '#presenter, #player', prev: 'a.prev', next: 'a.next' }, { url: /^http:\/\/(?:www\.)?slideshare\.net\/mobile\//, container: '.slideShow', prev: '#previous_slide', next: '#next_slide' }, { url: /^https?:\/\/(?:www\.)?slideshare\.net\//, container: '#svPlayerId, #player', prev: ['.lastScreenContainer .close-btn', '.btnPrevious'], next: '.btnNext' }, { url: /^https:\/\/docs\.google\.com\/presentation\/embed\?/, container: 'body', prev: 'a[role="button"][title="前へ"]', next: 'a[role="button"][title="次へ"]' } ]; function findMatchingSiteinfo(url) { for (var i = 0, l = siteinfos.length; i < l; ++i) { if (siteinfos[i].url.test(url)) { return siteinfos[i]; } } return null; } var MATCHES_SELECTOR = (function () { var elem = document.documentElement, i, name; for (i = 0; name = arguments[i++];) if (elem[name]) return name; })('matchesSelector', 'mozMatchesSelector', 'webkitMatchesSelector', 'oMatchesSelector'); function handleEvent(e) { var types = ['wheel', 'mousewheel']; e.currentTarget.removeEventListener(types[+!types.indexOf(e.type)], this); this.handleEvent = onwheel; this.handleEvent(e); } function onwheel(e) { if (e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) { return; } FIND_CONTAINER: { for (var elem = e.target; elem; elem = elem.parentNode) { // event target may be text node in Google Chrome if (elem.nodeType === 1 && elem[MATCHES_SELECTOR](this.siteinfo.container)) { break FIND_CONTAINER; } } return; } if (e.deltaY > 0 || e.wheelDelta < 0) { e.preventDefault(); clickElement(e.currentTarget, this.siteinfo.next); } else if (e.deltaY < 0 || e.wheelDelta > 0) { e.preventDefault(); clickElement(e.currentTarget, this.siteinfo.prev); } } function clickElement(element, selectors) { if (Array.isArray(selectors)) { return selectors.some(_clickElement, element); } return _clickElement.call(element, selectors); } function _clickElement(selector) { var target = this.querySelector(selector); if (target) { target.click(); return true; } return false; } (function main() { var siteinfo = findMatchingSiteinfo(location.href), listener; if (siteinfo) { listener = { siteinfo: siteinfo, handleEvent: handleEvent }; document.addEventListener('wheel', listener); document.addEventListener('mousewheel', listener); } }());
HTMLElement::click()、ユーザースクリプトを作るのに便利。
DOM Traversal の出番
({ observe: function (doc) { this.iterator = doc.createNodeIterator(doc, NodeFilter.SHOW_ELEMENT, this, true); doc.addEventListener('keydown', this, false); }, acceptNode: function (node) { if ('H1 H2 H3 H4 H5 H6'.indexOf(node.tagName) >= 0) { return NodeFilter.FILTER_ACCEPT; } return NodeFilter.FILTER_SKIP; }, handleEvent: function (evt) { var CTRL = evt.ctrlKey, SHIFT = evt.shiftKey, ALT = evt.altKey, META = evt.metaKey, code = evt.keyCode, node; if (code === 40 && ALT && !CTRL && !SHIFT && !META) { if ((node = this.iterator.nextNode())) { node.scrollIntoView(); evt.preventDefault(); } } else if (code === 38 && ALT && !CTRL && !SHIFT && !META) { if ((node = this.iterator.previousNode())) { node.scrollIntoView(); evt.preventDefault(); } } } }).observe(document);
acceptNode は CSS セレクタから作れるようにして、イベントとアクションの対応を関数内に書かないようにしよう(イベントセレクタ? イベントフィルタ? のようなものを作る)。
メモ:
- Opera 12.02 では「Alt + 修飾キー以外のキー」という組み合わせでは keydown イベントが発生しない。
- Chrome 21 では NodeFilter::acceptNode の this 値が window になっている(今回のコードでは問題にならない)。
// そういえばJavaScriptリファレンス 第6版の索引だけ立ち読みしたら、TreeWalker や NodeIterator が載っていないようだった。IE もバージョン 9 でようやく使えるようになったのに。