JavaScript的对象——灵活与危险
没有哪种数据结构比JavaScript的对象更简单灵活了。作为一个弱动态类型语言,JavaScript对对象的属性没有任何约束, 这带来的结果就是,在使用的时候很爽,想加啥属性直接加上去就行了,根本没有类或模板的限制, 想读啥属性直接“点”出来就行了,写出来那是相当简洁;然而这样的代码在运行的时候呢……
JavaScript这种灵活性最大的一个问题也是没有约束。比如一个网店系统有两个部分,一部分产生订单对象, 另一部分拿到订单对象来展示。咱们前端程序员自然是干后面展示那部分事儿的,比如要在页面上展示个订单里面商品的价格, 就会掉order.product.price.sum。然而写产生订单哪部分代码的哥们儿不一定靠谱, 订单里有个赠品就是没价格,price字段干脆没有,前端还是调order.product.price.sum,得,js代码执行到这就算死了, 后面啥也出不来了。更可怕的是现在很多系统引入了node,把前后端分离界限往后移了一截, 这回在node上调用order.product.price.sum,得,整个页面都出不来了。
是不是觉得这个例子挺low的?估计你们公司已经规定好了订单数据里的产品一定会有price字段,哪怕真没价格后端大哥也会给你传个{}。 况且人家后端用的是java,强静态类型语言,各种属性早就定义好了,咋能丢呢? 再说上线前要测试那么长时间,怎么会在线上出这种问题呢?
现在JSON已经是横飞于网络之间的标准数据结构,即便你们系统后端是用java写的,前端调用这个写后端接口时得到的还是JSON。 可是这个JSON是从啥转过来的呢?你指望一定是某个类的对象吗?够呛。后端大哥也许早发现了Java太死板, 为了贴合灵活的JSON,人家早用map了。再说,即便后端老老实实的用class,如果你们的网店系统很大, 有好几个店不同种类的订单,这些订单恐怕不是出自于同一个class吧,字段难免有差别,你能保证你们开发团队文档完整到你能掌握所有类的属性吗?
反正我们没有这么全的文档,即使有时候有点文档,后端大哥也会善意的告诉我:以代码为准!
如果想在上线前把这种缺失字段的bug全都测出来,我觉得太难为QA兄弟了,后端的数据又不是他们说了算, 保不齐上线很长时间后突然真给少点啥。
人总是不靠谱的,还得从代码上想办法。
最直接的办法是小心翼翼步步为营:在每调用一个有风险的属性的时候都判断一下其父节点是否存在。 这判断谨慎到什么程度就要估计一下数据来源的靠谱程度了。对于前面商品总价的例子,一个订单里总不会连商品都没有吧, 所以代码可以是这个样子:
var showPrice
if(order.product.price)
showPrice = order.product.price
实际情况往往比较复杂,比如我现在要取的数据是订单中一款产品的生产厂家的联系方式里面若干电话中的第一个, 而且我不太信任后端接口,连订单中有没有产品都不敢保证,那么……
var temp, temp1, temp2, temp3, phone;
if ((temp = order.product) != null) {
if ((temp1 = temp.seller) != null) {
if ((temp2 = temp1.contact) != null) {
if ((temp3 = temp2.phone) != null) {
phone = temp3[0];
}
}
}
}
如此拙劣!如果JS要这么写我宁愿回头做后端去写Java!
并没有~~ ヾ(¯∇ ̄๑)
我用coffeescript。其实我们现在所做的项目没有构建与coffee之上的,但是我用coffee,怎么用是另一个话题, 反正用了coffee,这段代码就是这样:
phone = order.product?.seller?.contact?.phone?[0]
遗憾的是,我是个多少有些强迫症的人,每当看到那么多问号就会联想到编译后的那一堆if,我都替电脑累。 再说这问号很容易遗漏,悄悄地少个问号很难发现。总之我只会在我自认为有可能缺失的属性前面加个问号。
其实做这些判断要做的无非就是一旦调用链上某个属性缺失就直接返回undefined。js本身不提供这玩意儿,自己封装一个。
Object.prototype.getAttr = function(path){
attrLink = path.split('.');
var ref = this[attrLink[0]];
for(var i=1; i < attrLink.length; i++){
if(ref)
ref = ref[attrLink[i]];
else
return undefined;
}
return ref;
}
于是访问属性就成了这样:
order.getAttr('product.seller.contact.phone.0')
这样就安全了。可是调试的时候我宁愿让解释器用红色的异常告诉我到底是谁没定义或者是空也不想悄咪咪的就得到一个未定义值。 办法是有的,上面的代码在ref为空的时候输出个日志就行了。最后的问题是getAttr方法并非是获取属性的强制方法, 它不像Java,只要把属性私有了就只能用getter。别说保证其它小伙伴一定要用这个方法取属性,就连我自己可能都写着写着就直接点上了。 最终,这个Object.getAttr的办法我在实际开发中从未用过。
感觉上前面那种在cofee里面加问号的办法也就够了,如果我们自己都觉得这个属性必须得有而后端接口就是没传那也太那个啥了。 但真实情况是一切皆有可能,这也不能怪后端大哥,没准这订单数据是从其他系统发送过来的或者是很古老的奇葩数据。
咋闹?我的原则是尽可能把损失降到最低,也就是把异常影响的代码范围缩到最小。这就要靠try..catch。
在哪儿try要看代码的情况,总不可能逮哪try哪。不过还是有些规律的。
那些不靠谱的数据应该不是我们自己写的,而是来源于外部接口,对这个接口数据利用的代码肯定是有一定范围的, 最起码的情况是把这一段给try了。这还太粗糙。在看看数据的特征,大量外部数据往往是可迭代的, 比如会显示一个订单列表,对这个订单列表肯定会有个循环,那么针对循环内部try一下,这样顶多是一个订单无法显示。 还有局部数据有没有统一处理的地方,比如我做的node项目使用handlebars模板,模板会用到很多自定义的helper, 这些helper大多数是用于把数据处理成显示的结果,比如格式化价格什么的。比较巧的是这些helper都有一个统一的代码入口:
helpers = _.extend(
require('./helper1'),
require('./helper2'),
require('./helper3')
)
// “_”是underscore或者lodash
其实也就是把各个文件中的helpers整合成了一个大对象,然后:
module.exports = _.reduce(helpers, function(memo, f, k){
memo[k] = function(){
try {
return f.apply(this, arguments)
} catch (e) {
console.error('handlebars helper error', e)
return ''
}
}
return memo
}, {})
在export前把所有helper函数都包裹在一个try...catch中。这样在一个helper中有字段缺失的错误也就会引起一小点东西显示不出来。
其实我把所有的helper集中到一个大对象中其初衷并非是为了这个,而是由于把每一个helper文件都注册一遍看起来重复太多,觉得不爽。 不是有一个著名的代码维护原则大概是说“任何重复的代码都应该避免”吗?这就看出来好处了。
好像看起来完美了,也不然。后来一哥们儿又写了个helper,没放到我的这些helpers里面,而是在别的文件中单独注册去了, 结果奇葩的数据真的出现了,悲剧真的防不胜防地上演了。。。
好吧,说到底想尽各种奇技淫巧也抵不过不按规矩出牌。其实要真的前后端所有同志把数据格式规定好了,并且严格执行, 我写的这一大堆全是废话。