this会在执行的上下文中绑定一个对象,有时候绑定全局对象,有时绑定的是某个对象,所以在什么情况下进行什么绑定,比较迷惑,于是打算写这篇文章梳理一下脉络。
先说结论:this的绑定取决于函数的直接调用位置。
1. 调用位置
首先要理解什么是调用位置:调用位置就是函数在代码中调用的位置,而不是函数声明的位置。
function foo{ console.log('foo'); bar(); // <-- bar()的调用位置}function bar{ console.log('bar'); baz(); // <-- baz()的调用位置}function baz{ console.log('baz');}foo(); // <-- foo()的调用位置2. 绑定规则
判断this是如何绑定,首先找到函数的调用位置,然后对比下面的规则,看符合哪一条,且这些规则具有不同的优先级。
2.1 默认绑定
首先,最常用的函数调用类型是:独立函数调用。这条规则可以看作是不符合其他规则时的默认规则。
场景 1:独立函数的调用
因为this没有绑定到任何对象,所以默认绑定到全局。
function foo() { console.log(this.a);}const a = 2;foo(); // 2场景 2:将函数作为参数传入另一个函数时
这样的绑定,本质上仍然是独立函数的调用。
function foo(fn) { fn();}function bar() { console.log(this.a); // window}var a = 8;foo(bar); // 8 但,如果使用let和const,或是严格模式下,隐式绑定会丢失。
let a = 8;function foo(fn) { fn();}function bar() { console.log(this.a);}foo(bar); // undefined2.2 隐式绑定
第二条需要考虑的规则是调用位置是否由上下文对象,或者说,是否被某个对象拥有或包含。
场景 1:通过对象调用函数
foo()被调用时,它的落脚点指向obj对象,调用位置会使用obj上下文来引用函数,因此也可以称为函数被调用时obj对象“拥有”或“包含”它。 当函数引用有上下文时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因此this.a和obj.a是一样的。
function foo() { console.log(this.a);}const obj = { a: 2, foo: foo // foo 指向 foo(),被 obj 包含/拥有}obj.foo(); // 2场景 2:多层对象调用函数
对象属性的引用链中只有最顶层(最后一层)会影响调用位置。
function foo() { console.log(this.a);}const obj2 = { a: 42, foo: foo}const obj1 = { a: 2, obj2: obj2}obj1.obj2.foo(); // 42场景 3:隐式丢失
隐式绑定的this很容易丢失绑定对象。
下面这个例子,虽然bar是obj.foo的一个引用,但实际上它引用的是foo()函数本身,函数调用位置是bar,它没有绑定任何对象,因此是默认绑定。
// 非严格模式function foo() { console.log(this.a);}var obj = { a: 2, foo: foo}// 函数别名var bar = obj.foo;var a = 'hello';bar(); // hello 另外一种丢失的情况,发生在传入回调函数时,这也包括我们常用的定时setTimeout
function foo() { console.log(this.a);}function bar(fn) { // fn引用的是foo fn(); // <-- 调用位置}var obj = { a: 2, foo: foo}// 函数别名var a = 'hello??';bar(obj.foo); // hello?? 回调函数丢失this绑定非常常见,甚至还有可能修改this的绑定。
2.3 显式绑定
隐式绑定的实现,必须是在一个对象内部包含一个指向函数的属性,然后通过该属性间接地引用函数。
如果我们不想这样做呢?可以使用显式绑定。
方法1:call(..) 和 apply(..)
第一个参数是对象,它们会把this绑定到这个对象,然后调用函数时指向这个对象。
function foo() { console.log(this.a);}var obj={ a: 2}foo.call(obj); // 输出:2 恭喜,成功绑上方法2:硬绑定
如果我们希望一个函数总是显示的绑定到一个对象上,可以怎么做呢?
可以用硬绑定的方法。这种绑定是一种显式的强制绑定,之后无论如何调用函数,它的this指向都不会修改,所以称之为硬绑定。
方法1:创建一个包裹函数,传入参数并返回这些值
function foo(arg) {console.log(this.a, arg);return this.a + arg;} obj = { a: 5 };
var bar = function () { return foo.apply(obj, arguments) }
var b = bar(3); // 5, 3 console.log(b); // 8
function foo() { console.log(this.a);}const a = 2;foo(); // 20 因为硬绑定非常常见,所以ES5也提供了内置的方法Function.prototype.bind()
方法3:使用Function.prototype.bind
function foo() { console.log(this.a);}const a = 2;foo(); // 21 var obj = { name: "快来绑定我" }
var bar = foo.bind(obj);
bar(); // 快来绑定我 bar(); // 快来绑定我
function foo() { console.log(this.a);}const a = 2;foo(); // 222.4 new 绑定
new关键字很容易让人想到“类”,但确切来说,它的作用不是初始化一个类,也不会实例化一个类。实际上,被new调用的函数,只是一个被new操作符调用的普通函数而已。
使用new关键字来调用函数时,会执行如下的操作:
创建(或者说构造)一个全新的对象
这个新对象会被执行[[ 原型 ]]连接
这个新对象会绑定到函数调用的this上(this的绑定在这个步骤完成)
如果函数没有返回其他对象,new表达式中的函数调用会返回这个新对象
function foo() { console.log(this.a);}const a = 2;foo(); // 233. 优先级
优先级总结:
new绑定 > 显示绑定(bind)> 隐式绑定 > 默认绑定
默认规则的优先级最低
显示绑定 > 隐式绑定
new 绑定优 > 隐式绑定
new 绑定 > bind绑定
论证过程参考: 《你不知道的JavaScript》上卷 p91-95 或 coderwhy 的文章 《前端面试之彻底搞懂this指向》
(题外话,他这篇文章的内容,除了例子,基本上都来自上面那本书hh )
4. 绑定的例外
当然,总是会有规则之外的例外。
忽略显示绑定
如果在显示绑定中,我们传入一个null或者undefined,那么这个显示绑定会被忽略,而会使用默认绑定:
function foo() { console.log(this.a);}const a = 2;foo(); // 24 为什么会想要传入一个null呢?
在某些情况下,我们要用aplly(..)来展开一个数组,或是用bind(..)做点什么。但这俩函数都需要传入一个this的绑定对象,但我们不太关心this绑定点啥,于是需要null这么一个绝妙的占位符。
间接引用
另外一种情况,当创建一个函数的间接引用时,会应用默认绑定规则。
间接引用最容易发生在赋值时:
function foo() { console.log(this.a);}const a = 2;foo(); // 25ES6箭头函数
ES6中的箭头函数并不会使用以上这四条绑定规则,它有自己的想法。它会根据当前的词法作用域来决定this。具体地说,箭头函数会继承外层调用的this绑定(无论this绑定到什么)。这和之前常用的self = this机制一致。
function foo() { console.log(this.a);}const a = 2;foo(); // 26 foo()内部创建的箭头函数会捕获调用foo()时的this。因为foo()绑定到obj1,相应地,bar(引用箭头函数)的this也会绑定到obj1,且硬绑定无法修改。
箭头函数也经常用在回调函数中,比如计时器:
function foo() { console.log(this.a);}const a = 2;foo(); // 27练手题目
题目摘自 coderwhy 的文章 《前端面试之彻底搞懂this指向》
第一题
function foo() { console.log(this.a);}const a = 2;foo(); // 28 第二题
function foo() { console.log(this.a);}const a = 2;foo(); // 29 var person2 = { name: 'person2' }
person1.foo1(); person1.foo1.call(person2);
person1.foo2(); person1.foo2.call(person2);
person1.foo3()(); person1.foo3.call(person2)(); person1.foo3().call(person2);
person1.foo4()(); person1.foo4.call(person2)(); person1.foo4().call(person2);
function foo(fn) { fn();}function bar() { console.log(this.a); // window}var a = 8;foo(bar); // 80原文:https://juejin.cn/post/7098523999344263205