本文目录

1

概念与实现

估计很多同学对链式调用相当熟悉了,可以直接跳过第一节。

2

辅助函数

为了链式调用,让我们写代码时,多写几行this,问题倒是不大。但有的情况是,代码是别人写的,并且对方并没有考虑到所谓i的链式调用。

3

案例

这个辅助函数虽然很简单,但其应用相当广。

4

后记

1.概念与实现

可链式调用的英文是chainable,我们先看看非chainable是什么样的:

<script>
    var object = {
        doSomething1: function(){
            console.log('doSomething1...')
        },
        doSomething2: function(){
            console.log('doSomething2...')
        },
        doSomething3: function(){
            console.log('doSomething3...')
        }
    }
    object.doSomething1();
    object.doSomething2();
    object.doSomething3();
</script>

代码中的最后三行,每次调用都要重复写对象的名称object。

而chainable的代码:

object.doSomething1().doSomething2().doSomething3();

实现的原理,亦很简单,每个方法最后都返回 this:

<script>
    var object = {
        doSomething1: function(){
            console.log('doSomething1...')
            return this;
        },
        doSomething2: function(){
            console.log('doSomething2...')
            return this;
        },
        doSomething3: function(){
            console.log('doSomething3...')
            return this;
        }
    }
    object.doSomething1().doSomething2().doSomething3();
</script>

然而代码都写在一行,是不利于阅读和断点调试的,开发版本中可以格式化一下:

object
.doSomething1()
.doSomething2()
.doSomething3();

说到格式化,如果链的节点返回不是同一个对象,建议使用如下的格式化方式:

object1
.doSomething1()
.doSomething2()
.getObject2()
.doSomethingElse1();
.doSomethingElse2();

尤其是jquery的代码:

$('.container')
.find('p')
.eq(0)
.removeClass('red')
.css('color', 'blue');

上面的格式,因人而异吧,有人觉得好看,有人觉得难看。

2.辅助函数

为了链式调用,让我们写代码时,多写几行this,问题倒是不大。但有的情况是,代码是别人写的,并且对方并没有考虑到所谓i的链式调用。难道我们只有放弃使用这种优雅的方式了吗?解决方案是有的。

比如有如下的第三方代码:

function Person(name,age){
    this.name = name;
    this.age = age;
    this.count++;
}
Person.prototype = {
    sayName: function(){
        console.log(this.name);
    },
    sayAge: function(){
        console.log(this.age);
    },
    getCount: function(){
        return this.count;
    },
    count: 0
};
var p = new Person('xxx',18);
p.sayName();
p.sayAge();
var count = p.getCount();
console.log(count);

上面 Person 类明显是不支持链式调用的。我们希望的使用方式是:

var p = new Person('xxx', 18);
var count = p
.sayName()
.sayAge()
.getCount();
console.log(count);

要做到链式调用,需要该相应的方法返回this,比如上面的 sayName 方法。

不改变源代码的基础上,如何让它返回 this 呢?添加一层包裹函数就能做到,比如:

var old = Person.prototype.sayName;
Person.prototype.sayName = function() {
    var result = old.apply(this, arguments);
    return result === undefined ? this : result;
};

先执行原方法,根据其返回值,来判断该方法是否支持链式调用。原sayName是没有返回值的,因此新sayName变成支持链式调用的。

如果把sayName改成 getCount ,那么新的getCount跟原先做的事情是一样的,仍不支持链式调用的,这是我们想要的结果。接下来封装个辅助函数:

function chainablize(constructor) {
    var prototype = constructor.prototype;
    for(var method in prototype) {
        try {
            if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') {
                (function(method) {
                    var old = prototype[method];
                    prototype[method] = function() {
                        var result = old.apply(this, arguments);
                        return result === void 0 ? this : result;
                    };
                })(method);
            }
        } catch(e) {}
    }
}

其中使用了 try-catch ,是因为一些原生对象中的某些属性,使用 typeof 时会报错。

