-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path6-1.html
331 lines (329 loc) · 15.7 KB
/
6-1.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
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="./public/favicon.ico" />
<meta http-equiv="cache-control" content="no-cache" />
<title></title>
<link rel="stylesheet" href=" https://necolas.github.io/normalize.css/8.0.1/normalize.css" />
<link rel="stylesheet" href="./hightlight/default.min.css" />
<link rel="stylesheet" href="./css/main.css" />
<link rel="stylesheet" href="./css/copybutton.css" />
<link rel="stylesheet" href="./css/hightlight.css" />
<script src="./hightlight/hightlight.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/clipboard.js/2.0.11/clipboard.min.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BEVZJDBC7Z"></script>
<script src="./js/gtag.js"></script>
</head>
<body>
<header>
<nav>
<h1>
<span id="toggle-menu"></span>
<a href="index.html"></a>
</h1>
</nav>
</header>
<main>
<aside>
<nav></nav>
</aside>
<article>
<h2>Chapter 6 運用代理器管理特性存取</h2>
<p>
代理器是 ES6 中一項有趣且具威力的特徵功能,它可作為物件和API
運用兩者之間居中協調的角色。概略地說,你可以使用一個 Proxy 代理器來決定偏好的存取 target
目標物件特性的方式。handler
物件可用來設定你的代理器的條件,定義且限制底層物件被存取的方式,接下來我們將會進行探討。
</p>
<h2>6-1 代理器初探</h2>
<p>
在預設的條件下,代理器並不會做太多事情一事實上是根本不會做任何事。如果你不進行任何的設定,代理器就只像是一個通往
target 物件的通道,一般稱為「不管理的轉送代理器 (no-op forwarding
proxy)」,意思為代理器物件上的所有操作均依從目標物件。
</p>
<p>
在下面的程式範例,我們會建立一個不管理的轉送代理器 Proxy。你可以觀察到如何藉由為
proxy.exposed 指定一個值 ,來將值傳遞給
target.exposed。你可以將代理器稅太目標物件的守門員:它可允許某些操作通過:禁止某些操作進得附他們會仔細地檢查每個與目標物件的互動操作‧
</p>
<pre><code class="language-js">
const target = {}
const handler = {}
const proxy = new Proxy(target, handler)
proxy.exposed = true
console.log(target.exposed)
// true
console.log(proxy.somethingElse)
// undefined
</code></pre>
<p>
我們可以為代理器物件加入一些機關 (traps),讓它更為有趣一些。機關可允許你用不同的方式攔截與
target 物件之間的互動行為,只要這些互動行為都是透過 proxy 物件進行。例如:我們可以使用一個
get 機關將每個試圖擷取 target 目標物件特性的操作記錄下來,或是一個 set
機關來避免某些特性被寫入變更。下面讓我們先來學習 get 機關。
</p>
<h3 id="6-1-1">6-1-1 定義 get 機關攔截擷取操作</h3>
<p>
在下方程式碼的 proxy 能夠記錄每個特性被擷取的事件,因為它有一個 handler.get
機關。它也可以將欲擷取的特性值在提供給特性操作器之前,先將值進行轉換後再回傳。
</p>
<pre><code class="language-js">
const handler = {
get(target, key) {
console.log( `Get on property"${ key }"`)
return target[ key]
}
}
const target = {}
const proxy = new Proxy(target, handler)
proxy.numbers = [1, 1, 2, 3, 5, 8, 13]
proxy.numbers
// 'Get on property"numbers'
// [1, 1, 2, 3, 5, 8, 13]
proxy['something-else']
// 'Get on property"something-else"'
// undefined
</code></pre>
<p>
ES6 也推出了一個內建的 Reflect 反射物件,功能與代理器互補。在ES6 代理器中所設定的機關與
Reflect 反射 API 有一對一對應關係:對每一個機關,在 Reflect
中都會有一個對應的反射方法。當我們想要將預設的行為設定為代理器機關時,這些方法
會特別有效;但我們並不需要繼續往下瞭解它的實作方法。
</p>
<p>
在下面的程式範例中,我們使用 Reflect.get 作為 get
操作的預設行為,同時也不需要擔心以手動的方式擷取 target 物件中的
key特性在這個案例中,操作看起來可能很簡單,但在其他機關的預設行為可能更難正確地記憶和實作。我們可以將機關中的每一個參數都轉導至反射API
且將它的結果回傳。
</p>
<pre><code class="language-js">
const handler = {
get(target, key) {
console.log( `Get on property "${ key }"` )
return Reflect.get(target, key)
}
}
const target = {}
const proxy = new Proxy(target, handler)
</code></pre>
<p>
get 機關不需要總是回傳原始 target[key]
的值。想像一下這樣的情境,你希望名稱以底線開頭的特性是不允許擷取的;在此情況下,你可以拋出一個錯誤,讓使用者知道該特性在透過代理器的檢查後是無法擷取的。
</p>
<pre><code class="language-js">
const handler = {
get(target, key) {
if (key.startsWith('_')) {
throw new Error( `Property "${ key }" 特性不允許存取。` )
}
return Reflect.get (target, key)
}
}
const target = {}
const proxy = new Proxy(target, handler)
proxy._secret
// Uncaught Error: "_secret" 特性不允許存取。
</code></pre>
<p>
當能夠定義 target
目標物件的擷取規範時,逶過代理器的條件設定禁止某些特性的擷取,就變得非常的有用:而且不需要將
target
物件暴露出來。就可以透過代理器使用‧以這樣的方式,你仍然可以自由地擷取目標物件,但是必須被強制透過代理器進行,並受限於定義的範圍,以精確地控制外界與物件互動的方式‧這在
ES6 推出代理器之前是不可能辦到的。
</p>
<h3 id="6-1-2">6-1-2 定義 set 機關攔截擷取操作</h3>
<p>
相對於 get 的機關,set
的機關可以攔截特性值的指定。假設我們想禁止對名稱以底線開頭的特性進行設定,那麼可以將之前所實作的
get 機關複製,就可以阻擋對這些的特性進行設定操作。
</p>
<p>
在下一個範例的 Proxy 代理器會禁止對名稱以底線開頭的特性進行 get擷取和 set 設定操作,當透過
Proxy 存取 target 物件時。請注意在此處set 機關如何回傳 true 呢?在 set 機關中回傳 true
值,代表著將指定的 value 值設定予 key 特性是成功的;如果 set 機關中回傳 false
值在嚴格模式(strict mode)下 ,對特性進行設定則會回傳 TypeError
的結果,或在一般模式下直接失敗且無回傳訊息。如果我們以 Reflect.set
取代,我們就不需要關注這些實作的細節:可以 return Reflect.set(target, key, value)
的方式即可。以這樣的方式撰寫,當未來有人閱讀到我們的程式碼時,他們就能夠理解使用
Reflect.set 的意義也就是預設的行為,與案例中使用 Proxy 物件並非總是相等。
</p>
<pre><code class="language-js">
const handler = {
get(target, key) {
invariant(key, 'get')
return Reflect.get(target, key)
},
set(target, key, value) {
invariant(key, 'set')
return Reflect.set(target, key, value)
}
}
function invariant (key, action) {
if (key.startswith('_')) {
throw new Error( `Can't ${ action } private "${ key }" property`)
}
}
const target = {}
const proxy = new Proxy (target, handler)
// 下方的程式碼說明proxy 物件如何回應使用者的操作。
proxy.text = 'the great black pony ate your lunch'
console.log(target.text)
'the great black pony ate your lunch'
proxy._secret
// Error:無法擷取私有的 "_secret" 特性
proxy._secret = 'invalidate'
// Error:無法設定私有的 "_secret" 特性
</code></pre>
<p>
被代理的物件,也就是上一個範例的 target
物件,必須完全的隱藏起來不被使用者得知,所以使用者被強制透過 proxy
物件對它進行操作。避免對 target 物件直接進行操作,代表著使用者必須道守 proxy
物件的存取規則一例如:「以底線開頭的特性是禁止使用」。因此,你可以將被代理的物件包裹於一個函式,並讓函式回傳
proxy 物件。
</p>
<pre><code class="language-js">
function proxied(){
const target = {}
const handler = {
get(target, key) {
invariant (key,'get')
return Reflect.get(target, key)
},
set(target, key, value) {
invariant (key,'set')
return Reflect.set(target, key, value)
}
}
return new Proxy(target, handler)
}
function invariant (key, action){
if (key.startswith('_')) {
throw new Error(`Can't ${ action } private "${ key }" property `)
}
}
</code></pre>
<p>
使用方式仍然相同,除了存取 target 物件會完全受到 proxy 物件和它的 機關管控。至此,所有
target 物件的 _secret 特性,透過 proxy 物件均 無法存取,且因為 target 物件也無法被除了
proxied 函式之外的方式存 取,所以它就再也無法被使用者操作了。
</p>
<p>
一般使用的方法,通常是提供一個代理函式,它需要一個 original
原始物件並回傅一個代理器。當你需要發佈一個公開的 API
時,就可以呼叫該函式,如下面範例所展示。concealWithPrefix 函式將 original
原始物件包裹於一個 proxy 代理器中,。它會對 Prefix 變數值開頭的特性管控(
若未提供則預設為底線_ ),不允許對其存取操作
</p>
<pre><code class="language-js">
function concealWithPrefix(original, prefix=''){
const handler = {
get (original, key){
invariant (key,'get')
return Reflect.get(original, key)
},
set(original, key, value) {
invariant (key,'set')
return Reflect.set(original, key, value)
}
}
return new Proxy(original, handler)
function invariant (key, action) {
if (key.startswith (prefix)) {
throw new Error ( `Can't ${ action } private"${ key }"property`)
}
}
}
const target = {
_secret: 'secret',
text: 'everyone-can-read-this'
}
const proxy = concealWithPrefix(target)
// 提供 proxy 予使用者操作
</code></pre>
<p>
你可能會認為,在 ES5 中可以藉由非公開的變數且有效範圍僅於concealWithPrefix
函式,來達到相同的功能,並不需要使用 Proxy
。但差異在於,代理器允許你將特性的存取權動態地「私有化」。若不使用 Proxy
,你無法將每個名稱以底線開頭的特性註記為非公開,僅允許私有使用。你可能可以對物件使用
Object.freeze
方法,但接著你也無法變更特性參考。或是你可以對每個特性去定義擷取和設定特性操作器,但再次地會發現,你無法對每一個特性都禁止存取,只有明確定義在擷取器和設定器的才會生效。
</p>
<h3 id="6-1-3">6-1-3 以代理器進行格式驗證</h3>
<p>
有些情況下我們會取得一個物件,其中包含著使用者的輸入,希望能夠進行輸入格式的驗證、是否符合預期的輸入結構、是否具備指定的特性、這些特性的頻型、以及這些特性應如何填入。例如:我們會想要驗證
customer 的電子郵件欄位是否包含一個 email 地址、cost 費用欄位是否包含一個數值、name
姓名欄位是否已填寫完成。
</p>
<p>
格式驗證有很多種方法可以採用。你可以使用驗證函式,如果在物件中發現無效的值時,便抛出錯諆;一旦確認值為有效,也必須確保該物件無法被變更。你可以對每一個特性個别進行驗證,但是當它們被變更後,必須記得再次進行驗證。也可以使用
Proxy 物件,提供使用者一個 Proxy
代理器操作目標物件,可以確保物件絕不會進入無效狀態,否則將會抛出錯誤。
</p>
<p>
添過 Proxy 進行格式驗證的另一個觀點,就是它可幫助你將驗證所需的各項因素與 target
物件分離出來,否則各種驗證的因子經常難以管控。這樣 target 物件就可以維持單純的 JavaScript
物件,表示當你提供使用者一個驗證代理器時,你自己仍然可以保有一份有效的且未受變更過的資料,作為一份由代理器認可的資料。
</p>
<p>
就像一個驗證函式,處理器在各個 Proxy 實例可被重新設定,不需要依賴延伸(extend)或 ES6
中的類别。
</p>
<p>
在接下來的範例,我們有一個簡單的 validator 物件,並有一個 Set
機關會於映射中查找特性。當透過代理器進行特性設定時,會使用特性的鍵於映射中尋找。如果在映射中含有該特性的機關,便會執行該條件的函式,無論該設定任務是否可正確設定完成。只要
person 的特性是透過代理器操作 validator 物件進行設定,模組的不變性
(invariants)就可以依據我們事先定義好的驗證規則達成。
</p>
<pre><code class="language-js">
const validations = new Map()
const validator={
set(target, key, value) {
if (validations.has(key)) {
return validations[key] (value)
}
return Reflect.set(target, key, value)
}
}
validations.set('age', validateAge)
function validateAge (value) {
if (typeof value !=='number' || Number.isNaN (value)) {
throw new TypeError ('Age must be a number')
}
if (value <= 0) {
throw new TypeError ('Age must be a positive number')
}
return true
}
</code></pre>
<p>
以下的程式碼說明我們如何使用 validator 處理器。這個一般性的代理器處理器會被傅遞至 Proxy
中以驗證 person
物件。處理器接著會執行我們所定義的機制,也就是確認透過代理器所設定的特性值,是否可以通過機制中的驗證規則。在這個案例中,我們加入了一個驗證規則,規則是
age 特性必須為正數。
</p>
<pre><code class="language-js">
const person = {}
const proxy = new Proxy (person, validator)
proxy.age = 'twenty three'
// TypeError : Age 必須是一個數值
proxy.age = NaN
// TypeError : Age 必須是一個數值
proxy.age = 9
// TypeError : Age必須是一個正数
proxy.age = 28
console.log(person.age)
// 28
</code></pre>
<p>
當代理器提供了一種機制,可透過對存取規則的定義,管控使用者對物件可行與不可行的操作之外;還有一種更嚴格的代理器設定,可讓我們在需要時完全關閉對
target 的存取:可廢止的代理器。
</p>
</article>
</main>
</body>
<script type="module" src="./js/main.js"></script>
</html>