nullやundefinedがきてもメソッドチェーン
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 2
◆ できるようなのを作りました
◆ 使い方にクセがあります
◆ 使い方にクセがあります
メソッドチェーンしたい 再び
メソッドチェーンで書いているとき (そうでないときでも)null や undefined が来た場合の分岐ってめんどうですよねnull や undefined はプロパティアクセスできないので メソッドを呼び出した(プロパティアクセスした)瞬間エラーが発生がおきてしまいます
x.pop().split(",")[1].indexOf("a")
y.match(/a([0-9]+)/)[1]
というコードだとy.match(/a([0-9]+)/)[1]
.pop() したときに x が空の配列だと undefined ですし .split(",") で 「,」が1つもない文字列だと [1] が undefined なので 次のメソッド呼び出しがエラーです
.match() も見つからなければ null が返ってくるので [1] がエラーになります
ですがいちいち確認して分岐って書くのがめんどうなだけでなく コードも綺麗じゃなくなるので嫌です
楽~に書く方法が考えてみると
- 返り値が null や undefined になるなら特殊なundefinedオブジェクトを返す
- undefinedオブジェクトのすべてのメソッドの返り値は undefinedオブジェクト
とすれば行けるんでは?
と思いましたが JavaScriptでは PHPの __get や __call みたいに存在しないプロパティにアクセスされたときに好きな値を返す方法が無いです
ないですよね。
なので苦労してたのですが この記事の callBy みたいに何かを通してメソッドを実行することでなんとかできる ということで callBy に似た使い方で使えるものができました
メソッドチェーンする
mcall
!function(){
Object.prototype.mcall = function(method, bind_this){
var that = this
var this_to_arg = true
var fn = method
if(typeof fn === "string" && fn.charAt(0) === ":"){
fn = fn.substr(1)
bind_this = window
}
else if(arguments.length < 2){
bind_this = that
this_to_arg = false
}
if(typeof method !== "function"){
fn = (bind_this || window)[method]
}
return typeof fn === "function" ? function(){
var args = [].slice.call(arguments)
this_to_arg && args.unshift(that)
var res = fn.apply(bind_this, args)
return res == undefined ? new Undef : res
} : Undef.maker
}
Object.prototype._ = Object.prototype.mcall
Object.prototype.get = function(key){
return this[key]
}
Object.prototype.__ = function(key){
return this[key] == undefined ? new Undef : this[key]
}
function Undef(){
}
Undef.prototype.mcall = function(){
return Undef.maker
}
Undef.prototype.isUndef = true
Undef.maker = function(){
return new Undef
}
}()
Object.prototype.mcall = function(method, bind_this){
var that = this
var this_to_arg = true
var fn = method
if(typeof fn === "string" && fn.charAt(0) === ":"){
fn = fn.substr(1)
bind_this = window
}
else if(arguments.length < 2){
bind_this = that
this_to_arg = false
}
if(typeof method !== "function"){
fn = (bind_this || window)[method]
}
return typeof fn === "function" ? function(){
var args = [].slice.call(arguments)
this_to_arg && args.unshift(that)
var res = fn.apply(bind_this, args)
return res == undefined ? new Undef : res
} : Undef.maker
}
Object.prototype._ = Object.prototype.mcall
Object.prototype.get = function(key){
return this[key]
}
Object.prototype.__ = function(key){
return this[key] == undefined ? new Undef : this[key]
}
function Undef(){
}
Undef.prototype.mcall = function(){
return Undef.maker
}
Undef.prototype.isUndef = true
Undef.maker = function(){
return new Undef
}
}()
ちょっと長くなってます
Object に mcall というメソッドを作っています
arr
.mcall("pop")()
.mcall("substr")(1)
.mcall("pop")()
.mcall("substr")(1)
という使い方です
mcall にメソッド名を渡すと 実行用の関数が返ってくるので引数を入れて実行します
arr が ["a","bcd"] だと返り値は "cd" です
コレは普通です
arr が [] だと返り値は Undef というundefinedまたはnullを表すオブジェクトです
substr の実行段階ではエラーが出ていません
返り値が Undef にどこかでなるとそれ以降で指定されたメソッドは実行しない作りです
_
mcall って毎回書くのはめんどうですよねなので _ でも使えるようにしています
["ab", "c;de"]
._("pop")() // "c;de"
._("split")(";") // ["c", "de"]
._("map")(function(e){return e+"X"}) // ["cX", "deX"]
._("join")("_") // "cX_deX"
._("getElementById", document)() // elem?
._("getChildIds", null)() // ["id01", "id02"]?
._("shift")() // "id01"?
._(parseInt, window)(16) // NaN?
._(":String")() // "NaN"?
function getChildIds(elem){
return [].map.call(elem.children, function(e){return e.id})
}
._("pop")() // "c;de"
._("split")(";") // ["c", "de"]
._("map")(function(e){return e+"X"}) // ["cX", "deX"]
._("join")("_") // "cX_deX"
._("getElementById", document)() // elem?
._("getChildIds", null)() // ["id01", "id02"]?
._("shift")() // "id01"?
._(parseInt, window)(16) // NaN?
._(":String")() // "NaN"?
function getChildIds(elem){
return [].map.call(elem.children, function(e){return e.id})
}
ちょっと長めの例を書いてみました
getElementById で 要素がないかもしれないです
getChildIds が宣言されてないかもしれません
getChildIds で空配列が返って来て shift で undefined になるかもしれません
そんなのに対応できてます
他機能の紹介です
_ の第二引数
指定することで メソッドを実行する this を指定できます
省略すると元々の this です
なので 配列に対しての pop や map では指定しなくていいです
指定した場合は今の this が第一引数に入ります
._("getElementById", document)()
では getElementById を実行する this が document になりますそうすると今までの this が消えてしまうので 第一引数に入れて実行します
document.getElementById(this)
となります引数を渡した場合は2つめ以降に入ります
call や apply と同じで null の指定は window になります
._("getChildIds", null)()
では グローバルで定義された関数 getChildIds を使うので 第二引数に null を指定します文字列でメソッド名を渡した場合はメソッドを実行する this の中から探されます
「:」から始まるメソッド名
コロンから始まるメソッド名を書くと 第二引数を書かなくても window を指定したことになります
._(":String")()
は._("String", window)()
と同じことですメソッド名じゃなくて関数を第一引数に渡すと
メソッド名を渡すと 実行する this からメソッドを探しましたが その処理をしなくなります
Array.prototype.slice.call(
むりやり配列として メソッドを呼ぶ方法がありますArray.prototype.slice.call(obj) は配列変換によく使われます
これをやることもできます
var x = {0:10,1:11,2:12,length:3}
Array.prototype.slice.call(x, 2) // [12]
x.mcall(Array.prototype.slice)(2) // [12]
x._(Array.prototype.slice)(2) // [12]
Array.prototype.slice.call(x, 2) // [12]
x.mcall(Array.prototype.slice)(2) // [12]
x._(Array.prototype.slice)(2) // [12]
3つとも同じになります
mcall だと普通に書くより文字数ちょっと多くなりますけど _ だとちょっと少なくなります
get
メソッドチェーンするなら 配列やオブジェクトの値を取り出す時にvar a = [1,2,3]
a[1] // 2
var o = {
a: 1,
b: 2
}
x["b"] // 2
じゃなくてa[1] // 2
var o = {
a: 1,
b: 2
}
x["b"] // 2
var a = [1,2,3]
a.get(1) // 2
var o = {
a: 1,
b: 2
}
o.get("b") // 2
としたいところですa.get(1) // 2
var o = {
a: 1,
b: 2
}
o.get("b") // 2
ということで Object.prototype.get が用意されてます
このメソッドは undefined や null はそのまま返します
なので
x._("get")(1)._("get")("a")._("get")(0)
とかくことになります
ちょっと長すぎ。。。
なので Object.prototype.__ も用意してます
これは get の undefined や null を Undefオブジェクトに 置き換えて返すものです
ところで
いきなりですが 存在するかわからないa.b.c.d
にアクセスするとき あなたならどうやりますか?スタンダードな方法 &&
a && a.b && a.b.c && a.b.c.d
プロパティが存在すれば さらに奥を見ていく ということをちょっと楽してやっています
チェーンが長くなるとどんどん長くなってきて 後半は嫌になってきます
|| にしてみる
(((a || {}).b || {}).c || {}).d
false になれば 空オブジェクトに置き換えてどんどん奥を見ていきます
オブジェクトなら絶対trueとみなされますし 非オブジェクトが途中に来てもプロパティアクセスで undefined になるので次が false で空オブジェクトになって という感じで続きます
カッコが多いし書いていて分かりづらいのであまりオススメじゃないです
裏ワザ? Object
Object(Object(Object(a).b).c).d
|| に近い方法で Object 関数を通します
Object関数は Objectが引数ならそのまま返して 非オブジェクトなら オブジェクト化して返します
null や undefined がオブジェクトになるのでチェーンを続けることが出来ます
こっちもカッコが見づらい難点があります
__
さて 話を戻すとします__を使うと
a.__("b").__("c").__("d")
と書けますさっきまでのと比較してみると
a._("get","b")._("get","c")._("get","d")
Object(Object(Object(a).b).c).d
(((a || {}).b || {}).c || {}).d
a && a.b && a.b.c && a.b.c.d
a.__("b").__("c").__("d")
Object(Object(Object(a).b).c).d
(((a || {}).b || {}).c || {}).d
a && a.b && a.b.c && a.b.c.d
a.__("b").__("c").__("d")
__ が一番綺麗です!
もうちょっとサンプル
これも特に意味のある例じゃないですrandom_word()
._("match")(/sample-([a-f][0-9]+)/)
._("get")(1)
._("getElementsByClassName", document)()
._(Array.prototype.slice)()
._("sort")(function(a,b){return a.dataset.order - b.dataset.order}))
.__(4)
._("getAttribute")("id")
function random_word(){
var a = trueRate(50) ? "sample-" : strgen(6)
var b = trueRate(80) ? "c63" : strgen(3)
return a + b
function strgen(n){
return btoa(Math.random()).substr(0,n)
}
function trueRate(percent){
return Math.random()*100 < percent
}
}
._("match")(/sample-([a-f][0-9]+)/)
._("get")(1)
._("getElementsByClassName", document)()
._(Array.prototype.slice)()
._("sort")(function(a,b){return a.dataset.order - b.dataset.order}))
.__(4)
._("getAttribute")("id")
function random_word(){
var a = trueRate(50) ? "sample-" : strgen(6)
var b = trueRate(80) ? "c63" : strgen(3)
return a + b
function strgen(n){
return btoa(Math.random()).substr(0,n)
}
function trueRate(percent){
return Math.random()*100 < percent
}
}
たまに マッチする結果が random_word から返ってくるので そのときそれに応じた class の要素とって~
ってまぁ見ればわかるので略します
こんな感じになるっていう見た目のイメージです
最後の値を使いたい時
チェーンの最後でDOM更新するなど副作用を起こして それだけで完了してしまえばいいですが 返り値を受け取って分岐したり 変数に保存しておくということもあると思います最終的に返って来たのが Undef オブジェクトなのかを簡単に把握するために Undef オブジェクトには isUndef プロパティが true で用意されています
var v = x._("match")(/[0-9]+/)__(1)
if(!v.isUndef){
y(v)
}
var val = v.isUndef ? undfined : v
こんな感じに使ってくださいif(!v.isUndef){
y(v)
}
var val = v.isUndef ? undfined : v
たいてい undefined だと何もしなくて ちゃんと値があるときだけ続きをすると思うので 上側のような使い方が多いと思います
jQueryぽい?
どことなくjQueryっぽい気がしますが jQueryを嫌う理由の- 内部でよくわからないことしてあちこちDOM書き換える
- 全部jQueryでラップされてる
というのがないのでべつにいいかな
null, undefined が来た時以外は普通にメソッドをそのまま使ってるだけですし 途中の返り値も普通のJavaScriptの値ですし 知らない間に変な所書き換えられることもないです