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一样的错——那就吐吧反正这种科普贴只是我分享自己的入门心得,又不是给大牛看的吼吼……

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容