美文网首页
JavaScript中的this (FCC成都全栈大会贺老【th

JavaScript中的this (FCC成都全栈大会贺老【th

作者: Kaku_fe | 来源:发表于2019-11-26 17:07 被阅读0次

    前言

    成都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

    相关文章

      网友评论

          本文标题:JavaScript中的this (FCC成都全栈大会贺老【th

          本文链接:https://www.haomeiwen.com/subject/nrhywctx.html