htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

JScript(not JavaScript)のArrayに対するメソッド群を追加する超単純なポリフィル

今さらこんなコードを書くこと自体恥ずべきことだと思うのだけど、勤務先の業務システムがJSPで実装されており、クライアントサイドはレガシーなIEが想定されています(嘆かわしいですが、よくある光景ですね…)

そしてこれまたよくある光景なのですが、秘伝のソースであるため、

<%-- yyyy/mm/dd add someone IE11対応 --%>
<META HTTP-EQUIV=X-UA-Compatible CONTENT=IE=5>
<%-- /IE11対応 --%>

みたいなのが書いてあるわけです。
ダブルクォートが省略されているとか、タグが大文字記述であるとか、昭和の臭いがすごいです。
お前それどこがIE11対応やねん、といったツッコミはさておき。

そんな環境ですと、おもむろにF12を押してデバッグをしようにも配列やArrayLikeを扱う上で非常につらみを感じるわけです。
「じゃあlodash使えばいいじゃん!」と

(function(s){
  s.src="https://cdn.jsdelivr.net/lodash/4.17.4/lodash.min.js";
  document.body.appendChild(s);
})(document.createElement("script"))

なんてコードを実行してみても、lodashの実行中にコケたりするんです。
そのときに知ったんですが、lodashってIE9+なんですね…。

便利なライブラリが使えないとなると、ある程度自分で書かざるをえないのでブックマークレットを作成して活用しています。

というのが前置き。
当たり前ですがコードの持ち出しは禁止なので、実際のコードとはちょっと違うと思います。

javascript:(function(d,i,l,x,a){d("indexOf",function(v){for(i=0,l=this.length;i<l;++i)if(this[i]===v)return i;return -1});d("includes",function(v){return!!~this.indexOf(v)});d("map",function(f,t){x=this,a=[];for(i=0,l=x.length;i<l;++i)a.push(f.call(t,x[i],i,x));return a});d("filter",function(f,t){x=this,a=[];for(i=0,l=x.length;i<l;++i)f.call(t,x[i],i,x)&&a.push(x[i]);return a});d("forEach",function(f,t){x=this;for(i=0,l=x.length;i<l;++i)f.call(t,x[i],i,x)});d("reduce",function(f,v){x=this,v=v!==void 0?v:x.shift();for(i=0,l=x.length;i<l;++i)v=f(v,x[i],i,x);return v});d("every",function(f,t){return!!this.reduce(function(p,c){return p&&f.call(t,c)},1)});d("some",function(f,t){return!!this.reduce(function(p,c){return p||f.call(t,c)},0)})})(function(n,f){if(![][n])Array.prototype[n]=f})

これに改行やインデントを付けるとこんな感じになり、

(function(d, i, l, x, a) {
  d("indexOf", function(v) {
    for (i = 0, l = this.length; i < l; ++i)
      if (this[i] === v) return i;
    return -1
  });
  d("includes", function(v) {
    return !!~this.indexOf(v)
  });
  d("map", function(f, t) {
    x = this, a = [];
    for (i = 0, l = x.length; i < l; ++i)
      a.push(f.call(t, x[i], i, x));
    return a
  });
  d("filter", function(f, t) {
    x = this, a = [];
    for (i = 0, l = x.length; i < l; ++i)
      f.call(t, x[i], i, x) && a.push(x[i]);
    return a
  });
  d("forEach", function(f, t) {
    x = this;
    for (i = 0, l = x.length; i < l; ++i)
      f.call(t, x[i], i, x)
  });
  d("reduce", function(f, v) {
    x = this, v = v !== void 0 ? v : x.shift();
    for (i = 0, l = x.length; i < l; ++i)
      v = f(v, x[i], i, x);
    return v
  });
  d("every", function(f, t) {
    return !!this.reduce(function(p, c) { return p && f.call(t, c) }, 1)
  });
  d("some", function(f, t) {
    return !!this.reduce(function(p, c) { return p || f.call(t, c) }, 0)
  })
})(function(n, f) {
  if (![][n]) Array.prototype[n] = f
})

変数名や構文に修正を加えるとこうなります。

