◆ ライブラリの中ではプロトタイプ拡張したいけど それを使う側には影響与えないようにしたい
◆ module 機能でも Array などはグローバルにあって共通なので拡張すると影響してしまう
◆ iframe を使えば別ページなので Array などが別のものになるので iframe 内だけに収められる

安全にプロトタイプ拡張したい

前記事でもやりたかったことです
プロトタイプチェーンの拡張を気軽にしたいのですが 外部に影響させたくないです
ライブラリなら そのライブラリ内で自分が使うだけでライブラリユーザは特に気にしなくていいようにしたいです

これまでだと lodash などのように値を関数に通してオブジェクトにラップすることでメソッドを使えるようにしたり 共通のプロパティを経由することでチェインしてるような書き方でオブジェクトにラップしてメソッドを使えるようにしていました

でもどちらもプロトタイプを直接拡張してるのとは異なるのでちょっと書きづらい書き方になります

lib("foo-bar").after("-")

"foo-bar".lib.after("-")

"foo-bar".after("-")

普通にプロトタイプ拡張する書き方が一番書きやすく見やすいです

iframe

最近は module も使えますが Array や String などはグローバルのオブジェクトなのでいくら module 内であっても変更すれば全てに影響します
サンドボックス的な完全にそこだけで完結した空間を作って 引数と返り値だけを受け渡しできないかなぁ と考えていたらひらめきました

iframe です!

iframe では完全に別ページを表示します
別ページなので window は異なります
もちろん Array なども別物です
about:blank にしてしまえば DOM にアタッチすると同期的に初期化されて親 frame から操作できます

function evalSandbox(code){
const iframe = document.createElement("iframe")
iframe.hidden = true
document.body.append(iframe)
const result = iframe.contentWindow.eval(code)
iframe.remove()
return result
}

const v1 = evalSandbox(String.raw `
String.prototype.lines = function(){return this.split(/\r?\n|\r/)}
"a\nb\nc".lines()
`)
console.log(v1)
// ["a", "b", "c"]
console.log("".lines)
// undefined

メインのページでは文字列で JavaScript を書いて evalSandbox 関数に文字列を渡します
evalSandbox 関数では iframe を作ってページ内の eval 関数で実行します

今回は String 型に行単位で配列に分割する lines メソッドを作ってみました
結果を見るとちゃんと \n で分割されているので String.prototype.lines は作られて実行できています

その後メインのページで "".lines を表示すると undefined ですのでメインのページには影響していません


JavaScript を文字列で書いていますが構文エラーが実行前に確認されなかったり 「`」 を内側で使えないなど不便な点が多いです
内側が JavaScript なのでこういうこともできます

function x(){
Array.prototype.foo = 100
return [].foo
}
evalSandbox(x.toString() + ";x()")
// 100
console.log([].foo)
// undefined

toString がムダに思うかもしれませんが そのままでは使えません

function x(){
Array.prototype.foo = 100
return [].foo
}
evalSandbox(`
window.x = parent.x
x()
`)
// 100
console.log([].foo)
// 100

このように関数を移動しても定義箇所でのスコープから外側の変数を参照するのでメインページ側の Array が拡張されています

ライブラリにする

毎回 iframe を作って実行だと効率が悪いのでライブラリにしてしまいます

[lib.js]
!function (){
function main(){
Array.prototype.last = function(){return this[this.length - 1]}
function last(a){return a[a.length - 1]}
function method(a){return a.last()}
function func(a){return last(a)}
return {method, func}
}
window.lib = evalSandbox(`(${main})()`)
}()

evalSandbox は同じもので ライブラリにこんなファイルを作りました
main 関数に iframe 内の実行内容を書いて実行結果をグローバルの lib に代入します
main では関数を定義したりプロトタイプ拡張を行って 最終的にエクスポートするデータを返します

実行すると
lib.func([1,2])
// 2
lib.func([1,2,3])
// 3
lib.method([1,2])
// a.last is not a function

あれ?
last メソッドを使う方ではエラーになりました

変換必要

原因見つけるのに変に苦労したのですが わかれば単純でした
「[1, 2]」という配列をライブラリに渡していますが この 「[1, 2]」 はメインページで作られたものなのでメインページの Array のインスタンスです
iframe のページで同じ値にアクセスしてもプロトタイプはメインページのものです
プロトタイプ拡張は iframe ページの Array に対して行っているのでプロトタイプのページの Array に変換が必要です

!function (){
function main(){
Array.prototype.last = function(){return this[this.length - 1]}
function method(arr){
return Array.from(arr).last()
}
return {method}
}
window.lib = evalSandbox(`(${main})()`)
}()

lib.method([1, 2])
// 2

返す値も変換しないと iframe 内のプロトタイプにチェーンしているので拡張されてメソッドが使える状態になっています
メインページと同じ状態にする必要がある場合は最終的な値も変換必要です

これは配列などオブジェクトの場合のみで 文字列などプリミティブ型では自動で変換されてるみたいです

!function (){
function main(){
return {
bool: true,
num: 1,
str: "",
arr: [],
obj: {},
}
}
window.values = evalSandbox(`(${main})()`)
}()

console.log(values.bool.constructor === Boolean)
// true

console.log(values.num.constructor === Number)
// true

console.log(values.str.constructor === String)
// true

console.log(values.arr.constructor === Array)
// false

console.log(values.obj.constructor === Object)
// false

console.log

すごく便利なのですが裏技っぽい方法なのでうまく動かなくなるところもあります
console.log を使うと問題がでます

!function (){
function main(){
return function(){
console.log(1)
console.log(Array)
}
}
window.lib = evalSandbox(`(${main})()`)
}()

lib()
// no output

console.log が実行されているのに何も表示してくれません
原因は iframe を remove しているからでした

実行されるのは evalSandbox 関数を終えたあとで lib 関数を呼び出したときです
そのときにはすでに iframe は remove されてなくなっています
DOM にアタッチされている状態じゃないと console.log で出力されないようです

エラーは出ないので 表示する console.log 自体は実行されているのに console につながっていないから表示できていないということかもしれません
不便なので デバッグ中は evalSandbox の remove は外しておいたほうが良さそうです

この挙動は chrome と firefox 両方共通でした