Javascript: 你真的了解this的含义吗
原文链接:传送门。
Javascript 的 this是许多笑话的笑点所在,那是因为它真的很复杂。然而,我见过许多开发者做了许多更加复杂的和特定域的事情来避免与this打交道。如果你对this还有点不太确定,我希望这篇文章可以对你有所帮助。
这篇文章是我的关于this对一个指导。
我将以最明确的场景开始,然后以最不明确的场景结束。这篇文章有点像一个大的if (…) … else if () … else if (…)结构,因此你可以直接导航到匹配你正在看的代码的那个部分。
如果函数被定义为一个箭头函数
const arrowFunction = () => { console.log(this); };
在这个情况下,this的值总是和父作用域中this的值一样。
const outerThis = this; const arrowFunction = () => { // Always logs `true`: console.log(this === outerThis); };
箭头函数是强大的,这是因为其内部的this值不会被改变,其总是与外部的this值保持一致,让我们继续看看其他示例。
在箭头函数中,this的值不能以bind来改变:
// Logs `true` - bound `this` value is ignored: arrowFunction.bind({foo: 'bar'})();
在箭头函数中,this的值不能以call或者appy来改变。
// Logs `true` - called `this` value is ignored: arrowFunction.call({foo: 'bar'}); // Logs `true` - applied `this` value is ignored: arrowFunction.apply({foo: 'bar'});
在箭头函数中,this的值不能以作为另一个对象的成员的方式被调用而改变。
const obj = {arrowFunction}; // Logs `true` - parent object is ignored: obj.arrowFunction();
在箭头函数中,this的值不能通过以构造函数的方式来调用而改变。
// TypeError: arrowFunction is not a constructor new arrowFunction();
“绑定”实例方法
在实例方法中,如果你想确保this总是指向类的实例,最好的方式就是使用箭头函数和类字段。
class Whatever { someMethod = () => { // Always the instance of Whatever: console.log(this); }; }
当使用实例方法作为组件中的事件监听器时(比如React组件,web组件),这种模式真的非常有用。
上面的内容或许感觉像打破了“this是与父作用域的this保持一致”这个规则。但是如果您认为类字段是在构造函数中设置内容的语法糖,那么其便开始变得有道理了。
class Whatever { someMethod = (() => { const outerThis = this; return () => { // Always logs `true`: console.log(this === outerThis); }; })(); } // …is roughly equivalent to: class Whatever { constructor() { const outerThis = this; this.someMethod = () => { // Always logs `true`: console.log(this === outerThis); }; } }
可替代的模式涉及在构造函数中绑定一个已存在的函数,或者在构造函数中分配一个函数,如果你因为一些原因不能使用类字段,在构造函数中分配函数是一个理想的替代方案。
class Whatever { constructor() { this.someMethod = () => { // … }; } }
如果函数/类用new关键字调用
new Whatever();
如上的代码会调用Whatever(或者如果它是一个类的话,便会调用其构造函数),并将this设置为Object.create(Whatever.prototype)的结果。
class MyClass { constructor() { console.log( this.constructor === Object.create(MyClass.prototype).constructor, ); } } // Logs `true`: new MyClass();
对于老式的构造函数来说,同样也是true。
function MyClass() { console.log( this.constructor === Object.create(MyClass.prototype).constructor, ); } // Logs `true`: new MyClass();
其他示例。
当以new的方式被调用时,this的值不能以bind来改变。
const BoundMyClass = MyClass.bind({foo: 'bar'}); // Logs `true` - bound `this` value is ignored: new BoundMyClass();
当以new的方式被调用时,当作为另一个对象的成员的方式来调用时,this的值也不能被改变。
const obj = {MyClass}; // Logs `true` - parent object is ignored: new obj.MyClass();
如果函数有一个“绑定的”this值
function someFunction() { return this; } const boundObject = {hello: 'world'}; const boundFunction = someFunction.bind(boundObject);
当调用boundFunction时,它的this值将会是传递给bind的对象(boundObject
)。
// Logs `false`: console.log(someFunction() === boundObject); // Logs `true`: console.log(boundFunction() === boundObject);
⚠️避免使用bind来绑定一个函数到它的外部this,相应的,我们应该使用箭头函数,因为从它的函数声明中,其使得this的意义比较明确,而不是晚一点在代码中发生的那些事。不要使用bind来设置this到一些与父对象不想关的值。它通常不是我们所其期望的而且这也是为什么this获得了这么坏的一个名声的原因。相应的我们应该考虑传递一个值作为参数。它更加的明确,并与箭头函数工作良好。
其他示例
当调用一个bound函数时,this不能以call或者apply来改变。
// Logs `true` - called `this` value is ignored: console.log(boundFunction.call({foo: 'bar'}) === boundObject); // Logs `true` - applied `this` value is ignored: console.log(boundFunction.apply({foo: 'bar'}) === boundObject);
当调用一个bound函数时,this的值不能通过以另一个对象的成员的方式调用而改变。
const obj = {boundFunction}; // Logs `true` - parent object is ignored: console.log(obj.boundFunction() === boundObject);
如果this在调用时被设置
function someFunction() { return this; } const someObject = {hello: 'world'}; // Logs `true`: console.log(someFunction.call(someObject) === someObject); // Logs `true`: console.log(someFunction.apply(someObject) === someObject);
this的值将是传递给call/apply的对象。
⚠️不要使用call/apply来将this的值设置为与父对象无关的值。这通常不是我们所期望的结果,而且这也是为什么this获得了如此坏的名声的原因。考虑传递一个值作为参数来替代。其更加明确并且与箭头函数工作良好。
不幸的是,this常常被诸如DOM事件监听器那样的东西设置为其他的值,并且使用它会导致难以理解的代码。
不要这样使用:
element.addEventListener('click', function (event) { // Logs `element`, since the DOM spec sets `this` to // the element the handler is attached to. console.log(this); });
我会尽量避免在如上的场景中使用this,而会用如下方式来代替:
element.addEventListener('click', (event) => { // Ideally, grab it from a parent scope: console.log(element); // But if you can't do that, get it from the event object: console.log(event.currentTarget); });
如果函数通过一个父对象被调用(parent.func()
)
const obj = { someMethod() { return this; }, }; // Logs `true`: console.log(obj.someMethod() === obj);
在这种情况下函数被作为obj对象的一个成员来调用,因此this将会是obj。这个发生在调用时,因此如果这个函数没有被其父对象所调用,或者以一个不同的父对象所调用,那个连接便会被打破。
const {someMethod} = obj; // Logs `false`: console.log(someMethod() === obj); const anotherObj = {someMethod}; // Logs `false`: console.log(anotherObj.someMethod() === obj); // Logs `true`: console.log(anotherObj.someMethod() === anotherObj);
someMethod() === obj将会返回false,因为someMethod并没有作为obj的成员被调用。当尝试诸如此类的事情时,你或许已经遇到了这个问题:
const $ = document.querySelector; // TypeError: Illegal invocation const el = $('.some-element');
这打破了规则。这是因为querySelector的实现会监视它自己的this值并期望它成为排序的DOM节点,然而如上的代码破坏了这个连接。要正确的实现如上的功能:
const $ = document.querySelector.bind(document); // Or: const $ = (...args) => document.querySelector(...args);
一个有趣的事实:并不是所有的APIs会在内部使用this。像console.log这样的控制台方法被更改为避免使用this引用,因此log便没有必要被绑定到console。
⚠️不要仅通过将this设置为与父对象不相关的值来将一个函数嫁接到一个对象上。它通常不是我们所期望的并会给this带来坏的名声。考虑作为参数传递一个值来代替。它更加显式,并与箭头函数工作良好。
如果函数或者父作用域处于一个strict模式下
function someFunction() { 'use strict'; return this; } // Logs `true`: console.log(someFunction() === undefined);
在这种情况下,this的值会是underfined。如果父作用域已经是strict模式了,那么在函数中“use strict”便不是必须的了。
⚠️不要依赖这个,我的意思是,有更简单的方式来获取underfined值。
其他场景
function someFunction() { return this; } // Logs `true`: console.log(someFunction() === globalThis);
在这个场景下,this的值是与globalThis一样的。
⚠️避免使用this来引用全局对象。相反,请显式的使用globalThis。
【完结】