7

我正在编写一个 JS webapp 客户端。用户可以编辑文本项目的列表/树(例如,待办事项列表或注释)。我经常用 jQuery 操作 DOM。

用户可以使用键盘上下导航列表(类似于 GMail 中的 J/K 键),并执行其他一些操作。其中许多操作具有镜像“向上”/“向下”功能,例如

$.fn.moveItemUp = function() {
    var prev = this.getPreviousItem();
    prev && this.insertBefore(prev);
    // there's a bit more code in here, but the idea is pretty simple,
    // i.e. move the item up if there's a previous item in the list
}

$.fn.moveItemDown = function() {
    var next = this.getNextItem();
    next && this.insertAfter(next);
    // ....
}

现在,这种具有两个几乎相同功能的模式在我的代码中的多个位置重复出现,因为项目列表/树上有许多非常对称的操作。

问题:我如何优雅地重构它以避免代码重复?

到目前为止,我想出的最简单的方法是使用 .apply() ...

$.fn.moveItem = function(direction) {
        var up = direction === 'up',
            sibling = up ? this.getPreviousItem() : this.getNextItem(), 
            func = up ? $.fn.insertBefore : $.fn.insertAfter;

        // ...

        if (! sibling) { return false; }

        func.apply(this, [ sibling ]);

        // ...
    };

当代码的其他元素的结构需要更改 moveUp/moveDown 时,好处是更容易维护。我已经需要多次稍微更改代码,并且我始终需要记住我需要在两个地方进行...

但我对“统一”版本不满意,因为:

  • 实现比简单的 moveUp/moveDown 更复杂,因此将来它可能不会那么容易维护。我喜欢简单的代码。所有这些参数,三元运算,每个应该“向上”和“向下”的函数中的 .apply() 技巧......
  • 不确定显式引用 jQuery.fn.* 函数是否安全(毕竟它们应该通过将它们应用于 jQuery 对象 $('...').* 来使用)

在使用 DOM 或类似结构时,您如何解决那些“几乎相同的代码”情况?

4

3 回答 3

4

您可以做的一件事是使用闭包来隐藏从用户传递的配置参数:

var mk_mover = function(direction){
     return function(){
         //stuff
     };
};

var move_up = mk_mover("up");
var move_down = mk_mover("down");

如果您想继续朝这个方向发展,基本上就变成了决定将参数传递给通用实现的最佳方式的问题,并且没有单一的最佳解决方案。


一个可能的方向是使用 OO 风格的方法,将配置“策略”对象传递给实现函数。

var mk_mover = function(args){
    var get_item = args.getItem,
        ins_next = args.insNext;
    return function(){
        var next = get_item.call(this);
        next && ins_next.call(this, next);
    };
 };

 var move_up = mk_mover({
     get_item : function(){ return this.getPrevious(); },
     get_next : function(x){ return this.insertBefore(x); }
 });

 var move_down = mk_mover({/**/});

我更喜欢在策略接口(不同方向的方法)较小且相对恒定的情况下执行此操作,并且当您希望打开将来添加新类型的方向的可能性时。

当我知道方向集和方法类型都不需要改变太多时,我也倾向于使用它,因为 JS 对 OO 的支持比对 switch 语句的支持更好。


另一个可能的方向是您使用的枚举方法:

var mk_mover = function(direction){
    return function(){
        var next = (
            direction === 'up' ? this.getNextItem() :
            direction === 'down' ? this.getPreviousItem() :
            null );

        if(next){
            if(direction === 'up'){
                this.insertAfter(next);
            }else if(direction === 'down'){
                this.insertBefore(next);
            }
        }

        //...
    };
}

有时您可以使用简洁的对象或数组而不是 switch 语句来使事情更漂亮:

var something = ({
    up : 17,
    down: 42
}[direction]);

虽然我不得不承认这有点笨拙,但它的好处是,如果您的方向集尽早确定,您现在可以灵活地在需要时添加新的 if-else 或 switch 语句,在自包含方式(无需在其他地方向策略对象添加一些方法......)


顺便说一句,我认为您建议的方法位于我建议的两个版本之间的一个不舒服的中间地带。

如果您所做的只是根据传递的值方向标记在函数内部进行切换,那么最好直接在需要的位置进行切换,而不必将事物重构为单独的函数,然后不得不做很多烦人的 .call和 .apply 的东西。

另一方面,如果您遇到了定义和分离函数的麻烦,您可以这样做,以便您直接从调用者(以策略对象的形式)接收它们,而不是自己手动进行调度。

于 2012-05-14T18:22:16.453 回答
2

我之前也遇到过类似的问题,我发现并没有一个很好的模式来处理反向执行看似相似的任务。

虽然作为人类的你认为它们相似是有意义的,但就编译器而言,你实际上只是在调用任意方法。唯一明显的重复是事物的调用顺序:

  1. 找到我们想在之前或之后插入的项目
  2. 检查项目是否存在
  3. 在它之前或之后插入当前项目

您已经以一种基本上进行这些检查的方式解决了问题。但是,您的解决方案有点难以理解。以下是我将如何解决这个问题:

$.fn.moveItem = function(dir){
  var index = dir == 'up' ? 0:1,
      item = this['getPreviousItem','getNextItem'][index]();
  if(item.length){
    this['insertBefore','insertAfter'][index].call(this,item);
  }
}

通过使用数组索引,我们可以比您的示例更容易地看到调用的内容。此外,apply 的使用是不必要的,因为您知道要传递多少个参数,因此 call 更适合该用例。

不确定这是否更好,但它更短一些(是否需要 return false ?)并且 IMO 更具可读性。

在我看来,集中动作比可读性更重要——如果你想优化、改变或附加到移动某物的动作,你只需要在一次地方做。您可以随时添加注释来解决可读性问题。

于 2012-05-14T18:05:12.137 回答
1

我认为单功能方法是最好的,但我会将它与上/下的东西分开。创建一个函数将一个项目移动到一个索引或另一个项目之前(我更喜欢后一个)并在那里处理所有“移动”事件,而不是依赖于方向。

function moveBefore(node) {
    this.parentNode.insertBefore(this, node); // node may be null, then it equals append()

    // do everything else you need to handle when moving items
}
function moveUp() {
    var prev = this.previousSibling;
    return !!prev && moveBefore.call(this, prev);
}
function moveDown() {
    var next = this.nextSibling;
    return !!next && moveBefore.call(this, next.nextSibling);
}
于 2012-05-14T18:11:00.103 回答