kmxz 发布的文章

Back in high school, I read a brilliant book, DOM Scripting (first edition) by Jeremy Keith. The Chinese translation of the book had a even better title: "JavaScript DOM 编程艺术", which literally means "the art of JavaScript DOM programming". The book was the first book I bought about JavaScript. It was so enlightening that it showed me how to write JavaScript in a "good" way (graceful degradation, in particular), and it eventually turned me into a frontend developer a decade later. If you ask to me to recommend 3 books for learning JavaScript, I'll definitely have DOM Scripting in the list.

Screenshot_2016-06-06_02-06-10.png

Last year, I saw a JavaScript function appeared in a question on SegmentFault, where the post author asked for some explanation on the code he posted. I wrote an answer:

The code you posted is a piece of shit. You can learn nothing from it. Actually, I've never seen any JavaScript code worse than that.

The post author replied: "I read the code from Keith's DOM Scripting". I was astonished. Recalling that DOM Scripting is such a masterpiece, I cannot believe it. After realizing the code was really extracted from the book, I felt embarrassed and woke up to the fact that JavaScript had really changed a lot over the years.

Screenshot_2016-06-06_02-03-54.png

Therefore, I made it a interview question for frontend developer candidates: read the very code section, and tell me how will you improve it. Only include basic code issues (including code style issues). Don't change the logic. Don't use fancy ES5/6 things. Don't say "requestAnimationFrame API" can help the performance. Just focus on the code itself. Here's the original code, again:

function moveElement(elementID,final_x,final_y,interval) {
    if (!document.getElementById) return false;
    if (!document.getElementById(elementID)) return false;
    var elem = document.getElementById(elementID);
    if (elem.movement) {
        clearTimeout(elem.movement);
    }
    if (!elem.style.left) {
        elem.style.left = "0px";
    }
    if (!elem.style.top) {
        elem.style.top = "0px";
    }
    var xpos = parseInt(elem.style.left);
    var ypos = parseInt(elem.style.top);
    var dist = 0;
    if (xpos == final_x && ypos == final_y) {
        return true;
    }
    if (xpos < final_x) {
        var dist = Math.ceil((final_x - xpos)/10);
        xpos = xpos + dist;
    }
    if (xpos > final_x) {
        var dist = Math.ceil((xpos - final_x)/10);
        xpos = xpos - dist;
    }
    if (ypos < final_y) {
        var dist = Math.ceil((final_y - ypos)/10);
        ypos = ypos + dist;
    }
    if (ypos > final_y) {
        var dist = Math.ceil((ypos - final_y)/10);
        ypos = ypos - dist;
    }
    elem.style.left = xpos + "px";
    elem.style.top = ypos + "px";
    var repeat = "moveElement('"+elementID+"',"+final_x+","+final_y+","+interval+")";
    elem.movement = setTimeout(repeat,interval);
}