(function(define) {
  var i, len, arr;
  
  define("indexOf", function(value) {
    for (i = 0, len = this.length; i < len; ++i) {
      if (this[i] === value) return i;
    }
    return -1;
  });
  
  define("includes", function(value) {
    return this.indexOf(v) !== -1;
  });
  
  define("map", function(func, thisArg) {
    arr = [];
    for (i = 0, len = this.length; i < len; ++i) {
      arr.push(func.call(thisArg, this[i], i, this));
    }
    return arr;
  });
  
  define("filter", function(func, thisArg) {
    arr = [];
    for (i = 0, len = this.length; i < len; ++i) {
      if (func.call(thisArg, this[i], i, this)) a.push(this[i]);
    }
    return arr;
  });
  
  define("forEach", function(func, thisArg) {
    for (i = 0, len = this.length; i < len; ++i) {
      func.call(thisArg, this[i], i, this);
    }
  });
  
  define("reduce", function(func, value) {
    if (value === void 0) {
      value = this.shift();
    }
    for (i = 0, len = this.length; i < len; ++i) {
      value = func(value, this[i], i, this);
    }
    return value;
  });
  
  define("every", function(func, thisArg) {
    return !!this.reduce(function(prev, curr) {
      return prev && func.call(thisArg, curr);
    }, true);
  });
  define("some", function(func, thisArg) {
    return !!this.reduce(function(prev, curr) {
      return prev || func.call(thisArg, curr);
    }, false);
  });
})(function(name, func) {
  if (Array.prototype[name] === void 0) Array.prototype[name] = func;
});

indexOfincludesに第二引数(探査を開始するインデックス)を定義していないのは必要ないからです。
アレ活用している人いるんですかね?

正直map filter forEachthisArgも使わないと言えば使わないんですが、なんとなく付けました。

lenという変数を定義してまでいちいちthis.lengthを代入している理由は、賢明な諸兄はもちろんご存知かと思いますが、forループで毎回Array#lengthにアクセスしてパフォーマンスが落ちるからです。
なので最初に長さを持ってしまいます。
その結果、高階関数に渡す関数がオブジェクト自身の要素数に副作用を起こすものである場合、意図しない動作になります。

まぁ自分しか使わないのでどうでもいいんですが。

 

 

話戻りますが、前述のJSPのコード、それはそれは本当に酷いです。

var url;

if (document.forms[0].document.forms[0].foo.value == "1") {
  document.forms[1].elem1.disabled = true;
  document.forms[1].elem2.disabled = true;
  document.forms[1].elem3.disabled = true;
  
  url = "/foo.html";
  document.forms[2].location.href = url;
  return;
}
else if (document.forms[0].document.forms[0].foo.value == "2") {
  document.forms[1].elem1.disabled = true;
  document.forms[1].elem2.disabled = true;
  document.forms[1].elem3.disabled = true;
  
  url = "/bar.html";
  document.forms[2].location.href = url;
  return;
}
else if (document.forms[0].document.forms[0].foo.value == "3") {
  document.forms[1].elem1.disabled = true;
  document.forms[1].elem2.disabled = true;
  document.forms[1].elem3.disabled = true;
  
  url = "/baz.html";
  document.forms[2].location.href = url;
  return;
}
else {
  document.forms[1].elem1.disabled = true;
  document.forms[1].elem2.disabled = true;
  document.forms[1].elem3.disabled = true;
  
  url = "/qux.html";
  document.forms[2].location.href = url;
  return;
}

みたいなJSが平気で埋め込まれていたりして、「DRYとはいったい…ウゴゴゴゴ」という気持ちでいっぱいになります。
クライアントサイドを担当した人すごい。

商業システムってこんなんばっか。
これで目立ったバグがないならまだいいんだけど、たまに動かなくなるからな…。

【参考】

unkode-mania.net

はてブのコメントにつけたスターを見易くするグリモンスクリプトがはてブリニューアルにつき動かなくなったので修正しました。

表題の通り。

gist.github.com

はてブ見ててどのコメントにスター付けたかよく忘れるので

GreaseMonkeyスクリプトを書きました。

gist.github.com

使うと自分が付けた星は点滅するようになります。
以上。

全てのフレームを対象にするには

某大手工業系企業の新規開発プロジェクトに参画しているのですが、非常に悲しいことに社内ポータルが未だにフレームを使用したサイトなのです。
フレームを使うと容易にペインで分けたサイトを構築できるため、実用重視のエンタープライズ界隈で重宝されるのはよく分かります。

ただ、フレーム*1を使うと、ブックマークレットを作ろうと思ったときにそのままではトップレベルのウィンドウにしか効力を発揮しません。
埋め込まれているフレームのソースURLがトップレベルのウィンドウと同じドメインに属しているのであれば*2、以下のようなコードを使うことで全フレームに対して操作することができます。