测试案例如下:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.count++;
}
Person.prototype = {
    sayName: function() {
        console.log(this.name);
    },
    sayAge: function() {
        console.log(this.age);
    },
    getCount: function() {
        return this.count;
    },
    count: 0
};

function chainablize(constructor) {
    var prototype = constructor.prototype;
    for(var method in prototype) {
        try {
            if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') {
                (function(method) {
                    var old = prototype[method];
                    prototype[method] = function() {
                        var result = old.apply(this, arguments);
                        return result === void 0 ? this : result;
                    };
                })(method);
            }
        } catch(e) {}
    }
}
chainablize(Person);
var p = new Person('xxx', 18);
var count = p
    .sayName()
    .sayAge()
    .getCount();
console.log(count);

现在考虑一个问题,如果Person实例是如下的方式使用呢?

var p = new Person('xxx', 18);
p.name = 'yyy';
p.age = 20;
p.sayName();
p.sayAge();

对象属性的赋值操作是很常见,如果赋值操作也能支持链式调用的就好了。所以有必要发明如下的api:

p.prop({
    name: 'yyy',
    age: 20
});

该api是我们主观添加进去的,第三方的代码可能不提供。应该长成如下的样子:

Person.prototype.prop = function(object) {
    for(var property in object) {
        this[property] = object[property];
    }
    return this;
}

然后我们把它也添加到 chainablize 函数里:

function chainablize(constructor) {
    var prototype = constructor.prototype;
    for(var method in prototype) {
        try {
            if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') {
                (function(method) {
                    var old = prototype[method];
                    prototype[method] = function() {
                        var result = old.apply(this, arguments);
                        return result === void 0 ? this : result;
                    };
                })(method);
            }
        } catch(e) {}
    }

    if('prop' in prototype) return;
    prototype.prop = function(object) {
        for(var property in object) {
            this[property] = object[property];
        }
        return this;
    }
}

最终测试案例如下:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.count++;
}
Person.prototype = {
    sayName: function() {
        console.log(this.name);
    },
    sayAge: function() {
        console.log(this.age);
    },
    getCount: function() {
        return this.count;
    },
    count: 0
};

function chainablize(constructor) {
    var prototype = constructor.prototype;
    for(var method in prototype) {
        try {
            if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') {
                (function(method) {
                    var old = prototype[method];
                    prototype[method] = function() {
                        var result = old.apply(this, arguments);
                        return result === void 0 ? this : result;
                    };
                })(method);
            }
        } catch(e) {}
    }

    if('prop' in prototype) return;
    prototype.prop = function(object) {
        for(var property in object) {
            this[property] = object[property];
        }
        return this;
    }
}

chainablize(Person);
var p = new Person('xxx', 18);
p
    .prop({
        name: 'yyy',
        age: 20
    })
    .sayName()
    .sayAge();

3.案例

这个辅助函数虽然很简单,但其应用相当广。

  • 3.1 实现canvas链式调用

非链式调用:

<canvas id="canvas" width="600" height="400"></canvas>
<script>
    // 代码来自于《canvas核心技术 图形、动画与游戏开发》一书的第2章
    var context = document.getElementById('canvas').getContext('2d');

    function drawGrid(context, color, stepx, stepy) {
        context.strokeStyle = color;
        context.lineWidth = 0.5;

        for(var i = stepx + 0.5; i < context.canvas.width; i += stepx) {
            context.beginPath();
            context.moveTo(i, 0);
            context.lineTo(i, context.canvas.height);
            context.stroke();
        }

        for(var i = stepy + 0.5; i < context.canvas.height; i += stepy) {
            context.beginPath();
            context.moveTo(0, i);
            context.lineTo(context.canvas.width, i);
            context.stroke();
        }
    }

    drawGrid(context, 'lightgray', 10, 10);
</script>
<style>
    body {
        background: #eeeeee;
    }
    
    #canvas {
        background: #ffffff;
        cursor: pointer;
        margin-left: 10px;
        margin-top: 10px;
        box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
    }
</style>

链式调用:

<canvas id="canvas" width="600" height="400"></canvas>
<script>
    // 辅助函数
    function chainablize(constructor) {
        var prototype = constructor.prototype;
        for(var method in prototype) {
            try {
                if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') {
                    (function(method) {
                        var old = prototype[method];
                        prototype[method] = function() {
                            var result = old.apply(this, arguments);
                            return result === void 0 ? this : result;
                        };
                    })(method);
                }
            } catch(e) {}
        }

        if('prop' in prototype) return;
        prototype.prop = function(object) {
            for(var property in object) {
                this[property] = object[property];
            }
            return this;
        }
    }
</script>
<script>
    // 使其canvas的context相关api可以链式调用
    chainablize(CanvasRenderingContext2D);

    var context = document.getElementById('canvas').getContext('2d');

    function drawGrid(context, color, stepx, stepy) {
        context.prop({
            strokeStyle: color,
            lineWidth: 0.5
        });

        for(var i = stepx + 0.5; i < context.canvas.width; i += stepx) {
            context
                .beginPath()
                .moveTo(i, 0)
                .lineTo(i, context.canvas.height)
                .stroke();
        }

        for(var i = stepy + 0.5; i < context.canvas.height; i += stepy) {
            context
                .beginPath()
                .moveTo(0, i)
                .lineTo(context.canvas.width, i)
                .stroke();
        }
    }

    drawGrid(context, 'lightgray', 10, 10);
</script>
<style>
    body {
        background: #eeeeee;
    }
    
    #canvas {
        background: #ffffff;
        cursor: pointer;
        margin-left: 10px;
        margin-top: 10px;
        box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
    }
</style>
  • 3.2 实现dom链式操作api

  • 3.2.1 非链式调用

    <script>
        var button = document.createElement('button');
        button.innerHTML = 'click me';
        button.setAttribute('data-msg', 'hello');
        button.addEventListener('click', function() {
            alert(this.getAttribute('data-msg'));
        });
        button.style.color = 'white';
        button.style.setProperty('background-color', 'blue');
        button.style.setProperty('margin', '100px 100px');
        document.body.appendChild(button);
    </script>

    3.2.2 链式调用

    <script>
        // 辅助函数
        function chainablize(constructor) {
            var prototype = constructor.prototype;
            for(var method in prototype) {
                try {
                    if(prototype.hasOwnProperty(method) && typeof prototype[method] == 'function') {
                        (function(method) {
                            var old = prototype[method];
                            prototype[method] = function() {
                                var result = old.apply(this, arguments);
                                return result === void 0 ? this : result;
                            };
                        })(method);
                    }
                } catch(e) {}
            }
    
            if('prop' in prototype) return;
            prototype.prop = function(object) {
                for(var property in object) {
                    this[property] = object[property];
                }
                return this;
            }
        }
        chainablize(CSSStyleDeclaration);
        chainablize(DOMTokenList);
        chainablize(Node);
        chainablize(Element);
    
        var button = document.createElement('button');
        button
            .prop({
                innerHTML: 'click me'
            })
            .setAttribute('data-msg', 'hello')
            .addEventListener('click', function() {
                alert(this.getAttribute('data-msg'));
            }, false);
        button.style
            .prop({
                color: 'white'
            })
            .setProperty('background-color', 'blue')
            .setProperty('margin', '100px 100px');
        document.body.appendChild(button);
    </script>

    4.后记

    其实本文的思想来源是一个库(chainvas)。库是有点老,不过作者是《css揭秘》的作者。可以把本文当成其源码分析,其源码没多少,不到200行,看完本文后,照着敲一边应该没问题了。话说 chainablize 函数确实修改了内置原型,毕竟为每个方法包裹了一层函数。但与一般的“猴子补丁”不一样,没有改变原有方法的行为,只是让其尽可能的返回this罢了。而新增prop确实是“猴子补丁”,可以删除。

 

posted on 2017-02-19 18:33  ZhanXie  阅读(477)  评论(0编辑  收藏  举报