Here are some possible answers I'll give, listed in the order they appeared:

  • Use var moveElement = function (...) syntax instead of function moveElement(...) to make the program more consistent (okay, it's just my personal preference).
  • Leave spaces after commas in argument list (i.e. moveElement(elementID, final_x, final_y, interval) instead of moveElement(elementID,final_x,final_y,interval)). Similarly, leave spaces around operators like /.
  • Either use camel case (better) or underscore case. Don't mix them up (elementID and final_x).
  • Always put curly brackets around the body of if, even when there is only one statement inside (return false in this example).
  • Always have consistent return type (don't return false at the beginning, but returning nothing at the end).
  • Don't declare variables using var inside if blocks. JavaScript's var keyword only gives function-level locality.
  • Don't use var to declare the same variable more than one time (there are var dist for multiple times in the code).
  • The is no point in assigning zero to dist at the beginning.
  • It's a very bad practice to pass a string as the argument to setTimeout. Pass a function instead.
  • Using == to compare is discouraged. Use === instead.
  • Some code quality tools (JSLint or JSHint) will ask you to supply the second argument (10) to parseInt.
  • Don't call to getElementById each time moveElement is called. Call getElementById just once, and use the element itself in recurring calls.
  • Similarly, don't call parseInt again and again. Put numeric values in variables instead.

Here's the revised code I'll have:

var moveElement = function (elementID, finalX, finalY, interval) {
    if (!document.getElementById) { return; }
    if (!document.getElementById(elementID)) { return; }
    var elem = document.getElementById(elementID);
    if (elem.movement) {
        clearTimeout(elem.movement);
    }
    var xpos = 0;
    var ypos = 0;
    var run = function () {
        var dist;
        if (xpos === finalX && ypos === finalY) {
            return;
        }
        if (xpos < finalX) {
            dist = Math.ceil((finalX - xpos) / 10);
            xpos = xpos + dist;
        }
        if (xpos > finalX) {
            dist = Math.ceil((xpos - finalX) / 10);
            xpos = xpos - dist;
        }
        if (ypos < finalY) {
            dist = Math.ceil((finalY - ypos) / 10);
            ypos = ypos + dist;
        }
        if (ypos > finalY) {
            dist = Math.ceil((ypos - finalY) / 10);
            ypos = ypos - dist;
        }
        elem.style.left = xpos + "px";
        elem.style.top = ypos + "px";
        elem.movement = setTimeout(run, interval);
    };
    run();
};

What will the frontend developer candidates answer? Maybe they're used to shims, polyfills, transpilers. Will they know the hard years we had with IE 6?

最近意识到很多 office politics 的事情。其实敝司挺多 office politics 的纷争,只是我恰巧都不用涉身其中。

所以说码农是幸运的,因为有自己的一亩三分地。自己的 code base 不容别人轻易更改,自己的 code style 可以保持 consistent。自己只要保证向上面按时交纳公粮,上面就不会找你麻烦,不会干涉你的具体工作。

而因为有这自己的一亩三分地产粮,如果运气不太差的话,也几乎不须去参与那些毫无意义的政治斗争,不须溜须拍马,唯唯诺诺;不须讨好谁,也不会得罪谁。

而产品经理乃至产品设计师就很不幸了。没有自己的一亩三分地,却必须要做出事情来给老板看,就不得不去别人的田地上收粮食,难免要淌浑水。尤其在公司威望不高的产品经理,简直惨不忍睹。近来公司里的各种冲突,大多源自于此。

所以公司找了两个 Android 程序员来给我打下手帮忙,我其实感到有负担。很多时候,他们写出来的代码我不满意,修改起来总是费力,但又不愿去和他自己解释(因为懒且怕尴尬)。而分配工作给他们,也常常成为让我头疼的事情。大约,我是在不喜欢有半点 management 性质的东西。大约,在我眼里,这样下来,已经不是完完全全的一亩三分地了。

可是,尘世里,又哪里去找真正一个人就能完成的工作呢?

前些天公司问我,入职一年要不要什么东西,「比如电脑啥的」。我正好心水 Dell Precision M4800 一阵子了,于是就打算买一台。

奈何考虑到 M4800 型号实在太老,只好考虑升级款 Precision 7510——虽然外观上不那么好看,不那么工程师了,但还算是勉强保留了一些移动工作站应有的硬朗。功能强大,对得起 Precision 这个名头。

经销商在出厂配置基础上进行了提升,换上了 32G 的 DDR4 内存,以及 256G SSD + 1T HDD 的硬盘。最终价格应该在 16500 和 19500 之间,具体没有问公司。

由于配置水平不错,终于不用再用 Xfce 了。拿到机器后先装的 Ubuntu GNOME。然而发现存在一些棱角——大约是 Ubuntu GNOME 还只是 Ubuntu 各 flavor 中较小众的一个;而且 Canonical 实在魔改了很多 GNOME 基础库里的东西。

于是我直接尝试了 Debian unstable (stretch),加 GNOME Shell。工作起来非常舒服。机器性能确实足以应付日常工作——同时开着两个虚拟机、几个 IDE、几十个浏览器标签页的情况下,依然可以秒开 GIMP。

Screenshot-from-2016-05-28-16-05-25.jpg

有了舒服的大显示器、同事送的键盘、新的电脑,有两个喝咖啡的杯子,有喜欢的各种书,还有漂亮的单车备件,办公桌简直完美。

IMG_20160528_171450780_HDR.jpg

不知道是出厂就有问题,还是经销商自己改装的时候弄出的问题,从一拿到机器,键盘中间的 touch stick 就无法使用。不过 Precision 系列送了三年内下一工作日免费上门服务。于是发邮件提 ticket,很快收到回复。第二天小哥就上门维修,花五分钟时间拿出新的键盘总成换上,解决了问题。

Screenshot from 2016-05-30 17-28-56.png

Time for productivity burst!

我想要优化每一个图片的大小,精准地选择文件格式和参数。
我想要每一个像素都对齐,每一个按钮都有合适的 ripple 效果。
但是整个公司根本没有一个人在乎。
所有人只在乎功能更快做完上线。

Android 开发中,用得最常见的 interface 之一便是 OnClickListener 了。不使用 android:onClick 属性的话,自然就不得不去 implement OnClickListener

假设我们不使用任何框架或是架构模式,只是写一个简单的 Activity;在一个 Activity 中有多个可点击的按钮时,我的同事会这么写:

public class ExampleActivity extends Activity implements OnClickListener {

    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_example);

        findViewById(R.id.first_button).setOnClickListener(this);
        findViewById(R.id.second_button).setOnClickListener(this);
    }

    @Override
    public void onClick(final View v) {
        switch (v.getId()) {
            case R.id.first_button:
                // bla bla bla
                break;
            case R.id.second_button:
                // bra bra bra
        }
    }
    
}

