JavaScript学习总结之Is this Bothering You?

写在前面:本文为JavaScript新手之作,大牛轻喷

图片盗自 https://www.youtube.com/watch?v=yVdU2coJ1VQ

要说当下编程社区里最火的语言是什么,如果JavaScript称第二,怕是没其他语言敢称第一。不过这几年JS实在发展得太快,而在ES5之后,好像除了统一的模块机制,我其实并没有觉得JS的发展给前端加上了什么非用不可的语言特性——嗯,是的我觉得前端不太用得上 async 和 await(什么?class?我没听过诶)。

不过JS仍然是一个很好玩的语言,和Lua一样,有着Scheme一样的风骨。所以窃以为,要理解一些JS的设计思路和编码习惯,还是要搞点函数式才行。网上有一套开源的书叫 You Don't Know JS,我之前小翻了前几本,觉得写得很不错。但是仔细想想它解释一些概念的说法,还是可能对命令式编程背景的读者不是那么友好。前两天组里有一个同事问我能不能对JS里的 this 给一个相对浅显的理解,我第一反应就是把 YDKJS 里让我印象深刻的说法丢出来:

this 相当于JS静态作用域里的一个动态变量

——呃,这个定义是挺准确的,但是真的好理解吗?

好吧,展开来写一篇我自己是怎么一步一步地理解JS里的 this 这个磨人的小妖精

静态作用域和动态作用域

变量作用域其实和所有编程语言都相关,但是函数式编程语言为了闭包的支持,对这个概念尤其看重。来一个简单的栗子——我们有如下两个函数一个变量和一次调用:

var v = 1;

function f1() {
    console.log(v);
}

function f2() {
    var v = 2;
    f1();
}

f2();

我们知道JS是一个静态作用域的语言,如果把上面代码贴到浏览器的 console 里执行一下,会输出 1。这说明 v 这个变量的绑定是在我们写 f1 的定义时就确定的,也可以说 v 的作用域是静态的,所以这样的设计叫静态作用域(也有人叫词法作用域)。但是如果JS是一个动态作用域的语言,v 的绑定就是在 f1 运行的时候确定的,在 f2 里这个值指向了上面那句 var v = 2,最终输出的结果就会是 2。这个问题实在太重要了,因为正是作用域在函数间的隔离,给了函数足够的抽象能力来实现各种对逻辑和数据的封装。

Now this is the main dish

好了,现在我们理解了静态和动态作用域以后,就可以贴点复杂一些但是更能说明问题的例子了——首先,假设我们的App有这么一个工具集:

var utils = {
    base: 0,
    accumulate: function(list, acc) {
        var result = this.base;
        for (var i of list) result = acc(result, i);
        return result;
    },
};

系不系很简单?accumulate 函数就是拿一列数字和一个 acc 函数,然后把这个函数对这列数字层层套上(如果脑海里飞过left fold/reduce这样的概念,请给自己一朵小红花)。现在假设我们的App就是一个用来把1、2、3、4加起来的简单计算器——

var app = {
    list: [1, 2, 3, 4],
    simpleSum: function() {
        var result = utils.accumulate(this.list, function(x, y) { return x + y });
        console.log(result);
    },
};

app.simpleSum();

是不是很明显会log一个10?现在我们来让逻辑复杂一些,求这几个数的平方和:

app.square = function(x) { return x * x };

app.sqrSum = function() {
    console.log(utils.accumulate(
        this.list,
        function(x, y) { return x + this.square(y) }  // <-- watch this
    ));
};

app.sqrSum();  // Hmm...

发现了吗,传递给 app.sqrSum 的函数里,this 指向的并不是 app。问题出在哪里呢?问题就在于,this函数里的作用域不是跟着定义走,而是在运行的时候被动态分发的(敲黑板!划重点!)。是不是看起来和动态作用域简直一毛一样?

所以我们怎么绕开这个坑呢?这就是在遥远的ES5时代及以前,大家经常用的一个办法:把 this 存成另一个变量,再把这个变量传进子函数里:

app.sqrSum = function() {
    var self = this;  // some may prefer using `that`
    console.log(utils.accumulate(
        this.list,
        function(x, y) { return x + self.square(y) }  // now self is referenced correctly
    ));
};

this 赋值给另一个变量再往里传是不是其实有点蛋疼?确实有点。JS社区里大家也这么觉得,所以才为此在ES6里搞了个箭头函数,在箭头函数定义里的 this 就是跟着词法作用域走的。这个特性在网上已经有太多讨论,一搜一大把,这里就不展开了。

更好的JS

The Better Parts

不管是箭头函数还是重新赋值,其实都不是我心中最好的解决方式——我其实更偏向于直接用函数,而不是用对象来封装数据和逻辑。学JS的朋友大多都读过或者听过 JavaScript the Good Parts 这本书的大名吧,作者 Douglas Crockford 在14年曾经谈过他觉得JavaScript里怎样做到更好的设计模式,这个Talk名字就叫 JavaScript the Better Parts。其中有一点我特别特别赞同:完全不要用this,因为……根本不需要!JS里的函数是比对象更全能的抽象方法。

再复用刚才的那个例子,我就不需要再特别解释,直接把用函数做抽象的代码贴上来一看就懂了:

var utils = (function utils() {
    var base = 0;
    function accumulate(list, func) {
        var result = base;
        for (var i of list) result = func(result, i);
        return result;
    }
    return { base: base, accumulate: accumulate };
})();

var app = (function app() {
    var list = [1, 2, 3, 4], square = function(x) { return x * x };
    return {
        simpleSum: function() {
            console.log(utils.accumulate(
                list,
                function(x, y) { return x + y }
            ));
        },
        sqrSum: function() {
            console.log(utils.accumulate(
                list,
                function(x, y) { return x + square(y) }
            ));
        },
    };
})();

是不是没有了 this 以后整个逻辑清晰不少?再也不用纠结指的是哪一层对象的字段了。而且用函数来做封装还有另外一个好处,不放在返回对象字段里的变量对外不可见,相当于一个私有变量。如果不习惯IIFE的写法,网上的资料也挺多的,搜一下不难,这里就略过不表咯。至于那些对象实例的 callbind 这些用法细节,在把 this 拿掉之后,也一样没有必要去深究了——是不是好棒棒?

但是残酷的现实是写一个网页App一般都会用个框架,框架里一般都会用上 this,实在是……(啊不,像我这样没有能力写一个框架的小菜鸡,是不会抱怨的)

P.S. 如果有大牛想吐槽我的API里犯了跟Lodash和Underscore一样的错——那就吐吧反正这种科普贴只是我分享自己的入门心得,又不是给大牛看的吼吼……

你可能感兴趣的:(JavaScript学习总结之Is this Bothering You?)