// for IE9+
(function loop(win) {
  return Array.prototype.reduce.call(win, function(p, c) {
    return p.concat(loop(c.window));
  }, [win]);
})(window)
.forEach(function(window) {
  var document;
  try { document = window.document; } catch(e) { return; }
  /* code here */
});
// compressed
(function l(w){return [].reduce.call(w,function(p,c){return p.concat(l(c.window))},[w])})(window).forEach(function(w){try{var d=w.document}catch(e){return}/* code here */})

*1:<iframe>も同様です。

*2:Same-Origin PolicyはWebにおける重要な概念です。

リンク化スクリプト

かねてからHatena::Let で公開 *1 していて、個人的にもかなり使っているスクリプトGreaseMonkey用に書き直したのでそれも公開してみます。
https://gist.github.com/htsign/5eed5473a9e75c7c45f3a5571d7d0803/raw/convertTextToLink.user.js

GreaseMonkey用っていうか単にES2015で書き直しただけで、特別GreaseMonkeyには依存していませんが。

せっかくなのでGreaseMonkey版はMutationObserverによる追加ノードの監視機能も付けました。
Hatena::Letの方(以下、ブックマークレット版)にはこの機能は持たせていません。
対象となるブラウザの範囲にIEも入るからです。
新しいノードに適用する為には都度実行してやれば済む話で、別にそんなに手間なわけでもありませんし。

一応Mutation Eventsdocument.addEventListener("DOMNodeInserted", ...みたいなやつ)はIE9から対応していますが、これは既に非推奨となった技術なので積極的に採用するつもりはないです。
場合分けめんどいですしね(本音)

NodeIteratorで探索しているのでそれなりに速いと思いますが、そもそもの計算量の時点でたかが知れてるのでアレです。

注意点として、テキストに対して要素を追加する(DOMに破壊的変更を加える)為、DOMの構造に依存した外部コードがある場合には不具合の原因となる可能性があります。

*1:ブックマークレット版もES2015版での大幅な編集に合わせてだいぶ構造変わってます

以後は特に指定のない限りJSはES6記法で書いていこうと思います。

Intlオブジェクトというものを知りました。
国際化表記における各種変換を取りまとめるそこそこ大規模なオブジェクトらしい。

Collator DateTimeFormat NumberFormatの3つのサブオブジェクトを持つ模様。
それぞれインスタンス化して使用するっぽい。

(() => {
  let localeOption = { style: "currency", currency: "jpy" };
  let locale = new Intl.NumberFormat("ja-JP", localeOption);
  console.log(locale.format(1234500)); // => ¥1,234,500
})();

便利。

第二引数のoptionにcurrencyDisplayを指定すると表記方法が変わるっぽい。
currencyDisplay: "symbol"がデフォルトで、他にcodenameがあるらしい。
symbolの場合の記号が半角とか全角とか、その辺は実装依存っぽい。

とか思ってたら

Number#toLocaleStringとか、他の代表的なオブジェクトにメソッドとして同等機能が提供されていた。
newはオーバーヘッドも大きいし*1、特別な理由がない限りは基本こっちですね。

*1:実測はしていませんが

Microsoft Edge (Windows 10 Mobile) のUser-Agent

なかなかすごいことになっています。
どこかの記事(確かスラド辺り)で読んだことありますが、WebKitと同じ挙動を示すようにしたとかなんとか。

Lumia 950で確認しました。

Mozilla/5.0 (Windows Phone 10.0; Android 4.2.1; Microsoft; Lumia 950) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2486.0 Mobile Safari/537.36 Edge/13.10586

例えば以下のような条件判定に一致するようになります。

var ua = navigator.userAgent.toLowerCase(), cond;

cond = /android/.test(ua) && /mobile/.test(ua);
console.log("Android : " + cond); // "Android : true"

cond = /safari/.test(ua);
console.log("Safari : " + cond);  // "Safari : true"

Linux; U;が入っていないのが救いですね。
参照: 滅び行くAndroid 標準ブラウザをサポート外にして悩みの種をなくす話 - Qiita


User-Agentでブラウザの判断をせざるを得ない現状も個人的にはどうかと思います。
本当であれば機能面で分けるべきなんですよね。

console.log("Legacy IE : " + (/*@cc_on ! @*/false));

var webkitKeys = Object.keys(window).filter(function(e){
  return window.hasOwnProperty(e) && e.indexOf("webkit") !== -1;
});
console.log("WebKit : " + (webkitKeys.length > 0));

まぁこれはこれで穴がありますが…。