事实上,Android 官方有些 sample 里面也是这么写的。然而在我看来,这么写代码是非常不优雅的,因为一个 OnClickListener 的实现,只应该关注和点击事件本身相关的内容,它的含义和 Activity 的含义是截然无关的,让同一个类继承/实现他们,会使得这个类的含义变得不清晰。同时,这样还给 ExampleActivity 类增加了一个 public 的方法,削弱了这个类的封闭性。

所以如果像下面这样,会好不少:

public class ExampleActivity extends Activity {

    private OnClickListener onClickListener = new OnClickListener() {
        @Override
        public void onClick(final View v) {
            switch (v.getId()) {
                case R.id.first_button:
                    // bla bla bla
                    break;
                case R.id.second_button:
                    // bra bra bra
            }
        }
    };

    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_example);

        findViewById(R.id.first_button).setOnClickListener(onClickListener);
        findViewById(R.id.second_button).setOnClickListener(onClickListener);
    }
    
}

这样写体现了 composition over inheritance 的思想。它避免了上面的所有问题,看起来舒服得多。

不过,这样还是让阅读代码时很不方便——看到 onCreate 里面时,还不得不经常滚动到声明 onClickListener 的地方去,并且在 onClick 中艰难地寻找真正和某个特定按钮相关的代码。当然这两个问题之前那个版本也都无法避免。

另一件糟糕的事情是,不同按钮的 listener 逻辑很可能是相对独立的,放到同一个 onClickListener 里,还是很丑陋。

所以为了进一步避免这几个问题,我一向都是用下面这样的写法:

public class ExampleActivity extends Activity {

    @Override
    public void onCreate(final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_example);

        findViewById(R.id.first_button).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                // bla bla bla
            }
        });
        findViewById(R.id.second_button).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(final View v) {
                // bra bra bra
            }
        });
    }
    
}

这样的话,不同逻辑间相对独立,看起来非常舒服,便于阅读,并且让我找回了写 JavaScript 的舒畅感觉(划掉)。

那么问题来了:为什么有的人不使用最后一种写法呢?

此问题已被提交至 https://segmentfault.com/q/1010000005337426