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

推荐阅读更多精彩内容