Web开发编程网
分享Web开发相关技术

换种方式理解 JavaScript 中的 this

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

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

1.两种类型的函数

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

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

1.1 普通函数

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

JavaScript 代码:
function add(x, y) {

    return x + y;

}

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

JavaScript 代码:
add(3, 5)

add.call(undefined, 3, 5);

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

JavaScript 代码:
function outer() {

    function inner() {

        console.log(this); // undefined

    }

 

    console.log(this); // 'outer'

    inner();

}

outer.call('outer');

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

JavaScript 代码:
const _this = 'outer';

console.log(_this); // 'outer'

{

    const _this = undefined;

    console.log(_this); // undefined

}

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

1.2 箭头函数

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

JavaScript 代码:
const add = (x, y) => {

    return x + y;

};

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

JavaScript 代码:
function outer() {

    const inner = () => {

        console.log(this); // 'outer'

    };

    console.log(this); // 'outer'

    inner();

}

outer.call('outer');

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

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

JavaScript 代码:
function ordinary() {

    const arrow = () => this;

    console.log(arrow.call('goodbye')); // 'hello'

}

ordinary.call('hello');

1.3 作为方法的普通函数

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

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

JavaScript 代码:
const obj = {

    prop: function () {}

};

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

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

后者相当于:

JavaScript 代码:
obj.prop.call(obj, x, y)

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

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

JavaScript 代码:
const obj = {

    prop() {}

};

2.常见的陷阱

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

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

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

JavaScript 代码:
// 在类或对象字面量中:

performCleanup() {

    cleanupAsync()

    .then(function () {

        this.logStatus('Done'); // (A)

    });

}

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

JavaScript 代码:
// 在类或对象字面量中:

performCleanup() {

    cleanupAsync()

    .then(() => {

        this.logStatus('Done');

    });

}

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

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

JavaScript 代码:
// 在类或对象字面量中:

prefixNames(names) {

    return names.map(function (name) {

        return this.company + ': ' + name; // (A)

    });

}

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

JavaScript 代码:
// 在类或对象字面量中:

prefixNames(names) {

    return names.map(

        name => this.company + ': ' + name);

}

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

以下是用于UI组件的类。

JavaScript 代码:
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 为点击注册一个事件处理程序。唉,如果这个处理程序被触发,你会得到一个错误:

JavaScript 代码:
TypeError: Cannot read property 'name' of undefined

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

JavaScript 代码:
const handler = this.handleClick;

handler();

    // 等同于: handler.call(undefined);

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

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

JavaScript 代码:
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 是固定的,并且不会通过正常的函数调用进行更改。

JavaScript 代码:
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 对象传递给它的回调函数。

JavaScript 代码:
beforeEach(function () {

    this.addMatchers({ // 访问 API 对象

        toBeInRange: function (start, end) {

            ···

        }

    });

});

重写这个函数很容易:

JavaScript 代码:
beforeEach(api => {

    api.addMatchers({

        toBeInRange(start, end) {

            ···

        }

    });

});

4.扩展阅读

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

未经允许不得转载:WEB开发编程网 » 换种方式理解 JavaScript 中的 this
微信扫码关注微信公众号

WEB开发编程网

谢谢支持,我们一直在努力

安全提示:您正在对WEB开发编程网进行赞赏操作,一但支付,不可返还。