JavaScript中的this (FCC成都全栈大会贺老【this in js】主题记录)

前言

成都FCC全栈大会贺老分享“This In JS”主题记录,我们知道,在Javascript中this和其他语言的this并不相同,我们在不同环境,不同语法,不同调用模式下,this可能总会出现各种奇怪的指向。

正文

function f() { this }
function f() { 'use strict'; this }
const f = () => this

我们先看看这样一个很简单例子,我相信这个例子大家初学JS或者面试的时候都用经历过,我们在不同的语法环境下,不同的模式下,this可能都是不同的值。
同时,this的取值也取决于代码调用方式,如

f()
obj.f()
new f()

是函数调用?对象属性调用?或者构造器的形式调用?同时JS也有独特的方法来改变this的指向,如我们常用的 call apply bind

f.call(obj)
f.call(null)
f.call(42)
f.call()

可能大家没想过,如果call中传入了null,传入了一个数字,那么this又是什么,同时,这些又和上文如是否是严格模式有关系,不同模式下this也都会发生变化。

我们来看一个例子

<form id=myForm>
    <input type="button" name="beijing" value="北京">
    <input type="button" name="shanghai" value="上海">
    <input type="button" name="chengdu" value="成都">
</form>

<script>
class Greeting {
    static from(control) { return new Greeting(control.value) }
    constructor(name) { this.name = name }
    hello() { console.log(`Hello ${this.name || 'world'}!`) }
}
[...myForm.elements]
    .map(e => [e, Greeting.from(e)])
    .forEach(([e, {hello}]) => e.addEventListener('click', hello))
</script>

这样一段代码,大家可以想想,当点击成都的时候,会打印什么话

······

答案是打印Hello chengdu,当调用click时,this其实指向的是element,也就是说,最后打印的name其实是元素上的name属性。

这么一看似乎问题不大,因为问题能很快定位,当我们把代码改成这样呢

<form id=myForm>
    <input type="button" name="beijing" value="beijing">
    <input type="button" name="shanghai" value="shanghai">
    <input type="button" name="chengdu" value="chengdu">
</form>

<script>
class Greeting {
    static from(control) { return new Greeting(control.value) }
    constructor(name) { this.name = name }
    hello() { console.log(`Hello ${this.name || 'world'}!`) }
}
[...myForm.elements]
    .map(e => [e, Greeting.from(e)])
    .forEach(([e, {hello}]) => e.addEventListener('click', hello))
</script>

我们的执行逻辑想打印input上的value,打印出来发现,没错,打印了Hello chengdu,其实这段代码对比上文就是有Bug的,当我们下次维护时,出了bug也能难以去定位问题。

我们在来看看Promise中的this。

Promise.all(numberPromises)
    .then(values => {
        const nonNumbers = values.filter(isNaN)
        return Promise.all(values.concat(nonNumbers.map(Promise.reject)))
    })
    .then(nextStep)
    .catch(errorLogger)

这是一段写法比较奇怪的代码,讲的是,我们需要筛选出不是数字的值,如果存在非组织,那么就会进入catch中,调用error,当我们在errorLogger打印信息时,我们应该是希望打印不是数字的值,可实际上,我们到catch后,发现打印的是

PromiseReject called on non-object

为什么呢?我们都认为Promise.reject是一个静态函数,其实在标准Promise定义中他是需要this的,因为Promise定义中,他是可以被子类化的,当我们调用Promise.reject时,他会看当前this指向的是什么类,如果是Promise子类的话,他便会创建一个Promise子类的实例。我想,对于大多数人而言,应该都不清楚Promise.reject需要一个this吧

this的问题

即便学会了也可能会出问题,他具体表现在:

  1. 容易挖坑
  2. 可能隐藏
  3. 难以定位

ES6解决方案

在JS中,我们用到this的地方主要有:

  1. 普通函数
  2. 回调函数
  3. 构造器
  4. 方法

在ES6中,我们提供了class,来解决构造器this的问题,提供了allow function来解决,那其实我们对this判断有可能失败的情况,那么就还有普通函数以及方法。正常情况下,我们自己的代码还是可以分辨出this指向,但是对于第三方库以及框架,this到底指向什么就难以区分
贺老准备在下一次T39会议提出,通过语言的层面解决this指向不清的问题

