## 前言

最近更新了我的系列文章,其中有一部分是**关于 JavaScript 语言中“点表示法”的使用**。

文章中有 5 个工具函数是纯`JavaScript`的,我觉得不仅仅是小程序项目用得上,**其他前端 JS 项目也应该用得上**。

我把文章中的这一部分单独整理出来,**给需要写前端代码的朋友参考参考**。

你可以直接在 [Github 项目]( https://github.com/sdjl/WxMpCloudBooster) 中的前端 `utils.js` 文件中找到这 5 个工具函数。

下面是文章节选,完整的文章列表可以在 [Github 项目]( https://github.com/sdjl/WxMpCloudBooster) 中查看。

## 点表示法`a.b.c`

### 用点表示法给对象赋值:`putValue`函数

想象一下,如果`a`是一个对象,你要给`a.b.c.d`赋值为 1 ,你会这样写?

```javascript
let a // 通过某种方式获得的对象
if (!a.b) {
  a.b = {}
}
if (!a.b.c) {
  a.b.c = {}
}
a.b.c.d = 1
```

拜托,大学生才这样写。为此,我们引入了`putValue`函数:

```javascript
utils.putValue(a, 'b.c.d', 1)
```

`putValue`函数会自动创建`a.b`、`a.b.c`中间对象,它的声明如下:

```javascript
/**
* 向对象中按照路径赋值,如果路径上的中间对象不存在,则自动创建。
* @param {Object} obj - 目标对象。
* @param {string} key - 属性路径,支持'a.b.c'形式。
* @param {*} value - 要设置的值。
* @param {Object} [options={}] - 可选参数。
*   - {boolean} remove_undefined - 如果为 true 且 value 为 undefined ,则删除该属性。
* @throws {Error} 如果 obj 为 null 或 undefined ,或路径不合法(如中间非对象)则抛出异常。
*/
putValue(obj, key, value, {remove_undefined = true} = {}){
  // ...
}
```

使用这个函数有两个地方要注意,一个是如果路径上的中间对象不是对象,会抛出异常。例如`a.b=2`,这里`b`不是对象,此时会抛出异常。

另一个是如果`value`是`undefined`,则会删除该属性。例如下面的代码:

```javascript
utils.putValue(a, 'b.c.d', undefined)
console.log(a) // {b: {c: {}}},putValue 会自动创建中间对象,但不会自动删除空对象
```

但若你真想赋值为`undefined`,可设置`remove_undefined`参数为`false`。


### 读取对象属性值:`pickValue`函数

对应的,如果要获取`a.b.c.d`的值,可以使用`pickValue`函数:

```javascript
utils.putValue(a, 'b.c.d', 1)
const value = utils.pickValue(a, 'b.c.d')
console.log(value) // 1
```

当然你也可以直接使用`javascript`的原生语法:

```javascript
const value = a.b?.c?.d
```

这两种写法,当中间路径不存在时,均会返回`undefined`。

但`a.b?.c?.d`这种写法是硬编码,而在实际开发中路径可能是动态的。例如用户要修改一个配置项,这个配置可能是`user_config.font.size`也可能是`user_config.page.color.background`,如果使用硬编码的方式,可能会写出这样的代码:

```javascript
if (key === 'font.size') {
  user_config.font.size = value
} else if (key === 'page.color.background') {
  user_config.page.color.background = value
}
// 更多的 if 语句...
```

这样写显然不够优雅,看看`putValue`与`pickValue`的组合用法。


```javascript
// 写入用户配置:
utils.putValue(user_config, key, value)

// 读取用户配置
const value = utils.pickValue(user_config, key)
```

不管`key`怎么变,一句话搞定,感觉一下子和大学生拉开差距了是吧?


### 向数组末尾添加元素:`pushValue`函数

在实际开发中,常有向数组末尾添加数据的需求。例如记录用户最近的评论,此时你可以使用`pushValue`函数:

```javascript
const user_data = {} // 用户数据
let comment = {content: '顶'} // 用户的评论

utils.pushValue(user_data, `articles.recent_comments`, comment)

console.log(user_data) // {articles: {recent_comments: [{content: '顶'}]}}
```

在上面代码中,`pushValue`函数先是自动创建了`user_data.articles.recent_comments`数组,然后把`comment`添加到数组末尾。`pushValue`函数的声明如下:

```javascript
/**
* 将值推入对象指定路径的数组中,若路径或数组不存在则自动创建。
* @param {Object} obj - 目标对象。
* @param {string} key - 数组属性的路径,支持'a.b.c'形式。
* @param {*} value - 要推入的值。
* @throws {Error} 如果路径不是数组,则抛出异常。
*/
pushValue(obj, key, value){
  // ...
}
```

再次调用此函数,数组中就会有两个评论:

```javascript
comment = {content: '再顶'}

utils.pushValue(user_data, `articles.recent_comments`, comment)

console.log(user_data) // {articles: {recent_comments: [{content: '顶'}, {content: '再顶'}]}}
```

### 向对象中添加多个属性:`putObj`函数

前面我们使用`putValue`函数向`obj`对象写入了一个属性值,但如果你要写入很多个(例如 100 个)属性值,你可能会使用`for`循环:

```javascript
let user_config = {}
let new_config_keys // 100 个新的配置项(数组)
let new_config_values // 对应的 100 个值(数组)

for (let i = 0; i < new_config_keys.length; i++) {
  utils.putValue(user_config, new_config_keys, new_config_values)
}
```

这样写没问题,但在实战中,你拿到的用户配置往往不是数组的形式,而很可能是一个对象,例如:

```javascript
let new_config = {
  font: {
    size: 16,
    'family.first': 'Arial',
    'family.second': 'sans-serif',
  },
  'page.color.background': '#fff',
  // ...
}

// 你对拿到的配置数据又进一步处理
new_config.update_time = new Date()
```

这种情况下你可以使用`putObj`函数一次性写入多个属性值:

```javascript
utils.putObj(user_config, new_config)

console.log(user_config)

/* 输出如下:
{
  font: {
    size: 16,
    family: {
      first: 'Arial',
      second: 'sans-serif'
    }
  },
  page: {
    color: {
      background: '#fff'
    }
  },
  update_time: '...',
}
*/
```

**注意`putObj`会自动处理上面`new_config`变量中各种路径的写法**。`putObj`函数的声明如下:

```javascript
/**
* 将一个对象的所有属性按路径添加到另一个对象中。
* @param {Object} obj - 目标对象。
* @param {Object} obj_value - 要添加的属性对象,键支持'a.b.c'形式的路径。
* @param {Object} [options={}] - 可选参数。
*   - {boolean} remove_undefined - 如果为 true 且 value 为 undefined ,则删除该属性。
* @throws {Error} 如果 obj 为 null 或 undefined ,或路径不合法(如中间非对象)则抛出异常。
*
* 注意
*   若 obj_value 中出现重复路径,则后者会覆盖前者。
*   如 obj_value = {a: {b: 1}, 'a.b': 2},则结果为 {a: {b: 2}}
*/
putObj(obj, obj_value, { remove_undefined = true} = {}) {
  // ...
}
```

### 从对象中获取多个属性:`pickObj`函数

同样的,我们可以一次性读取多个对象的属性值。例如虽然小程序中的用户配置非常复杂,但当前页面仅关注背景颜色、字体大小等少量配置项,你可以这样使用`pickObj`函数:

```javascript
let user_config // 某个用户的所有配置

// 本页面需要关注的配置
const keys = ['page.color.background', 'font.size', 'font.family']

// 获取当前页面需要的配置
const curr_config = utils.pickObj(user_config, keys)

console.log(curr_config)

/* 输出如下:
{
  'page.color.background': '#fff',
  'font.size': 16,
  'font.family': {
    first: 'Arial',
    second: 'sans-serif'
  }
}
*/

console.log(curr_config.font) // undefined
```

注意,传给`pickObj`函数的第二个参数是一个字符串数组,而不是对象。并且,`pickObj`返回的对象中,属性值不是以`curr_config.font.size`这样的形式返回,而是返回`curr_config['font.size']`。

当然,如果你想要`curr_config.font.size`这样的形式,可用`putObj`转换一下:

```javascript
let obj_config = utils.putObj({}, curr_config)

console.log(obj_config.font.size) // 16
```


## 点表示法在微信小程序中实战演示

为什么要设计这几个函数?为什么要支持`config.a.b.c`与`config['a.b.c']`两种写法混用?为什么传给`putObj`的第二个参数是对象,而传给`pickObj`的第二个参数是字符串数组?为什么`pickObj`返回的对象属性值不是`config.a.b.c`这样的形式,而是`config['a.b.c']`?

因为这样设计符合实战需求,**一句话解释就是:“这样好用”**。

下面我们通过几个案例来演示这些函数在实战中的应用。


### 在 js 中设置用户配置

假设用户首次打开小程序,你需要设置用户默认字体大小为 16 ,背景颜色为白色。可以这样写:

```javascript
let user_config = {}
utils.putValue(user_config, 'font.size', 16)
utils.putValue(user_config, 'page.color.background', '#fff')
```

使用`putValue`设置后,你想修改字体大小和背景颜色?可以这样写:

```javascript
user_config.font.size = 18
user_config.page.color.background = '#000'
```

### 在 wxml 中实现修改用户配置

你可能会在 wxml 页面中实现多个配置项的修改,并且使用同一个函数来处理。这时你可以这样写:

```html
<button bind:tap="changeConfig" data-key="font.size" value="16" >
<button bind:tap="changeConfig" data-key="page.color.background" value="#fff">
```

```javascript
changeConfig(e){
  const { user_config } = this.data
  const { key, value } = e.currentTarget.dataset

  // 从 wxml 中获得点表示法的 key 字符串,直接调用 putValue 函数
  utils.putValue(user_config, key, value)

  // 修改背景色时顺便改一下字体颜色(两种写法混用)
  if (key === 'page.color.background' && value === '#fff') {
    user_config.page.color.font_color = '#000'
  }

  // 记录最近修改时间
  user_config.update_time = new Date()
}
```

### 在 wxml 中使用用户配置

要在页面中使用`page.color.background`与`page.color.font_color`的值,实现根据用户配置显示不同的颜色,可以这样写:

```html
<view style="background-color: {{color.background}}">
  <text style="color: {{color.font_color}}">
      Hello, WxMpCloudBooster!
  </text>
</view>
```

```javascript
onLoad(){
  const { user_config } = this.data
  const color = utils.pickValue(user_config, 'page.color')
  this.setData({color})
}
```

你看,我们传递给`pickValue`的`key`根据实际需求可长可短。


### 初始化默认的用户配置

你希望为每个用户设置一个默认的用户配置,并且你想用常规方式写(不使用点表示法)。可以这样:

```javascript
// 默认配置
const DEFAULT_CONFIG = {
  font: {
    size: 16,
  },
  page: {
    color: {
      background: '#fff',
      font_color: '#000'
    }
  },
  // 这里也可以使用点表示法 a.b.c ,但你不想这样写...
}

App({
  initConfig(){
    let { user_config } = this.data
    utils.putObj(user_config, DEFAULT_CONFIG) // user_config 的其他值会被保留
    // 保存用户配置...
  }
})
```

注意,上面代码中`user_config`可能会有其他没有出现在`DEFAULT_CONFIG`中的配置项,这些配置项会被保留。


### 记录用户最近发表的内容

假如你已经实现了“记录用户最近发布的评论”功能,代码如下:

```html
<button bind:tap="append" data-key="articles.recent_comments" data-prop="comment" >
```

当用户点击这个按钮时,假设`this.data`中已经有一个`comment`对象,你可以这样添加评论:

```javascript
append(e){
  const { user_data } = this.data
  const { key, prop } = e.currentTarget.dataset
  const value = this.data[prop] // prop === "comment"

  utils.pushValue(user_data, key, value) // 注意这里用的是 push
}
```

上面这个`append`函数会把`this.data.comment`对象添加到`user_data.articles.recent_comments`数组的末尾。

然后,此时你希望再增加一个按钮,可以把最近的点赞数据`this.data.like`添加到`user_data.articles.recent_likes`数组的末尾,那么只需一句:

```html
<button bind:tap="append" data-key="articles.recent_likes" data-prop="like" >
```

完成了,你不需要修改`append`函数,只需要给`data-key`和`data-prop`属性设置不同的值即可。

可见,**点表示法很大的目的是为了在`wxml`中可以方便地指定路径,并在`js`中方便地处理这些路径。**


### 在页面中修改多个配置项

假设你有一个修改用户配置项的页面,`wxml`代码如下:

```html
<!-- 注意这里有一个 for 循环 -->
<view wx:for="{{configs}}">
  配置名称:{{item.title}}
  当前值:{{item.value}}
  输入新值:<input type="text" />
  点击修改:<button bind:tap="changeConfig"/>
</view>
```

上面代码使用了`for`循环,`configs`变量中有多少个值,就会显示多少个配置项。

为了实现在用户打开页面时显示的是用户的当前值(而不是默认值),你还需要从`user_config`中读取当前用户的配置值。

代码样例如下:

```javascript
// 代码中写死了需要修改的配置项以及默认值
configs = [
  {title: '字体大小', key: 'font.size', value: 16},
  {title: '背景颜色', key: 'page.color.background', value: '#fff'},
  {title: '字体颜色', key: 'page.color.font_color', value: '#000'},
]

// 读取当前用户的配置值
const uc_obj = utils.pickObj(user_config, configs.map(item => item.key))

// 注意,这里的 uc_obj 是 uc_obj['font.size'] 这样的形式,而不是 uc_obj.font.size

// 用户当前值覆盖默认值
configs.forEach(item => {
  if (uc_obj[item.key] !== undefined) {
    item.value = uc_obj[item.key]
  }
})

this.setData({configs}) // 传给 wxml 页面显示
```

这样你就实现了修改多个配置项的页面,用户打开页面时显示的是用户当前的配置值。

*提问:假设我们坚决不使用点表示法,且要实现上面这些功能,你要如何设计才能如此简单、高效?*


### 让你的函数也支持点表示法

好了,目前我们花了不少篇幅介绍点表示法,这是因为**后面我们会介绍更多的工具函数,而这些工具函数都支持点表示法的调用方式**。

当你编写自己的工具函数时,你可以调用`putValue`、`pickValue`、`pushValue`、`putObj`、`pickObj`这 5 个函数,**轻松地让你的工具函数也支持点表示法**。如果你不知道如何实现,可以参考`utils.js`中其他函数的代码。

**(文章节选完,如果你感兴趣的话可以看看 [Github 项目]( https://github.com/sdjl/WxMpCloudBooster))**
举报· 450 次点击
登录 注册 站外分享
46 条回复  
qwq11 小成 2024-9-1 22:51:04
1. 有个项目叫 TypeScript ,专门解决这种问题的,应该挺小众的,不然你不应该不知道
gouflv 小成 2024-9-1 23:15:38

5 个前端 JS 函数,只为了优雅解决 a.b.c.d = 1 问题

感觉回到了 10 年前
lisongeee 小成 2024-9-1 23:55:57

5 个前端 JS 函数,只为了优雅解决 a.b.c.d = 1 问题

> 假设我们坚决不使用点表示法,且要实现上面这些功能,你要如何设计才能如此简单、高效?

修改 ast 实现就行,指定一个带有特定 Identifier 的 CallExpression 如

__safe__(a.b.c = 1)

将这个 CallExpression 修改为类似 if(!a){a={b:{c:1}}}else if(!a.b){a.b={c:1}}else{a.b.c=1} 的 IfStatement

不过我看大佬你写的都是原生框架,估计懒得弄这种编译插件

另外用字符串表示 MemberExpression ,如果改变量名的时候还得一个一个改,可维护性太低(如果你乐意那当我没说)
renmu 该用户已被删除 2024-9-2 00:21:22
提示: 作者被禁止或删除 内容自动屏蔽
nakar 小成 2024-9-2 00:49:14

5 个前端 JS 函数,只为了优雅解决 a.b.c.d = 1 问题

lodash 的 set 了解下?
DOLLOR 小成 2024-9-2 00:50:18

5 个前端 JS 函数,只为了优雅解决 a.b.c.d = 1 问题

对于 a.b.c.d = 1 ,如果 a.b.c.d 可能存在 undefined ,我一般用 lodash.merge 。
对于合并默认配置,可以用 lodash.defaultsDeep 。

一般还是避免使用计算属性名(即`obj[expr]`),免得后期维护困难。
favourstreet 小成 2024-9-2 07:03:34

5 个前端 JS 函数,只为了优雅解决 a.b.c.d = 1 问题

lens 和 optic 了解一下?楼主你这不是重新发明 lens 吗
daysv 小成 2024-9-2 08:49:32

5 个前端 JS 函数,只为了优雅解决 a.b.c.d = 1 问题

写的很多, 我用 loadsh
murmur 小成 2024-9-2 08:57:01

5 个前端 JS 函数,只为了优雅解决 a.b.c.d = 1 问题

https://www.npmjs.com/package/object-path
哎 前端娱乐圈
12345下一页
返回顶部