跳转到内容
View in the app

A better way to browse. Learn more.

彼岸论坛

A full-screen app on your home screen with push notifications, badges and more.

To install this app on iOS and iPadOS
  1. Tap the Share icon in Safari
  2. Scroll the menu and tap Add to Home Screen.
  3. Tap Add in the top-right corner.
To install this app on Android
  1. Tap the 3-dot menu (⋮) in the top-right corner of the browser.
  2. Tap Add to Home screen or Install app.
  3. Confirm by tapping Install.
欢迎抵达彼岸 彼岸花开 此处谁在 -彼岸论坛

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

发表于

前言

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

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

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

你可以直接在 Github 项目 中的前端 utils.js 文件中找到这 5 个工具函数。

下面是文章节选,完整的文章列表可以在 Github 项目 中查看。

点表示法a.b.c

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

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

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

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

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

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

/**
* 向对象中按照路径赋值,如果路径上的中间对象不存在,则自动创建。
* @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不是对象,此时会抛出异常。

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

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

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

读取对象属性值:pickValue函数

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

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

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

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

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

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

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

这样写显然不够优雅,看看putValuepickValue的组合用法。

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

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

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

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

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

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函数的声明如下:

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

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

comment = {content: '再顶'}

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

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

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

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

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[i], new_config_values[i])
}

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

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

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

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

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函数的声明如下:

/**
 * 将一个对象的所有属性按路径添加到另一个对象中。
 * @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函数:

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转换一下:

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

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

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

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

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

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

在 js 中设置用户配置

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

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

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

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

在 wxml 中实现修改用户配置

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

<button bind:tap="changeConfig" data-key="font.size" value="16" >
<button bind:tap="changeConfig" data-key="page.color.background" value="#fff">
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.backgroundpage.color.font_color的值,实现根据用户配置显示不同的颜色,可以这样写:

<view style="background-color: {{color.background}}">
  <text style="color: {{color.font_color}}">
      Hello, WxMpCloudBooster!
  </text>
</view>
onLoad(){
  const { user_config } = this.data
  const color = utils.pickValue(user_config, 'page.color')
  this.setData({color})
}

你看,我们传递给pickValuekey根据实际需求可长可短。

初始化默认的用户配置

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

// 默认配置
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中的配置项,这些配置项会被保留。

记录用户最近发表的内容

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

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

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

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数组的末尾,那么只需一句:

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

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

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

在页面中修改多个配置项

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

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

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

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

代码样例如下:

// 代码中写死了需要修改的配置项以及默认值
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 => {
  item.value = uc_obj[item.key]
})

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

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

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

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

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

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

(文章节选完,如果你感兴趣的话可以看看 Github 项目

Featured Replies

No posts to show

创建帐户或登录来提出意见

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.