回答編集履歴

1

ビルトインオブジェクト、ネイティブオブジェクト、Function\.prototype\.apply, getElementById\.\.\.etc の説明を追加

2016/11/06 03:00

投稿

think49
think49

スコア18166

test CHANGED
@@ -1,3 +1,7 @@
1
+ ### Function.prototype.apply.call
2
+
3
+
4
+
1
5
  関数が `Function.prototype` を継承していなくとも `TypeError` を発生させません。
2
6
 
3
7
 
@@ -22,4 +26,414 @@
22
26
 
23
27
 
24
28
 
29
+ ### Function.prototype.apply の用途
30
+
31
+
32
+
33
+ 用途によって呼び出し方も変わるものです。
34
+
35
+
36
+
37
+ ```JavaScript
38
+
39
+ function fn () {
40
+
41
+ console.log(arguments);
42
+
43
+ }
44
+
45
+
46
+
47
+ fn.apply(null, [1, 2, 3]); // [1, 2, 3]
48
+
49
+ ```
50
+
51
+
52
+
53
+ ユーザ定義関数をそのまま呼び出す場合は `Function.prototype` を継承している事がコード制作者にとって自明なので問題ありません。
54
+
55
+ しかし、その関数の所在が明らかでない場合、例えば引数で受け取った関数の場合はどうでしょう。
56
+
57
+
58
+
59
+ ```JavaScript
60
+
61
+ function hoge (fn) {
62
+
63
+ fn.apply(null, [1, 2, 3]);
64
+
65
+ }
66
+
67
+ ```
68
+
69
+
70
+
71
+ この場合、`hoge()` が呼び出されるまで `fn.apply(null, [1, 2, 3]);` が実行可能か判断することが出来ません。
72
+
73
+ それが**どんな関数であっても呼び出せることを保証するなら** `Function.prototype.apply.call` で呼び出すのが確実です。
74
+
75
+
76
+
77
+ ```JavaScript
78
+
79
+ function hoge (fn) {
80
+
81
+ Function.prototype.apply.call(fn, null, [1, 2, 3]);
82
+
83
+ }
84
+
85
+ ```
86
+
87
+
88
+
89
+ ### コールバック関数
90
+
91
+
92
+
93
+ 前節では説明を省略しましたが、引数に何らかのアクションをする場合は引数値をテストして例外処理を設けるのが安全な設計です。
94
+
95
+ 私はこの手の問題はESで規定されたビルトイン関数を参考にすることが多いのですが、ここでは `Array.prototype.forEach` を事例にあげます。
96
+
97
+
98
+
99
+ ```JavaScript
100
+
101
+ [1, 2, 3].forEach(null); // TypeError: null is not a function
102
+
103
+ ```
104
+
105
+
106
+
107
+ 以下、ES7 (ES2016)より引用します。
108
+
109
+
110
+
111
+ > **22.1.3.10 Array.prototype.forEach ( callbackfn [ , thisArg ] )**
112
+
113
+ > 1. Let O be ? ToObject(this value).
114
+
115
+ > 2. Let len be ? ToLength(? Get(O, "**length**")).
116
+
117
+ > 3. If IsCallable(callbackfn) is false, throw a **TypeError** exception.
118
+
119
+ >
120
+
121
+ > - [22.1.3.10 Array.prototype.forEach ( callbackfn [ , thisArg ] ) - ECMAScript® 2016 Language Specification](http://www.ecma-international.org/ecma-262/7.0/#sec-array.prototype.foreach)
122
+
123
+
124
+
125
+ > **7.2.3 IsCallable ( argument )**
126
+
127
+ > 1. If Type(argument) is not Object, return false.
128
+
129
+ > 2. If argument has a [[Call]] internal method, return true.
130
+
131
+ > 3. Return false.
132
+
133
+ > - [7.2.3 IsCallable ( argument ) - ECMAScript® 2016 Language Specification](http://www.ecma-international.org/ecma-262/7.0/#sec-iscallable)
134
+
135
+
136
+
137
+ 第一引数 `callbackfn` を Object 型に変換し、`[[Call]]` を持たない場合に `TypeError` 例外を発生させます。
138
+
139
+ 同様の処理をしたい場合は次のように書きます。
140
+
141
+
142
+
143
+ ```JavaScript
144
+
145
+ function sample (callbackfn) {
146
+
147
+ callbackfn = Object(callbackfn); // Object 型に変換する
148
+
149
+
150
+
151
+ if (typeof callbackfn !== 'function') { // callbackfn が [[Call]] を持たないなら
152
+
153
+ throw new TypeError(callbackfn + ' is not a function');
154
+
155
+ }
156
+
157
+ }
158
+
159
+ ```
160
+
161
+
162
+
163
+ 次に `Function.prototype.apply` を使うケースを想定します。
164
+
165
+ 上記コードを拡張するなら、引数 `callbackfn` が `Function.prototype` を継承しているかを判定すればいいでしょう。
166
+
167
+
168
+
169
+ ```JavaScript
170
+
171
+ function sample (callbackfn, _arguments) {
172
+
173
+ callbackfn = Object(callbackfn); // Object 型に変換する
174
+
175
+
176
+
177
+ if (typeof callbackfn !== 'function') { // callbackfn が [[Call]] を持たないなら
178
+
179
+ throw new TypeError(callbackfn + ' is not a function');
180
+
181
+ }
182
+
183
+
184
+
185
+ if (!(callbackfn instanceof Function)) {
186
+
187
+ throw new Error(callbackfn ' must be an instance of Function');
188
+
189
+ }
190
+
191
+
192
+
193
+ callbackfn(_arguments);
194
+
195
+ }
196
+
197
+ ```
198
+
199
+
200
+
201
+ ただし、iframe等、異なる所属のドキュメント上で生成された関数に対して上記コードは期待通りに動作しないので厳密性を上げるにはもう少し手を入れる必要があります。
202
+
203
+
204
+
205
+ ```JavaScript
206
+
207
+ if (!(callbackfn instanceof Function) || Object.prototype.toString.call(callbackfn) !== '[object Function]' || ) {
208
+
209
+ throw new Error(callbackfn ' must be an instance of Function');
210
+
211
+ }
212
+
213
+ ```
214
+
215
+
216
+
217
+ ここまで説明しておいて何ですが、実際にはここまで書くことはありません。
218
+
219
+ **ECMAScript には [[Call]] を持たない値に対して関数呼び出ししようとした場合に TypeError 例外を返す仕様がある**からです。
220
+
221
+
222
+
223
+ ```JavaScript
224
+
225
+ function sample (callbackfn, _arguments) {
226
+
227
+ callbackfn = Object(callbackfn); // Object 型に変換する
228
+
229
+
230
+
231
+ callbackfn(_arguments); // [[Call]] を持つ値にたいして関数呼び出しを実行する
232
+
233
+ }
234
+
235
+
236
+
237
+ sample(null); // TypeError: callbackfn is not a function
238
+
239
+ ```
240
+
241
+
242
+
243
+ そして、`Function.prototype.apply` にも `[[Call]]` が存在しない場合に `TypeError` 例外を発生させる規定があります。
244
+
245
+
246
+
247
+ > **19.2.3.1 Function.prototype.apply ( thisArg, argArray )**
248
+
249
+ > 1. If IsCallable(func) is false, throw a **TypeError** exception.
250
+
251
+ > - [19.2.3.1 Function.prototype.apply ( thisArg, argArray ) - ECMAScript® 2016 Language Specification](http://www.ecma-international.org/ecma-262/7.0/#sec-function.prototype.apply)
252
+
253
+
254
+
255
+ 従って、Object 型への型変換処理を除けば次のように書けます。
256
+
257
+
258
+
259
+ ```JavaScript
260
+
261
+ function sample (callbackfn, _arguments, thisArg) {
262
+
263
+ return Function.prototype.apply.call(callbackfn, thisArg, _arguments);
264
+
265
+ }
266
+
267
+
268
+
269
+ function callbackfn1 () {
270
+
271
+ console.log(arguments);
272
+
273
+ }
274
+
275
+
276
+
277
+ sample(callbackfn1, [1, 2, 3]); // [1, 2, 3]
278
+
279
+ sample.__proto__ = null;
280
+
281
+ sample(callbackfn1, [1, 2, 3]); // [1, 2, 3] ([[Prototype]] が Function.prototype でなくとも実行できる)
282
+
283
+ sample({}); // TypeError: Function.prototype.apply was called on #<Object>, which is a object and not a function
284
+
285
+ sample(null); // TypeError: Function.prototype.apply was called on null, which is a object and not a function
286
+
287
+ ```
288
+
289
+
290
+
291
+ ### getElementById.apply()
292
+
293
+
294
+
295
+ jQuery に影響されてか次のようなショートカット関数をたまに見かけます。
296
+
297
+
298
+
299
+ ```JavaScript
300
+
301
+ function $ (id) {
302
+
303
+ return document.getElementById(id);
304
+
305
+ }
306
+
307
+ ```
308
+
309
+
310
+
311
+ このコードには `document` が現在のページの `document` に固定されているという問題があります。
312
+
313
+ iframe要素、`DOMParser` で生成、`window.open` 等、他の所属の `document` も指定できたり、任意の要素ノードを指定できる方が汎用性が高まるでしょう。
314
+
315
+
316
+
317
+ ```JavaScript
318
+
319
+ function $ (id, node) {
320
+
321
+ var getElementById = document.getElementById,
322
+
323
+ _arguments = Array.prototype.slice.call(arguments);
324
+
325
+
326
+
327
+ _arguments = [id].concat(_arguments.slice(2)); // 第二引数を取り除いた引数リストを生成する
328
+
329
+
330
+
331
+ if (Object(node) !== node || !('nodeType' in node) || typeof node.getElementById !== 'function') { // Object 型ではない、もしくは nodeType プロパティを持たない、もしくは getElementById プロパティが [[Call]] を持たないなら
332
+
333
+ node = document;
334
+
335
+ }
336
+
337
+
338
+
339
+ return getElementById.apply(node, _arguments);
340
+
341
+ }
342
+
343
+
344
+
345
+ console.log($('hoge'));
346
+
347
+ console.log($('hoge', document)); // 任意の this 値を指定できる
348
+
349
+ console.log($('hoge', document, 1, 2)); // document.getElementById('hoge', 1, 2) と等価 (将来的に getElementById が第二引数以降を持つようになっても対応できる)
350
+
351
+
352
+
353
+ document.getElementById.__proto__ = null; // [[Prototype]] に Function.prototype がセットされていない状態にする
354
+
355
+ console.log($('hoge')); // TypeError: getElementById.apply is not a function
356
+
357
+ console.log($('hoge', document)); // TypeError: getElementById.apply is not a function
358
+
359
+ console.log($('hoge', document, 1, 2)); // TypeError: getElementById.apply is not a function
360
+
361
+ ```
362
+
363
+
364
+
365
+ `getElementById` は DOM 規定のAPIですが、DOM がJavaScript 以外も考慮されたAPIという事もあってか [[Prototype]] に `Function.prototype` が存在する事を保証していません。
366
+
367
+ 多くの実装では `document.getElemehtById.apply` が期待通りの動作するよう実装されていますが、仕様にない以上、次のように `Function.prototype` を継承していない実装がある可能性があります。
368
+
369
+
370
+
371
+ - [DOM Standard](https://dom.spec.whatwg.org/)
372
+
373
+ - [DOM Standard 日本語訳](https://triple-underscore.github.io/DOM4-ja.html)
374
+
375
+
376
+
377
+ ```JavaScript
378
+
379
+ document.getElementById.__proto__ = null; // [[Prototype]] に Function.prototype がセットされていない
380
+
381
+ document.getElementById.apply(document, ['hoge']); // TypeError: document.getElementById.apply is not a function
382
+
383
+ ```
384
+
385
+
386
+
387
+ 前節と同様、`Function.prototype.apply` を直接呼び出せば安全に実装できます。
388
+
389
+
390
+
391
+ ```
392
+
393
+ function $ (id, node) {
394
+
395
+ var getElementById = document.getElementById,
396
+
397
+ _arguments = Array.prototype.slice.call(arguments);
398
+
399
+
400
+
401
+ _arguments = [id].concat(_arguments.slice(2)); // 第二引数を取り除いた引数リストを生成する
402
+
403
+
404
+
405
+ if (Object(node) !== node || !('nodeType' in node) || typeof node.getElementById !== 'function') { // Object 型ではない、もしくは nodeType プロパティを持たない、もしくは getElementById プロパティが [[Call]] を持たないなら
406
+
407
+ node = document;
408
+
409
+ }
410
+
411
+
412
+
413
+ return Function.prototype.apply.call(document.getElementById, node, _arguments);
414
+
415
+ }
416
+
417
+
418
+
419
+ document.getElementById.__proto__ = null; // [[Prototype]] に Function.prototype がセットされていない状態にする
420
+
421
+ console.log($('hoge'));
422
+
423
+ console.log($('hoge', document)); // 任意の this 値を指定できる
424
+
425
+ console.log($('hoge', document, 1, 2)); // document.getElementById('hoge', 1, 2) と等価 (将来的に getElementById が第二引数以降を持つようになっても対応できる)
426
+
427
+ ```
428
+
429
+
430
+
431
+ ### 更新履歴
432
+
433
+
434
+
435
+ - 2016/11/06 12:00 コールバック関数、Function.prototype.apply, getElementById...etc の説明を追加
436
+
437
+
438
+
25
439
  Re: kkkke さん