-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathNote1. Bound functions.html
381 lines (311 loc) · 23.3 KB
/
Note1. Bound functions.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
<!DOCTYPE HTML>
<html lang="ru-RU">
<head>
<meta charset="UTF-8">
<title>Preview</title>
<style type="text/css">
html {
margin:0;
padding:0;
border:0;
vertical-align:
baseline;
overflow:hidden;
background-color:#f2f2f2;
color:#3c3c3c;
font-size:62.5%;
line-height: 2.4em;
}
body {
margin:0;
padding:0;
border:0;
vertical-align: baseline;
}
a { color:#308bd8;font-weight:bold;text-decoration:none; }
a:hover { text-decoration:underline; }
h1, h2 {
margin-bottom: 10px;
}
h3, h4, h5, h6 {
margin-bottom: 5px;
}
hr {
width: 90%;
margin: 3em auto;
border: 0;
color: #eee;
background-color: #ccc;
height: 1px;
-webkit-box-shadow:0px 1px 0px rgba(255, 255, 255, 0.75);
}
li {
margin-bottom: 10px;
}
pre {
margin: 0 0 30px 0;
width: 100%;
overflow: hidden;
padding: 3px 10px 3px 20px;
-webkit-border-radius: 3px;
background-color: #eee;
border: 1px solid #ddd;
}
code {
font-size: 1.1em;
padding: 2px;
-webkit-border-radius: 3px;
background-color: #eee;
border: 1px solid #ddd;
}
pre code {
border: none;
padding: 0;
background-color: transparent;
-webkit-border-radius: 0;
}
p {
margin-bottom: 20px;
}
blockquote {
margin-left: auto;
margin-right: 0;
width: 95%;
padding: 0 10px;
border-left: 3px solid #ddd;
color: #777;
}
table {
margin-left: auto;
margin-right: auto;
border-bottom: 1px solid #ddd;
border-right: 1px solid #ddd;
border-spacing: 0;
-webkit-box-shadow: 0 2px 4px #999;
}
table th {
padding: 3px 10px;
background-color: #eee;
border-top: 1px solid #ddd;
border-left: 1px solid #ddd;
}
table tr {
}
table td {
padding: 3px 10px;
border-top: 1px solid #ddd;
border-left: 1px solid #ddd;
}
caption {
font-size: 1.2em;
font-weight: bold;
margin-bottom: 5px;
}
.markdowncitation {
}
.footnote {
font-size: 0.8em;
vertical-align: super;
}
.footnotes ol {
font-weight: bold;
}
.footnotes ol li {
}
.footnotes ol li p {
font-weight: normal;
}
/* custom formatting classes */
.shadow {
-webkit-box-shadow: 0 2px 4px #999;
}
#wrapper {
position: fixed;
height: 100%;
overflow-y: scroll;
overflow-x: hidden;
font-size: 1.0em;
width: 99.9%;
margin-right: 5px;
}
#content {
margin: 0 50px 0 50px;
padding: 30px 0 30px 0;
font-family: Cochin, Palatino, Georgia, serif;
font-size: 1.7em;
width: 850px;
margin-left: auto;
margin-right: auto;
}
#content img { display: block; margin: 1em auto;border: none;max-width:100%; }
#wrapper::-webkit-scrollbar { width:8px;height:10px;-webkit-transition:all .45s ease-in; }
#wrapper::-webkit-scrollbar-button:start:decrement, #wrapper::-webkit-scrollbar-button:end:increment { height:0px;display:block;background-color:transparent; }
#wrapper::-webkit-scrollbar-track-piece { background-color:transparent;-webkit-border-radius:6px; }
#wrapper::-webkit-scrollbar-thumb:vertical { height:50px;background-color:#c8c8c8;-webkit-border-radius:6px;-webkit-transition:all .45s ease-in; }
</style>
</head>
<body>
<div id="top-fader"></div>
<div id="wrapper">
<div id="content">
<h1 id="ecmascript.boundfunctions.">Заметка №1. ECMAScript. Bound functions.</h1>
<p>Как мы уже знаем, в редакции <code>ECMA-262-5</code> был стандартизирован метод <code>bind</code>. Источником для текущей версии послужил одноименный метод из библиотеки <a href="http://www.prototypejs.org/">Prototype.js</a>, но имеющий некоторые отличия. Основное предназначение хорошо известно и удачно используется в <code>ES3</code>. Главная задача данной заметки — показать детали и разницу в реализации <code>ES5</code>.</p>
<h4>Цели.</h4>
<p><code>Function.prototype.bind</code> имеет два предназначения: статическое связывание (<code>bound</code>) значения <code>this</code> функции и частичное применение (<code>partial application</code>).</p>
<h4 id="this.">Связывание значения this.</h4>
<p>Основным предназначением метода <code>bind</code> является статическое связывание значения <code>this</code> функции в последующих вызовах. Как мы уже знаем, значение <code>this</code> может изменяться при различных вызовах функции. Т.о. главной целью <code>bind</code> является зафиксировать значение <code>this</code>. Это может пригодиться, когда мы хотим использовать метод объекта в качестве функции обработчика (<code>event handler</code>) какого-либо события.</p>
<pre><code>var widget = {
state: {...},
onClick: function onWidgetClick(event) {
if (this.state.active) {
...
}
}
};
document.getElementById('widget').onclick = widget.onClick.bind(widget);</code></pre>
<p>Для простоты, обработчик устанавливается присваиванием атрибуту <code>onclick</code>, в реальных же условиях, лучше воспользоваться методом <code>addEventListener</code> или <code>attachEvent</code>. В примере показано как можно выставить значение <code>this</code>, чтобы при вызове обработчика оно указывало на объект <code>widget</code>.</p>
<h4>Частичное применение.</h4>
<p>Метод <code>bind</code> также может использоваться для каррирования (<a href="http://ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%80%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5">currying</a>, в математике это называется <em>частичным применением</em> функции). Это техника преобразования функции от множества аргументов в цепочку вложенных функций с единственным аргументом. Это означает, что мы можем создать функцию, основанную на других функциях и некоторые из аргументов этой новой функции могут быть предустановлены. Применение такой функции с частью аргументов называется <em>частичным применением</em> функции.</p>
<pre><code>function foo(x, y) {
//partial
if (typeof y == 'undefined') {
return function partialFoo(y) {
//complete
return x + y;
};
}
// complete
return x + y;
}
foo(10, 20); // 30
foo(10)(20); // 30;
var partialFoo = foo(10); // function
partialFoo(20); // 30</code></pre>
<p>На практике <em>частичное применение</em> функции может пригодиться, когда необходимо часто вызывать функцию в которой одно из значений аргумента повторяется. Также иногда бывает необходимо заранее передать в функцию обработчик (<code>event handler</code>) некоторые данные. Тема <em>частичного применения</em> функции относится к некоторым математическим теоремам и к <a href="http://ru.wikipedia.org/wiki/%D0%9B%D1%8F%D0%BC%D0%B1%D0%B4%D0%B0-%D0%B8%D1%81%D1%87%D0%B8%D1%81%D0%BB%D0%B5%D0%BD%D0%B8%D0%B5">лямбда-исчислению</a> где функции могут принимать только один аргумент.</p>
<pre><code>function foo(x, y) {
return x + y;
}
var partialFoo = foo.bind(null, 10);
partialFoo(20); // 30</code></pre>
<p>В последнем примере значение <code>this</code> для нас не играет никакой роли, так как в функции мы его не используем.</p>
<h4>Реализация.</h4>
<p>Реализация метода <code>bind</code> в <code>ES5</code> отличается от версии, которую используют в <code>ES3</code>. В функциях, которые получаются путем использования метода <code>bind</code>, отсутствуют некоторые внутренние св-ва, которые присутствуют у обычных функций. Это сделано для оптимизации. Такие функции не имеют таких свойств как: <code>prototype</code>, <code>[[Code]]</code> (тело функции — код), <code>[[FormalParameters]]</code> (список формальных параметров функции), а также <code>[[Scope]]</code> (лексическая область видимости, в которой была создана функция).</p>
<p>В то же время такие функции имеют ряд дополнительных внутренних свойств: <code>[[TargetFunction]]</code> — ссылка на оригинальную функцию; <code>[[BoundThis]]</code> — связанное значение <code>this</code>; <code>[[BoundArgs]]</code> — связанные аргументы, которые используются в <code>partial function</code>.</p>
<p>Три внутренних метода у bound-функции — <code>[[Call]]</code>, <code>[[Construct]]</code> и <code>[[HasInstance]]</code> переопределены.</p>
<p>Внутренний метод <code>[[Call]]</code> функции вызывается каждый раз, когда функция активируется (к примеру, вызов функции <code>foo()</code> или когда к функции применяются методы <code>call</code> и <code>apply</code>).</p>
<p>После того, как внутренний метод <code>[[Call]]</code> bound-функции отработает, вызывается оригинальный метод <code>[[Call]]</code> передавая нужное значение <code>this</code>. Это можно представить на псевдо-коде так:</p>
<pre><code>boundFn.[[Call]] = function(thisValue, extraArgs):
var boundArgs = boundFn.[[BoundArgs]],
boundThis = boundFn.[[BoundThis]],
targetFn = boundFn.[[TargetFunction]],
args = boundArgs.concat(extraArgs);
return targetFn.[[Call]](boundThis, args);</code></pre>
<p>Далее вызывается стандартный метод функции <code>[[Call]]</code>, который устанавливает контекст исполнения для свойства <code>[[Code]]</code> (см. <a href="http://es5.github.com/#x13.2.1]">раздел 13.2.1 — [[Call]]</a>).</p>
<pre><code>function F(x, y) {
this.x = x;
this.y = y;
return this;
}
// связываем значение "this" и передаем аргумент "x"
var BoundF = F.bind({z: 30}, 10);
// создаем объект и передаем аргумент "y"
var objectFromCall = boundF(20);
// т.к. значение "this" внутри BoundF указывает на
// объект {z: 30} мы получаем:
alert([
objectFromCall.x, // 10, из [[BoundArgs]]
objectFromCall.y, // 20, из extraArgs
objectFromCall.z // 30, из [[BoundThis]]
]);</code></pre>
<p>Если внимательно посмотреть на псевдо-код работы <code>[[Call]]</code> метода bound-функции, то можно заметить, что значение <code>this</code> (первый аргумент <code>thisValue</code>) нигде не участвует, а только <code>[[BoundThis]]</code> будет передаваться в оригинальный <code>[[Call]]</code>. Таким образом, даже вызывая функцию с использованием методов <code>call</code> и <code>apply</code> значение <code>this</code> не изменится:</p>
<pre><code>var b = BoundF.call({z: 40});
alert(b.z); // по-прежнему 30</code></pre>
<p>Перегруженный метод <code>[[Construct]]</code> (<a href="http://es5.github.com/#x15.3.4.5.2">15.3.4.5.2</a>) после своего выполнения также передаст управление оригинальному методу <code>[[Construct]]</code>, который описан в секции <a href="http://es5.github.com/#x13.2.2">13.2.2</a>. Есть одно важное замечание при использовании bound-функции в качестве конструктора. Оригинальный метод <code>[[Construct]]</code> вызывается, после того как отработает перегруженный метод <code>[[Construct]]</code>, далее активируется <strong>оригинальный</strong> метод <code>[[Call]]</code>, а не перегруженный. Это означает, что в качестве значения <code>this</code> будет использоваться <strong>только что созданный объект</strong>, а не <em>связанное значение</em>.</p>
<p>Это довольно логично, но некоторым может показаться странным такое поведение. Это важное отличие от реализации метода <code>bind</code> на JavaScript. Так метод <code>bind</code> в библиотеке <code>Prototype.js</code> всегда использует связанное значение <code>this</code>. Т.о. если в конструкторе явно возвращается значение <code>this</code> то мы получим связанное значение. В противном случае результат bound-функции конструктора неопределен и зависит от возвращаемого значения bound-функции.</p>
<p>По большому счету, невозможно реализовать на JavaScript метод <code>bind</code>, который будет полностью удовлетворять реализации <code>ES5</code>. Но можно добиться <a href="http://code.google.com/p/js-examples/source/browse/trunk/bind_emulation.js">приемлемой эмуляции</a>.</p>
<p>В реализации <code>bind</code> метода в редакции <code>ES5</code>, мы получим следующие результат:</p>
<pre><code>// добавим св-во в прототип объекта
F.prototype.a = 40;
// 15.3.4.5.2 [[Construct]] для bound-функции
var objectFromConstruct = new BoundF(20);
alert([
objectFromConstruct.x, // 10, из [[BoundArgs]]
objectFromConstruct.y, // 20, из extraArgs
objectFromConstruct.z, // undefined, т.к. не используется [[BoundThis]]
objectFromConstruct.a // 40, из прототипа F.prototype === objectFromConstruct.[[Prototype]]
]); </code></pre>
<p>Как видно из примера у bound-функции отсутствует св-во <code>prototype</code> и значение берется из прототипа оригинальной функции. Даже если мы вручную добавим это св-во в bound-функцию, ничего не изменится, т.к. <code>[[Construct]]</code> и <code>[[Call]]</code> используют внутри оригинальную функцию.</p>
<pre><code>alert(BoundF.prototype); // undefined
BoundF.prototype = {
constructor: BoundF,
a: 100,
b: 200
};
var objectFromConstruct2 = new BoundF;
// св-ва по прежнему берутся из прототипа оригинальной функции
alert([
objectFromConstruct2.a // 40, из F.prototype а не 100
objectFromConstruct2.b // undefined
]);</code></pre>
<p>Резюмируя:</p>
<ul>
<li>Простой вызов — <code>BoundF()</code> — используется <code>boundThis</code>;</li>
<li><code>call/apply</code> — <code>BoundF.call(manualThis) — используется</code>boundThis`;</li>
<li>Вызов в качестве конструктора — <code>new BoundF()</code> — используется только что созданный объект;</li>
</ul>
<p>Перегруженный метод <code>[[HasInstance]]</code> (<a href="http://es5.github.com/#x15.3.4.5.3">15.3.4.5.3</a>) после своего выполнения передает управление оригинальному методу <code>[[HasInstance]]</code> (<a href="http://es5.github.com/#x15.3.5.3">15.3.5.3</a>). Этот внутренний метод используется оператором <code>instanceof</code>, который возвращает <code>true</code> как для оригинальной так и для bound функций:</p>
<pre><code>alert([
objectFromConstruct instanceof F, // true
objectFromConstruct instanceof BoundF // true
]);</code></pre>
<p>Нужно отметить, что из-за такой делегации оператор <code>instanceof</code> вернет <code>true</code> даже для того объекта, который был создан через конструктор оригинальной функции:</p>
<pre><code>var object = new F;
alert([
object instanceof F, // true
object instanceof BoundF // true
]);</code></pre>
<p>Это также относится к использованию метода <code>Object.create</code>. Даже если мы определим св-во <code>prototype</code> у <code>BoundF</code> и передадим его в качестве первого аргумента в <code>Object.create</code>, оператор <code>instanceof</code> вернет <code>false</code> как для оригинальной функции конструктора, так и для bound-функции конструктора:</p>
<pre><code>var boundProto = {
constructor: BoundF
};
BoundF.prototype = boundProto;
var foo = Object.create(BoundF.prototype);
console.log(Object.getPrototypeOf(foo) === BoundF.prototype); // true
// из-за перегруженного [[HasInstance]],
// который в итоге вызовет F.[[HasInstance]] мы получим false
console.log(foo instanceof BoundF); // false
console.log(foo instanceof F); // false
var bar = Object.create(F.prototype);
console.log(bar instanceof BoundF); //true
console.log(bar instanceof F); // true
// но если мы установим F.prototype
// в нужный нам объект, то instanceof
// вернет true
F.prototype = boundProto;
console.log(foo instanceof BoundF); // true
console.log(foo instanceof F); // true</code></pre>
<p>Есть и еще одна особенность у bound-функций. Если у такой функции попытаться запросить св-во <code>caller</code> или <code>arguments</code> то немедленно будет брошено исключение <code>TypeError</code> даже в <code>non-strict</code> режиме, в отличие от обычных функций:</p>
<pre><code>alert([
F.caller, // ошибка только в strict режиме
F.arguments, // ошибка только в strict режиме
BoundF.caller, // всегда ошибка
BoundF.arguments // всегда ошибка
]);</code></pre>
<h4>Функция конструктор с различным количеством аргументов.</h4>
<p>На <a href="https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/bind#Supplemental">MDC</a> была описана интересная техника использования функции конструктора с различным количеством аргументов. Для этого в конструктор, в качестве аргумента передается массив с нужными значениями. Получается некоторое подобие <code>Function.prototype.apply</code>, но с возможностью использования с ключевым словом <code>new</code> — для использования функции как конструктор. Ниже представлена немного модифицированная версия примера с MDC:</p>
<pre><code>Function.prototype.construct = function(args) {
var boundArgs = [].concat.apply(null, args),
boundFn = this.bind.apply(this, boundArgs);
return new boundFn();
};
function Point(x, y) {
this.x = x;
this.y = y;
}
var point = Point.construct([2, 4]);
console.log(point.x, point.y) // 2, 4</code></pre>
<p>Но нужно помнить, что такой подход не очень эффективен, так как при каждом вызове создается <code>bound</code>-функция.</p></div>
</div> <!-- End wrapper -->
<div id="bottom-fader"></div>
</body>
</html>