头图
本文作者:相逢在雨季

在面试的过程中,经常能看到候选人写熟练掌握或者精通ES6,那就拿一道题试一试你的掌握情况吧,题目其实非常简单,如下所示。

// 有以下数据结构
const data = [{
    key: 'name',
    value: '豆皮范er'
}, {
    key: 'age',
    value: 1,
}, {
    key: 'from',
    value: '数据平台'
}]

// 实现一个转换函数 processFn
// 根据对应的key,value生成对象
// 要求尽可能的多用一些es6的新特性,尽可能的少去声明变量,减少副作用。

const processFn = data => processData
{
    name: '豆皮范er',
    age: 1,
    from: '数据平台'
}

好多候选人一看题目,思路一下就有了,啪一下就写出来了,很快啊。
先看下实现的最多的版本


const processFn = data => {
    const processData = {};
    for (let i = 0; i < data.length; i++) {
        const key = data[i].key;
        const value = data[i].value;
        processData[key] = value;
    }
    return processData;
}

很标准的实现,不是吗,问这里面用到了哪些ES6的特性,可以看出,主要用到了三个,一个是箭头函数=>,一个是块级声明let,一个是常量声明const

这里可以在顺手问一个拓展的考点,如何实现一个真正的const,因为这里的const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动, 那如何实现一个真正只能只读的对象呢,下面给出答案。
const constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

着手ES6的改造

继续上面的题,其实如果单独问ES6有哪些新特性,很多人都能答出来很多,但是一旦运用到真正的代码编写中,就还是使用回老一套,其实就这道题而言比如箭头函数解构赋值扩展运算符使用表达式作为对象的属性名, 等等这些ES6的特性都能运用其中,下面就把这些特性一一代入到这个题当中,来体验一下综合运用ES6的感觉吧。
首先,先加上解构赋值解构赋值可以很方便的从一个对象中取值,这里可以在取得数组中每一个对象的keyvalue时运用上。

const processFn = data => {
    const processData = {};
    for (let i = 0; i < data.length; i++) {
        const { key, value } = data[i];
        processData[key] = value;
    }
    return processData;
}

在这里还可以顺带的把for循环改成 for...of, for...of可以遍历一个迭代器(iterator), Array本质上就是一个迭代器,因为它实现了迭代器接口。

这里又可以拓展的去问一个问题,标准三连,什么是迭代器?javascript都有哪些迭代器?如何实现一个迭代器?这个大家下来后自行回答

还是先继续改造代码。把解构的过程放在for...of上,代码就变得更简洁了一些。

const processFn = data => {
    const processData = {};
    for (let { key, value } of data) {
        processData[key] = value;
    }
    return processData;
}

然后,可以加上扩展运算符了,对于一个对象来说,扩展运算符的作用是可以合并一个对象,在ES6的时代,也可以使用Object.assign。这两个方法其实都可以。但无论用哪个方法,都会遇到一个问题,属性是一个动态的变量,在ES5的时代,想把属性作为变量进行对象赋值,只能使用以下方式

const keyName = 'name';
const obj = {}
obj[keyName] = 'xxxx';

ES6允许我们使用字面量的形式来完成这个事情,只需要给属性表达式加上一个[]即可,现在组合使用扩展运算符对象属性表达式来优化一下代码吧。

const processFn = data => {
    const processData = {};
    for (let { key, value } of data) {
        Object.assign(processData, {[key] : value})
    }
    return processData;
}

或者

const processFn = data => {
    let processData = {};
    for (let { key, value } of data) {
        processData = {...processData, [key]: value}
    }
    return processData;
}

这样就成功的把这些ES6的特性综合的运用上了,现阶段总结一下用到的ES6特性吧,constlet箭头函数扩展运算符for...of, 解构赋值对象属性表达式。算下来有个了。

减少一些副作用

不过这还没有结束,题目中还有一个要求,尽可能的少去声明变量,减少副作用。除了函数声明所必须的声明const,剩下的还有两处显式声明,那最少可以把这种显式的声明减少到几个呢,答案实际上是0个,这时就需要去改动for循环了,因为for循环是肯定会有显示的const或者let声明的,for循环本身就是命令式的编程方式,如果使用的是声明式的方式,那就可能会把显式声明变成参数的隐式声明,可以进一步提高代码的可读性和简洁性,这一点其实卡住了很多候选人。
解决的思路其实可以转化为,如何把一个数组进行所谓的折叠(fold),就是把数组的多项合并成一项,这里其实涉及到的是一个ES5的数组函数,reduce,众所周知,对reduce的经典使用当然就是累加了。

[1,2,3].reduce((total, item) => total + item, 0)

其实以类比的思想,把对应的数字换成数组中的对象,把累加换成提取keyvalue,进行合并,那数组的合并就是一种变相的累加了。有了reduce,就可以更大的发挥ES6的结合性了。

整合代码

const processFn = data => data.reduce((obj, {key, value}) => {
    return {...obj, [key]: value};
}, {})

最后的return其实也可以去掉,让代码可以在一行的空间就可以实现了,在箭头函数返回对象的时候加一个括号即可。

const data => data.reduce((obj, {key, value}) => ({...obj, [key]: value}), {})

至此,我们对这个题的改造就结束了,综合使用了ES6特性,以及减少了显式的声明。写出这样的代码,这道题的解答就可以毕业啦。

彩蛋时间

这就结束了吗,还没有,可以看出,ES6虽然很强大,但是要掌握它还是要学习各种特性,而且还要综合一些ES5的特性结合在一起才能发挥出最好的功效,有没有本身就和谐统一的形式来做这个事情呢,其实是有的,坐稳了,我们要加速了。进入到声明式的世界,打开函数式编程的大门吧。

说起函数式编程(Functional Program),其实也是一项法则,入门的话,你们只需要记住下面几个公理和几个概念就可以了。类比小说《三体》的黑暗森林法则就是这么解释的。

  • 函数是一等公民(function first),FP的世界只有函数
  • 函数自身都有可组合的特性(composition),但函数本身是纯净(pure)的

想要继续了解函数式编程,还有两个很重要的概念,柯里化(curry)和PointFree

针对JS函数式编程有一个很重要的工具库,对,它不是lodash而是Ramda,使用Ramda来做一下上面的题吧。

// 打开 http://ramda.cn/docs/ 在控制台粘贴下面代码

const data = [{
    key: 'name',
    value: '豆皮范er'
}, {
    key: 'age',
    value: 1,
}, {
    key: 'from',
    value: '数据平台'
}]
const processFn = R.reduce(
    R.useWith(R.merge, [
        R.identity,
        R.converge(R.objOf, [
            R.prop('key'),
            R.prop('value')
        ])
    ]), {})
    
console.log(processFn(data))

乍一看代码,我大意了啊,没看懂,就像面壁者罗辑的咒语一样晦涩难懂和不可理喻对吧,但在某种程度上,即使只从表面来看,它又充满了和谐简洁的美,拥有高度的秩序排列和统一性,而且完全符合上面的两条公理两个重要概念,如果大家对什么是函数式编程,它主要有那些概念,具体的应用场景有哪些感兴趣的话,我会另写一篇详细的讲一讲函数式编程

The End


豆皮范儿
39 声望5 粉丝

爱编程的字节跳动数据平台前端妹子