换种方式理解 JavaScript 中的 this

10年服务1亿前端开发工程师

在这篇博文中,我将采取了一种不同的方式来解释 JavaScript 中的 this :我假设箭头函数是真正的函数,而普通函数是特殊结构的方法。我认为这样更容易理解 this – 试试看。

注:在没特殊说明的情况下,示例默认在 strict mode(严格模式) 下运行,即加上 'use strict'

1.两种类型的函数

在这篇文章中,我们关注两种不同类型的函数:

  • 普通函数:function () {}
  • 箭头函数:() => {}

1.1 普通函数

如下创建一个普通函数v:

function add(x, y) {
    return x + y;
}

每个普通函数都有隐含的参数 this,这个参数在被调用时总是被自动填充。换句话说,以下两个表达式是等价的(在严格模式下)。

add(3, 5)
add.call(undefined, 3, 5);

如果你嵌套普通的函数,this 会被覆盖:

function outer() {
    function inner() {
        console.log(this); // undefined
    }

    console.log(this); // 'outer'
    inner();
}
outer.call('outer');

inner() 里面, this 并不是指向 outer() 中的 this ,因为 inner() 里有它自己的 this 。这与变量在嵌套作用域中的工作方式类似:

const _this = 'outer';
console.log(_this); // 'outer'
{
    const _this = undefined;
    console.log(_this); // undefined
}

由于普通函数总是有隐含的参数 this ,所以对于普通函数来说更好的名字就是“方法”。

1.2 箭头函数

如下方式创建一个箭头函数(我正在使用 block(块) 语法 ,使其看起来有点类似于函数定义):

const add = (x, y) => {
    return x + y;
};

如果你在一个普通函数里面嵌套一个箭头函数,那么 this 函数不会被覆盖:

function outer() {
    const inner = () => {
        console.log(this); // 'outer'
    };
    console.log(this); // 'outer'
    inner();
}
outer.call('outer');

由于箭头功能的行为,我也偶尔称他们为“真正函数”。它们与大多数编程语言中的函数类似 – 比普通函数更重要。

请注意,箭头函数中的 this 不受 .call() ,或者其他任何方式的影响。箭头函数中的 this 总是由箭头函数创建时的作用域决定。例如:

function ordinary() {
    const arrow = () => this;
    console.log(arrow.call('goodbye')); // 'hello'
}
ordinary.call('hello');

1.3 作为方法的普通函数

如果一个普通函数是一个对象的属性值,它就成为一个方法:

注:一个普通函数总是作为一个方法被调用,如果它不被作为对象的属性调用,它将在全局对象的上下文中被隐式地调用(即使它不作为全局对象的属性存在)。

const obj = {
    prop: function () {}
};

访问对象属性的一种方法是通过点操作符(.)。该操作符有两种不同的模式:

  • 获取和设置属性:obj.prop
  • 调用方法:obj.prop(x, y)

后者相当于:

obj.prop.call(obj, x, y)

你可以再次看到,当调用普通函数时, this 总是被填充。

JavaScript 为定义方法提供了特别的,方便的语法:

const obj = {
    prop() {}
};

2.常见的陷阱

通过我们刚刚学到的东西,让我们来看看常见的陷阱。

2.1 陷阱:在 Promises 回调中访问 this

一旦异步函数 cleanupAsync() 完成,请考虑以下基于Promise的代码,在该代码中我们打印 “Done”。

// 在类或对象字面量中:
performCleanup() {
    cleanupAsync()
    .then(function () {
        this.logStatus('Done'); // (A)
    });
}

问题是(A)行中的 this.logStatus() 调用失败,因为 this 不是指向 .performCleanup() 的那个 this – 也就是回调中的 this 被覆盖。 换句话说:我们应该使用一个箭头函数,而不是一个普通函数。如果我们这样做,一切运作良好:

// 在类或对象字面量中:
performCleanup() {
    cleanupAsync()
    .then(() => {
        this.logStatus('Done');
    });
}

2.2 陷阱:在 .map() 回调中访问 this

同样,以下代码在(A)行中失败,因为回调会覆盖方法 .prefixNames()this

// 在类或对象字面量中:
prefixNames(names) {
    return names.map(function (name) {
        return this.company + ': ' + name; // (A)
    });
}

再次,我们可以通过使用箭头函数来修复它:

// 在类或对象字面量中:
prefixNames(names) {
    return names.map(
        name => this.company + ': ' + name);
}

2.3 陷阱:使用方法作为回调

以下是用于UI组件的类。

class UiComponent {
    constructor(name) {
        this.name = name;

        const button = document.getElementById('myButton');
        button.addEventListener('click', this.handleClick); // (A)
    }
    handleClick() {
        console.log('Clicked '+this.name); // (B)
    }
}

在(A)行中,UiComponent 为点击注册一个事件处理程序。唉,如果这个处理程序被触发,你会得到一个错误:

TypeError: Cannot read property 'name' of undefined

为什么呢? 在(A)行中,我们使用了普通的点操作符,而不是特殊的方法调用点操作符。因此,存储在 handleClick 中的函数成为处理程序。也就是说,大致发生以下事情。

const handler = this.handleClick;
handler();
    // 等同于: handler.call(undefined);

结果,this.name 在(B)行中失败了。

那么我们如何解决这个问题呢?问题在于调用方法的点操作符不是简单的读取属性的组合,然后调用结果。它做更多。所以,当我们提取一个方法的时候,我们需要亲自提供缺失的部分,并在(A)行中,通过函数的.bind()方法为 this 填充一个固定值:

class UiComponent {
    constructor(name) {
        this.name = name;

        const button = document.getElementById('myButton');
        button.addEventListener(
            'click', this.handleClick.bind(this)); // (A)
    }
    handleClick() {
        console.log('Clicked '+this.name);
    }
}

现在, this 是固定的,并且不会通过正常的函数调用进行更改。

function returnThis() {
    return this;
}
const bound = returnThis.bind('hello');
bound(); // 'hello'
bound.call(undefined); // 'hello'

3.保持安全的规则

避免 this 问题的最简单方法是避免使用普通函数,并且总是使用方法定义或箭头函数。

不过,我喜欢声明式函数的语法。提升有时也很有用。如果你不在里面引用 this,你可以安全地使用它们。有一个 ESLint 规则可以帮你解决这个问题。

3.1 不要把 this 当成一个参数

一些API通过 this 提供类似参数的信息。我不喜欢这样,因为它阻止了你使用箭头函数,并且违背了最初提到的简单的经验法则。

我们来看一个例子:beforeEach() 函数通过 this 将一个 API 对象传递给它的回调函数。

beforeEach(function () {
    this.addMatchers({ // 访问 API 对象
        toBeInRange: function (start, end) {
            ···
        }
    });
});

重写这个函数很容易:

beforeEach(api => {
    api.addMatchers({
        toBeInRange(start, end) {
            ···
        }
    });
});

4.扩展阅读

原文链接:http://2ality.com/2017/12/alternate-this.html

赞(1) 打赏
未经允许不得转载:WEB前端开发 » 换种方式理解 JavaScript 中的 this

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

前端开发相关广告投放 更专业 更精准

联系我们

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