V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
learning
V2EX  ›  JavaScript

Beautiful Mixins -《Beautiful JavaScript》读书分享

  •  
  •   learning · 2016-08-26 11:51:35 +08:00 · 2322 次点击
    这是一个创建于 2800 天前的主题,其中的信息可能已经有所发展或是发生改变。

    刚开始,代码只是代码而已。当代码多了,我们发明了函数,以便代码可以重用。到后来,我们的函数也越来越多,所以我们又发明了别的一些编程概念。但是当我们沉迷于这些新概念的同时,我们可能就错过了一些近在眼前的美丽事物……

    类继承

    类概念主要存在于面向对象编程中的,可能大家上学时都学过,比如 Java 、 C#、 Objective-C 等面向对象语言。每个实体都需要先要有一个类,这个类对于我们来说是比较抽象的(不是说 abstract class 哦),然后子类还可以继承父类。

    这感觉似乎有点亚里士多德形而上学和分类法的味道,但简单来说这就是面向对象的基础,在写出实际代码之前,你得先写出一些类、接口,然后类继承类、类实现接口……

    实际上我们人类并不是非常擅长分类,打个比方,一个按钮( Button ),它应该是一个矩形( Rectangle ),还是一个控件( Control )呢?我们可以让 Button 继承 Rectangle ,让 Rectangle 继承 Control ……等等,是不是有什么不对?

    所以,面向对象的概念可能会在项目刚开始时就把你压垮,但这并不是说面向对象就不好,只是有时候并不适合你的项目。

    原型链继承

    可能大家都知道, JavaScript 是不支持class关键字的,因为 JavaScript 本身就不作为一个面向对象语言来设计,但并不妨碍大家想出各种招来实现class

    JavaScript 是基于原型链(Prototype)继承的,但是其诡异的写法可能让好多初学者望而却步。

    function Circle() {
      this.radius = 7;
    }
    Circle.prototype = {
      area: function () {
        return Math.PI * this.radius * this.radius;
      },
      grow: function () {
        this.radius++;
      },
      shrink: function () {
        this.radius--;
      }
    };
    

    在 ES5 之后,我们有了Object.create方法,好理解了一些。

    var circle = Object.create({
      area: function () {
        return Math.PI * this.radius * this.radius;
      },
      grow: function () {
        this.radius++;
      },
      shrink: function () {
        this.radius--;
      }
    }, {
      radius: {
        writable: true,
        configurable: true,
        value: 7
      }
    });
    

    只是第二参数用起来有点复杂,其实就是跟Object.defineProperties()的参数一样,但是我们现在又有了 ES6 呀:

    class Cicle {
      constructor() {
        this.radius = 7;
      }
      area() {
        return Math.PI * this.radius * this.radius;
      }
      grow() {
        this.radius++;
      }
      shrink() {
        this.radius--;
      }
    }
    

    这都是一些使用 JavaScript 实现class的例子,实际上, JavaScript 还可以有另外一种重用代码的方式—— Mixins 。

    Mixins

    这就是我们今天的主题,近在眼前的美丽事物,但可不是什么新概念,用过 Ruby 或 Python 的同学,可能有所耳目, Mixins 的字面意思就是把东西掺合在一起。

    在 JavaScript 我们有callapply,很容易切换上下文,将各种互不相干糅合,达到 Mixins 的目的。

    假设我们要做一个圆形按钮(RoundButton),它有两个特性:

    1. 是圆的
    2. 可点击

    我们可以把这两个特性分别写做 2 个函数:

    // 是圆的
    var withCircle = function () {
      this.area = function () {
        return Math.PI * this.radius * this.radius;
      };
      this.grow = function () {
        this.radius++;
      };
      this.shrink = function () {
        this.radius--;
      }
    }
    
    // 可点击
    var withClickable = function () {
      this.hover = function () {
        console.log('hovering');
      };
      this.press = function () {
        console.log('button pressed');
      };
      this.release = function () {
        console.log('button released');
      };
      this.fire = function () {
        this.action.fire();
      };
    }
    

    这是我们的圆形按钮:

    var RoundButton = function(radius, label, action) {
      this.radius = radius;
      this.label = label;
      this.action = action;
    };
    

    现在我们要让给这个圆形按钮附上那两个特性:

    withCircle.call(RoundButton.prototype);
    withClickable.call(RoundButton.prototype);
    
    var button = new RoundButton(4, 'yes!', function() {
      return 'you said yes!'
    });
    button1.fire(); // 输出 'you said yes!'
    

    这样写,是不是瞬间显得既简洁又自然?让人一眼看懂代码在做什么。

    当然这些附加特性的函数用的多了,也就创建了许多函数,这里可以简单的用一个立即执行函数(Immediately Invoked Function Expression)的闭包来对其进行优化一下,以withCircle为例:

    var withCircle = (function () {
      function area() {
        return Math.PI * this.radius * this.radius;
      }
      function grow() {
        this.radius++;
      }
      function shrink() {
        this.radius--;
      }
      return function () {
        this.area = area;
        this.grow = grow;
        this.shrink = shrink;
      };
    })();
    

    这样就不需要每次使用都新建函数了,从而节省更多的资源。

    Advice

    有时候,你无法确保某些函数可能会覆盖原有的功能,例如以下例子:

    Button.prototype.press = function() {
      console.log('pressed');
    };
    
    // 这时再用我们的 withClickable ,就会覆盖掉 press
    withClickable.call(Button.prototype);
    

    这时候我们应该采用 Advice , Twitter 的Flight框架已经提供了此功能:

    var withClickable = function () {
      this.after('press', function () {
        console.log('press again.');
      });
    };
    
    withAdvice.call(Button.prototype);
    withClickable.call(Button.prototype);
    
    var button = new Button();
    button.press(); // 输出 'pressed', 'press again.'
    

    两个press并不会互相冲突,而是有先后顺序的执行,就类似通过addEventListener添加了多个事件而不是直接修改onclick一样,具体细节可以参考 Flight 的 API 。

    小结

    作为一名程序员,我们或许在上学时就被灌输了面向对象的固有思想,毕竟面向对象从上世纪 90 年代到现在,经久不衰,自由它的优势。但是在 JavaScript 中,如果你并不善于面向对象的抽象思维,何不尝试一下 Mixins 呢?而且 Mixins 与类继承相比,还能更好的解耦合,可以用于任何 Object 之上,正好用上了 JavaScript 若类型的优势。

    最近在读《 Beautiful JavaScript 》这本书,有一些好的内容,正好可以跟大家分享,但并不是全部,有兴趣的同学也可以自己读一下,请支持正版。

    原文链接: http://t.cn/RteECIF 微信号:程序员晋级之路『 code-learning 』

    clipboard.png

    1 条回复    2016-08-26 14:46:43 +08:00
    zhuangzhuang1988
        1
    zhuangzhuang1988  
       2016-08-26 14:46:43 +08:00
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   904 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 21:56 · PVG 05:56 · LAX 14:56 · JFK 17:56
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.