Outdated draft: gilbert/es-explicit-this

显示this

我们知道,在函数中,this其实是作为一个隐藏的变量来提供给开发者使用,那么我们是否可以把this显示出来呢,在TS中,我们可以使用这样的代码

Number.prototype.toHex = function (this: number) {
    return this.toString(16)
}

显示的告诉this是一个number类型的代码,那么我们可以借鉴显示this的方式,构建出如下的代码。

function getX(this) { // 显示 this
    return this.x
}
function getX(this o) { // 别名
    return o.x
}
function getX(this {x}) { // 解构
    return x
}

这样写this,能带来什么好处呢?

示例1

// original code
class Player {
    attack(opponent) {
        return Game.calculateResult(
            this.input(),
            opponent.input(),
        )
    }
}

// better naming
class Player {
    attack(this offense, defense) {
        return Game.calculateResult(
            offense.input(),
            defense.input(),
        )
    }
}

通过别名,我们可以增强代码的可读性

示例二

// original code
function process (name) {
    this.taskName = name;
    const that = this
    doAsync(function (amount) {
        this.x += amount;
        that.emit('change', this)
    });
};


// better naming
function process (this obj, name) {
    obj.taskName = name;
    doAsync(function callback (this result, amount) {
        result.amount += 2;
        obj.emit('change', result)
    });
};

我们知道,同名变量后者会覆盖前者,this也不例外,该例子我们可以通过显示this的方式,保证this的是你想要的,而不用另外定义变量

示例三

function div(@int32 this numerator, @int32 denominator) {
    // if (numerator !== numerator|0) throw new TypeError()
    // if (denominator !== denominator|0) throw new TypeError()
    // ...
}

使用了显示的this后,我们也可以使用decorator封装一些公共逻辑处理this。
主要
这一份提案主要解决了this指向容易混淆的问题。这里,贺老也提出了一份另外的提案,用于提前发现this指向错误的问题。

Outdated draft: hax/proposal-function-this

增加一个具体属性 thisArgumentExpected,我们可以使用这个属性,提前发现我们this是否出现错误,如,我们定义一个APIon

// safer API:
function on(eventTarget, eventType, listener, options) {
    if (listener.thisArgumentExpected) throw new TypeError('listener should not expect this argument')
    return eventTarget.addEventListener(eventType, listener, options)
}

当这个api发现thisArgumentExpected是true的话,就提前抛出错误,而不需要等待点击的时候才发现错误,越早检测到,对代码的稳定性也就越好。同时,针对不同情况thisArgumentExpected,thisArgumentExpected也会自动变化

// 箭头函数
let arrow = () => { this }
arrow.thisArgumentExpected // false

// 使用bind的函数
let bound = f1.bind()
bound.thisArgumentExpected // false

// 未使用bind的普通函数
function func() {}
func.thisArgumentExpected // false

// 使用bind的普通函数
function implicitThis() { this }
implicitThis.thisArgumentExpected // true

// 显示this的普通函数
function explicitThis(this) {}
explicitThis.thisArgumentExpected // true

// 对象
class C {
    m1() {}
    m2() { this }
    m3(this) {}
    m4() { super.foo }
    static m1() {}
    static m2() { this }
    static m3(this) {}
    static m4() { super.foo }
}
C.prototype.m1.thisArgumentExpected // false
C.prototype.m2.thisArgumentExpected // true
C.prototype.m3.thisArgumentExpected // true
C.prototype.m4.thisArgumentExpected // true
C.m1.thisArgumentExpected // false
C.m2.thisArgumentExpected // true
C.m3.thisArgumentExpected // true
C.m4.thisArgumentExpected // true
C.thisArgumentExpected // null

通过这样的形式,我们对对应API增加thisArgumentExpected检测,便可以提前知道我们的代码是否可能会有this指向错误的问题

总结

上面的介绍目前只是提案,并没有实际支持,是否得到通过还未可知。不过从介绍看,确实解决了this的一些问题,对于开发第三方库以及框架的同学而言是一份好消息,对于普通开发者,也能间接的提升开发体验(知道该库的this到底是什么)

本文提到的提案地址

Outdated draft: gilbert/es-explicit-this
Outdated draft: hax/proposal-function-this

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

推荐阅读更多精彩内容