背景

本文是根据阮一峰大大的文章写几个小Demo,这里的代码都是我自己看了一遍需求敲出来的,不存在直接复制黏贴的情况,请放心。

数组负数索引

众多语言都支持数组的索引为负数,就像这段Python代码一样:

1
2
3
arr = [1, 2, 3]
print(arr[-1])
## 3

我们可以通过Proxy让JS也支持这一操作:

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
function getPositiveKey(key, length) {
const index = Number(key)
if (index < 0) {
key = String(index + length)
}
return key
}

function negativeArray(arr) {
return new Proxy(arr, {
get(target, key) {
key = getPositiveKey(key, target.length)
return Reflect.get(target, key)
},
set(target, key, value) {
key = getPositiveKey(key, target.length)
return Reflect.set(target, key, value)
}
})
}

const arr = negativeArray([5, 6, 7])

console.log(arr[-1])
// 7

console.log(arr[1])
// 6

注意,尽管我们用arr[-1],但是传进去的key还是会被转为字符串,所以需要一步转换。

同时,我们在getPositiveKey中,为了符合Reflect.get函数的参数类型要求,把计算出来的索引也转换成了字符串,不过这里是无所谓的,因为传给Reflect后它会自己进行一个转换。

数字链式调用

下面代码的目的是:

  • 如果获取的是value,就返回数字本身。
  • 如果获取的东西在Math中存在,就调用它,并且把结果再包装成我们的代理对象。
  • 都不存在就返回一个undefined,因为我这里是一个小Demo,就不做太多的处理了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function proxy(n) {
return new Proxy({ n }, {
get(target, key) {
if (key === 'value') {
return target.n
}

if (key in Math) {
return proxy(Math[key](target.n))
}

return undefined
}
})
}

console.log(proxy(64).sqrt.log2.value)
// 3

上述操作就是先把64开根,结果是8; 再取以2为底的对数,结果是3。

注意到这里把数字包了一层对象,因为Proxy是不支持代理基本类型的。

DOM生成器

此Demo的Codepen地址,可以在线体验一下。

每次都写document.createElement然后dom.appendChild实在是太煎熬了,所以有了这样一个例子:

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
const dom = new Proxy({}, {
get(_, tag) {
return (...children) => {
const el = document.createElement(tag)
children.forEach(child => {
if (typeof child === 'string') {
child = document.createTextNode(child)
}

if (child instanceof Node) {
// 所有DOM都继承了Node
el.appendChild(child)
} else {
// 如果child不是Node就认为它是属性对象
Object.entries(child).forEach(([key, value]) => {
el.setAttribute(key, value)
})
}
})

return el
}
}
})

const root = document.getElementById('root')
const el = dom.p(
dom.span('Hello! '),
'This is ',
dom.a('my website', { href: 'https://kifuan.top', target: '_blank' }),
)
root.appendChild(el)

别忘了在HTML里带上一个:

1
<div id="root"></div>

数据双向绑定

Vue里面最香的就是这玩意,为表单数据获取验证省出了巨大时间,那么在这里通过Proxy的知识我们也可以实现一个简单的双向绑定。

外部链接

如果只是把这些链接写在开头很容易被忽略掉,所以我这里单独摘出来一个板块让下面这些更醒目

  • 视频 我录了一个视频从头开始实现这个功能,过程中有说到怎么去思考这个问题的实现方法,这里去B站看

  • Codepen 本Demo的Codepen地址,可以在线体验。

需求

只要我们这么写

1
2
<input type="text" data-value="foo" data-update="foo">
<p data-value="foo"></p>

当上方输入的时候下方就会实时更新,也就是说:

  • data-value指的是当数据更新时被实时更新
  • data-update指的是数据会被这个元素更新

实现

Talk is cheap. Show me the code.

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
'use strict'

function bindUpdates(setProp) {
return Array.from(document.querySelectorAll('*[data-update]'))
.reduce((pre, cur) => {
const key = cur.dataset.update
cur.addEventListener('keyup', () => {
setProp(key, cur.value)
})
// 一个key只会被一个DOM更新
return { ...pre, [key]: cur }
}, {})
}

function bindValues() {
return [...document.querySelectorAll('*[data-value]')]
.reduce((pre, cur) => {
const key = cur.dataset.value
// 一个key会被多个DOM读取
pre[key] ||= []
pre[key].push(cur)
return pre
}, {})
}

function bind() {
const values = bindValues()

const updates = bindUpdates((key, value) => {
data[key] = value
})

const data = new Proxy({}, {
set(_, key, value) {
values[key].forEach(el => {
if (el.value !== undefined) {
el.value = value
} else {
el.innerText = value
}
})
// 表示设置成功
return true
},

get(_, key) {
return updates[key].value
}
})

return data
}

const data = bind()

之后在HTML里这么写:

1
2
3
4
5
6
7
8
<h1>双向绑定</h1>
<input type="text" data-value="account" data-update="account" placeholder="账号"><br><br>
<input type="text" data-value="password" data-update="password" placeholder="密码">
<p>账号: <span data-value="account"></span></p>
<p>密码: <span data-value="password"></span></p>

<button onclick="data.account = ''">清空账号</button>
<button onclick="data.password = ''">清空密码</button>

就可以达到预期效果了,想要预览可以到Codepen里面查看,在这里再放一个链接