1.15 减少隐式副作用
在JavaScript中有两类十分常见却可以轻易规避的错误。第一类是语法错误,第二类是无意识的隐式副作用。
隐式副作用是代码复用的大敌,因为它导致函数被其作用域外的状态所劫持。隐式副作用的产生是由于多个函数间共享变量或属性所致,举例来说,应用中有一个购物车功能,用户在会话期间可以对购物车的内容进行保存。
用户想要更改当前会话中购物车的内容顺序,只需:
test('Order WITH unintentional side effect.', function () {
var cartProto = {
items: [],
addItem: function addItem(item) {
this.items.push(item);
}
},
createCart = function (items) {
var cart = Object.create(cartProto);
cart.items = items;
return cart;
},
// Load cart with stored items.
savedCart = createCart(["apple", "pear", "orange"]),
session = {
get: function get() {
return this.cart;
},
// Grab the saved cart.
cart: createCart(savedCart.items)
};
// addItem gets triggered by an event handler somewhere:
session.cart.addItem('grapefruit');
ok(session.cart.items.indexOf('grapefruit')
!== -1, 'Passes: Session cart has grapefruit.');
ok(savedCart.items.indexOf('grapefruit') === -1,
'Fails: The stored cart is unchanged.');
});
很不幸,当用户在当前会话中添加或删除购物车内容时,之后的购物车设置信息也被连带删除了,导致这个问题的代码片段是:
createCart = function (items) {
var cart = Object.create(cartProto);
cart.items = items;
return cart;
},
请留意此处,cart.items是对原型对象上items属性的直接引用,现在对代码做略微修改。
cart.items = Object.create(items);
新的购物车拥有自己的内容副本,不会再对storedCart对象带来直接影响。
若想减少隐式副作用在程序中的出现概率,最好的方法就是在函数内部对它进行规避。所有外部变量传入函数之前,最好是先经过一轮复制,不要将原始值直接传入。
纯函数没有隐式副作用,因为它在调用时不会更改任何外部变量,决定它返回值的因素仅有一个,即它的入参。
尽可能地确保你的函数在执行过程中不会影响外界的状态,在执行结尾处返回修改后的变量副本而不是原始引用,请注意,在整个执行过程中你仍然有更改外部变量的机会。就像REST架构下客户端与服务器的数据传输一样:客户端首先从服务器获取一份数据资源的副本,修改其内容,再将处理过的副本发送回至服务器。建议归建议,大部分情况下开发者为了兼顾到应用的性能不会这么去做,不过使用纯函数或许可以起到一定效果。
在每个函数内部对隐式副作用做规避,一来可以减少代码冗余,二来可以帮助你提升程序分层设计的意识。举例来说,你有一个开工已达数月之久的项目,现在你需要对其新增数据校验功能,当项目中仅有一个函数能够对数据进行提交操作时,你只需把校验功能安插在此函数调用之前即可。但如果这样的函数在项目中有百来个,势必会影响校验功能的加入。
将不同的功能逻辑相互隔离,可以让你更好地管理应用状态。如果函数在执行过程中不会被外界的状态变更所干扰,它只需少量代码就可以完成手头的工作,因为眼前的任务只有一个。
同理,操作DOM的函数一定只专注于DOM树操作,比如视图的render()方法,又如一些DOM树插件。