引言:this,那个让人又爱又恨的“磨人精”
this像一个捉摸不定的幽灵,在不同的场景下,悄然改变着自己的指向,让初学者摸不着头脑,也让经验丰富的开发者偶尔“翻车”。
本文将结合大量代码示例,详细解释 this 的四种绑定规则、箭头函数的特殊性、严格模式的影响以及它们之间的优先级。我们还会深入探讨 this 的底层原理,并针对前端面试中常见的 this 问题进行逐一击破。准备好了吗?让我们开始这场 this 的奇妙之旅吧!🚀
this 的四大天王:绑定规则深度解析
要理解 this,首先要掌握它的四种核心绑定规则。JavaScript 引擎在执行函数时,会根据函数的调用方式,自动确定 this 的值。这四种规则分别是:默认绑定、隐式绑定、显式绑定和 new 绑定。它们就像 this 的“四大天王”,各自掌管着不同的绑定场景。
1. 默认绑定:当“皇帝”不指定,就归“老天爷”管
默认绑定是 this 绑定中最常见也最容易被忽视的一种情况。当函数作为独立函数被调用,且没有其他明确的绑定规则生效时,this 会被默认绑定到全局对象。在浏览器环境中,这个全局对象就是 window;在 Node.js 环境中,则是 global。
底层原理: 当函数以独立形式被调用时,JavaScript 引擎会将其 this 上下文设置为全局对象。这可以理解为一种“后备”机制,确保 this 始终有一个指向。
代码示例与分析:
function foo() {
console.log( this.a );
}
var a = 2;
foo(); // 输出 2
在这个例子中,foo() 是一个独立函数调用。没有明确的对象来调用它,也没有使用 call、apply 或 new。因此,this 默认绑定到全局对象 window(在浏览器中)。window.a 的值为 2,所以 console.log(this.a) 输出 2。
这种情况下,如果函数内部没有定义 a,但全局对象有 a,那么就会访问到全局的 a。
2. 隐式绑定:谁调用,this 就指向谁
隐式绑定是 this 绑定中最直观的一种情况。当函数作为某个对象的方法被调用时,this 会隐式地绑定到那个调用它的对象。简单来说,就是“谁调用我,我就指向谁”。
底层原理: 当函数作为对象属性被访问并执行时,JavaScript 引擎会将该对象作为函数的 this 上下文。这使得方法能够访问和操作其所属对象的属性。
代码示例与分析:
var obj = {
a: 1,
foo: function() {
console.log(this.a);
}
};
obj.foo(); // 输出 1
在这个例子中,foo 函数是 obj 对象的一个方法,并通过 obj.foo() 的形式被调用。因此,foo 内部的 this 隐式绑定到 obj 对象。obj.a 的值为 1,所以 console.log(this.a) 输出 1。
隐式绑定常常与函数作为参数传递、或者对象属性赋值等情况结合考察,容易造成
this丢失。
示例 1 :
让我们回顾一下第一个示例:
function foo() {
console.log( this.a );
}
function doFoo() {
foo();
}
var obj = {
a: 1,
doFoo: doFoo
};
var a = 2;
obj.doFoo()
输出结果: 2
解析: 乍一看,obj.doFoo() 调用了 doFoo,而 doFoo 又调用了 foo。你可能会觉得 this 应该指向 obj。但仔细分析:
obj.doFoo():这里doFoo是作为obj的方法被调用,所以doFoo内部的this指向obj。但doFoo内部并没有使用this。doFoo()内部调用foo():这里的foo()是一个独立的函数调用,它没有被任何对象“点”出来。因此,foo()内部的this遵循默认绑定规则,指向全局对象window。全局变量a的值为2,所以最终输出2。
这个例子完美地展示了 this 绑定只取决于函数被调用的那一刻,而不是函数在哪里定义,也不是函数被哪个函数调用。它只关心谁最终调用了它。
3. 显式绑定:强行指定 this 的“归属”
显式绑定是指通过 call()、apply() 和 bind() 这三个方法,明确地指定函数执行时 this 的指向。它们就像 this 的“红娘”,强制 this 绑定到你想要的对象。
底层原理: call、apply 和 bind 是 Function.prototype 上的方法。它们允许你借用其他对象的方法,或者改变函数执行时的 this 上下文。它们在内部机制上会临时修改函数的 this 指向,然后执行函数。
call() 和 apply():立即执行,指定 this
- 共同点: 立即执行函数,并接受第一个参数作为
this的值。 - 区别:
call()接受一系列参数,apply()接受一个参数数组。
代码示例与分析:
function foo() {
console.log(this.a);
}
var obj = { a: 1 };
foo.call(obj); // 输出 1
foo.apply(obj); // 输出 1
通过 foo.call(obj) 或 foo.apply(obj),我们强制 foo 函数内部的 this 指向 obj 对象,即使 foo 是一个独立函数。因此,this.a 访问的是 obj.a,输出 1。
bind():绑定 this,返回新函数
bind() 方法与 call() 和 apply() 不同,它不会立即执行函数,而是返回一个绑定了 this 的新函数。这个新函数的 this 永远指向 bind() 传入的第一个参数,即使之后使用 new 关键字调用,也无法改变其 this 绑定。
代码示例与分析:
function foo() {
console.log(this.a);
}
var obj = { a: 1 };
var bar = foo.bind(obj);
bar(); // 输出 1
foo.bind(obj) 返回了一个新的函数 bar,这个 bar 函数的 this 永远被绑定到 obj。所以当调用 bar() 时,this.a 依然是 obj.a,输出 1。
call(null) 或 call(undefined) 的特殊情况
function a() {
console.log(this);
}
a.call(null);
打印结果: window 对象
解析: 根据 ECMAScript 规范,如果 call 或 apply 的第一个参数是 null 或 undefined,那么 this 会被自动替换为全局对象(在浏览器中是 window)。这可以看作是显式绑定的一种特殊情况,它最终会回退到默认绑定。
严格模式下的显式绑定:
'use strict';
function a() {
console.log(this);
}
a.call(null); // null
a.call(undefined); // undefined
解析: 在严格模式下,call 或 apply 的第一个参数如果传入 null 或 undefined,this 不会再被转换为全局对象,而是保持为 null 或 undefined。这是严格模式对 this 行为的修正,使得代码更加可预测。
4. new 绑定:构造函数的新生之力
当函数作为构造函数,使用 new 关键字调用时,this 会被绑定到新创建的实例对象。这是 this 绑定中优先级最高的一种情况(除了箭头函数)。
底层原理: 当使用 new 关键字调用一个函数时,JavaScript 引擎会执行以下四个步骤:
- 创建一个全新的空对象:这个对象就是即将返回的实例。
- 链接原型:将这个新对象的
[[Prototype]](即__proto__)链接到构造函数的prototype属性。 - 绑定
this:将这个新对象绑定为函数调用的this。这意味着在构造函数内部,this指向的就是这个新创建的对象。 - 返回新对象:如果构造函数没有显式地返回其他对象,那么
new表达式会自动返回这个新创建的对象。如果构造函数显式地返回了一个对象,那么new表达式会返回这个显式返回的对象;如果返回的是非对象值,则会被忽略,仍然返回新创建的对象。
代码示例与分析:
function Foo(a) {
this.a = a;
}
var bar = new Foo(2);
console.log(bar.a); // 输出 2
在这个例子中,new Foo(2) 创建了一个新的对象 bar,并将 Foo 函数内部的 this 绑定到 bar。因此,this.a = a 实际上是 bar.a = 2。最终 console.log(bar.a) 输出 2。
new 绑定与隐式绑定的结合
new 绑定和隐式绑定的区别:
var obj = {
name: 'cuggz',
fun: function(){
console.log(this.name);
}
}
obj.fun() // cuggz
new obj.fun() // undefined
解析:
obj.fun():这里fun是作为obj的方法被调用,遵循隐式绑定,this指向obj。所以this.name是obj.name,输出cuggz。new obj.fun():这里fun是作为构造函数被new调用,遵循new绑定。new会创建一个新的空对象,并将fun内部的this绑定到这个新对象。由于这个新对象上没有name属性,所以this.name输出undefined。
这个例子再次强调了 this 的绑定只取决于函数被调用的方式,而不是函数定义的位置。
箭头函数的“特立独行”:没有自己的 this
ES6 引入的箭头函数(Arrow Function)在 this 的处理上与传统函数有着根本性的不同。它没有自己的 this 绑定,而是捕获其所在(定义时)上下文的 this 值,作为自己的 this。这意味着箭头函数的 this 在定义时就已经确定,并且永远不会改变,即使你尝试使用 call、apply 或 bind 也无法改变。
底层原理: 箭头函数在被定义时,会“记住”其外层(词法)作用域的 this。这个 this 值在箭头函数被创建的那一刻就已经固定,并且在整个生命周期中都不会改变。这与普通函数在运行时动态确定 this 的机制截然不同。
代码示例与分析:
var a = 10
var obj = {
a: 20,
say: () => {
console.log(this.a)
}
}
obj.say()
var anotherObj = { a: 30 }
obj.say.apply(anotherObj)
输出结果: 10 10
解析:
obj.say():say是一个箭头函数。它定义在全局作用域下(因为对象字面量不创建独立作用域),所以它的this捕获的是全局对象window。因此,this.a访问的是window.a,输出10。obj.say.apply(anotherObj):即使我们尝试使用apply显式绑定this到anotherObj,但由于say是箭头函数,它的this已经固定为window,无法被改变。所以依然输出10。
更复杂的箭头函数场景
var obj = {
say: function() {
var f1 = () => {
console.log("1111", this);
}
f1();
},
pro: {
getPro:() => {
console.log(this);
}
}
}
var o = obj.say;
o();
obj.say();
obj.pro.getPro();
输出结果:
解析:
o():o是obj.say的引用,并在全局作用域中独立调用。say是一个普通函数,此时say内部的this遵循默认绑定,指向window。f1是一个箭头函数,它定义在say函数内部,所以f1的this捕获的是say函数的this,即window。因此,console.log(1111", this)输出1111 window对象。obj.say():这里say是作为obj的方法被调用,遵循隐式绑定,所以say内部的this指向obj。f1作为箭头函数,其this捕获的是say函数的this,即obj。因此,console.log("1111", this)输出1111 obj对象。obj.pro.getPro():getPro是一个箭头函数,它定义在对象字面量pro内部。然而,对象字面量不构成独立的作用域。pro对象本身没有this。因此,getPro的this会继续向上捕获,直到找到最近的非箭头函数父级作用域的this。在这个例子中,getPro的定义上下文是全局作用域(因为它没有被包裹在任何普通函数中),所以它的this最终指向全局对象window。因此,console.log(this)输出window对象。
箭头函数的 this 是词法作用域的 this,即它在定义时就确定了,并且会穿透对象字面量,向上寻找最近的非箭头函数父级作用域的 this。
this 的“排位赛”:绑定优先级大揭秘 🏆
当一个函数可能同时符合多种 this 绑定规则时,JavaScript 引擎会按照一个固定的优先级顺序来决定 this 的最终指向。理解这个优先级是彻底掌握 this 的关键所在。我们可以将 this 的绑定优先级总结为:
new 绑定 > 显式绑定 (call/apply/bind) > 隐式绑定 > 默认绑定
让我们逐一深入理解这个优先级链:
-
new绑定 (最高优先级):- 如果函数是通过
new关键字调用的,那么this总是指向新创建的对象。这是因为new操作符在执行时会创建一个全新的对象,并将该对象绑定为函数内部的this。这个绑定优先级最高,甚至可以覆盖bind带来的显式绑定。
- 如果函数是通过
-
显式绑定 (
call/apply/bind):- 如果函数通过
call()、apply()或bind()方法调用,那么this会被强制绑定到这些方法传入的第一个参数。其中,bind创建的新函数,其this绑定优先级高于隐式绑定和默认绑定。
- 如果函数通过
-
隐式绑定:
- 如果函数作为对象的方法被调用(即通过
obj.method()的形式),那么this指向调用该方法的对象。
- 如果函数作为对象的方法被调用(即通过
-
默认绑定 (最低优先级):
- 如果以上所有规则都不适用,函数以独立形式调用,那么
this指向全局对象(非严格模式下)或undefined(严格模式下)。
- 如果以上所有规则都不适用,函数以独立形式调用,那么
箭头函数的特殊性:
需要特别注意的是,箭头函数的 this 绑定不遵循上述四种规则。它完全取决于其外层(词法)作用域的 this,并且一旦确定就无法改变。因此,在某种意义上,箭头函数的 this 优先级是最高的,因为它根本不参与上述规则的判断和竞争,它只是“继承”了父级的 this。
代码示例与分析:
new 绑定 vs 隐式绑定 vs 显式绑定
function foo(something){
this.a = something
}
var obj1 = {
foo: foo
}
var obj2 = {}
obj1.foo(2);
console.log(obj1.a); // 2
obj1.foo.call(obj2, 3);
console.log(obj2.a); // 3
var bar = new obj1.foo(4)
console.log(obj1.a); // 2
console.log(bar.a); // 4
输出结果: 2 3 2 4
解析:
obj1.foo(2):foo作为obj1的方法被调用,遵循隐式绑定。this指向obj1,所以obj1.a被设置为2。console.log(obj1.a)输出2。obj1.foo.call(obj2, 3):这里使用了call进行显式绑定。foo函数的this被强制绑定到obj2。所以obj2.a被设置为3。console.log(obj2.a)输出3。console.log(obj1.a):此时obj1.a仍然是2,因为上一步的操作是针对obj2的,没有影响obj1。输出2。var bar = new obj1.foo(4):这里使用了new关键字。new绑定优先级最高。new会创建一个新的对象bar,并将foo函数内部的this绑定到bar。所以bar.a被设置为4。console.log(bar.a)输出4。
这个例子清晰地展示了隐式绑定、显式绑定和 new 绑定在不同调用方式下的优先级和效果。
bind 与 new 的优先级
function foo(something){
this.a = something
}
var obj1 = {}
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2
var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3
输出结果: 2 2 3
解析:
var bar = foo.bind(obj1):bind方法返回了一个新的函数bar,这个bar函数的this永远被绑定到obj1。bar(2):调用bar函数,由于bar已经通过bind绑定到obj1,所以this指向obj1。obj1.a被设置为2。console.log(obj1.a)输出2。var baz = new bar(3):这里是关键!虽然bar已经通过bind绑定到obj1,但是当bar被new关键字调用时,new绑定的优先级会高于bind带来的显式绑定。因此,new会创建一个全新的对象baz,并将foo函数内部的this绑定到baz。所以baz.a被设置为3。console.log(obj1.a):此时obj1.a仍然是2,因为new bar(3)的操作是针对新创建的baz对象的,没有影响obj1。输出2。console.log(baz.a):输出baz对象上的a属性,即3。
这个例子完美地印证了 new 绑定优先级高于显式绑定(包括 bind)。
深入剖析:面试常考的 this 陷阱与实战技巧 🤯
理解了 this 的四大绑定规则和优先级,我们就可以开始应对那些“刁钻”的面试题和实际开发中的 this 陷阱了。这些问题往往是多种绑定规则的组合,或者涉及一些特殊情况。
self 变量的妙用
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log(this.foo);
console.log(self.foo);
(function() {
console.log(this.foo);
console.log(self.foo);
}());
}
};
myObject.func();
输出结果: bar bar undefined bar
解析:
myObject.func():func作为myObject的方法被调用,遵循隐式绑定,所以func内部的this指向myObject。因此,console.log(this.foo)输出myObject.foo,即"bar"。var self = this;:在func函数内部,self变量被赋值为当前的this(即myObject)。这是一个非常常见的技巧,用于在嵌套函数中“保存”外部this的指向。console.log(self.foo):由于self指向myObject,所以输出myObject.foo,即"bar"。(function() { ... }()):这是一个立即执行函数 (IIFE)。这个 IIFE 是一个普通的函数调用,它没有被任何对象“点”出来,也没有使用call、apply或new。因此,它遵循默认绑定。在非严格模式下,IIFE 内部的this指向全局对象window。由于window对象上没有foo属性,所以console.log(this.foo)输出undefined。console.log(self.foo)(IIFE 内部):虽然 IIFE 内部的this指向window,但self变量是在外部func作用域中定义的,并且通过闭包被 IIFE 访问到。self仍然指向myObject,所以console.log(self.foo)输出myObject.foo,即"bar"。
这个例子完美地展示了 self = this 这种模式在解决嵌套函数 this 指向问题上的有效性。
立即执行函数与 this 的复杂交互
window.number = 2;
var obj = {
number: 3,
db1: (function(){
console.log(this);
this.number *= 4;
return function(){
console.log(this);
this.number *= 5;
}
})()
}
var db1 = obj.db1;
db1();
obj.db1();
console.log(obj.number); // 15
console.log(window.number); // 40
输出结果: window 对象, window 对象, obj 对象, 15, 40
解析: 这道题目看起来有点复杂,但只要一步步分析 this 的指向,就能迎刃而解。
-
db1: (function(){ ... })():- 这是一个立即执行函数 (IIFE)。这个 IIFE 是在全局作用域中被定义的,并且是立即执行的。因此,IIFE 内部的
this遵循默认绑定,指向全局对象window。 console.log(this):输出window对象。this.number *= 4:此时this是window,所以window.number变为2 * 4 = 8。- IIFE 返回了一个匿名函数
function(){ ... },这个匿名函数被赋值给了obj.db1。
- 这是一个立即执行函数 (IIFE)。这个 IIFE 是在全局作用域中被定义的,并且是立即执行的。因此,IIFE 内部的
-
var db1 = obj.db1;:- 将
obj.db1(即 IIFE 返回的匿名函数)赋值给全局变量db1。
- 将
-
db1():- 这里调用的是全局变量
db1,它是一个独立的函数调用,遵循默认绑定。因此,匿名函数内部的this指向全局对象window。 console.log(this):输出window对象。this.number *= 5:此时this是window,window.number变为8 * 5 = 40。
- 这里调用的是全局变量
-
obj.db1():- 这里调用的是
obj对象的db1方法,遵循隐式绑定。因此,匿名函数内部的this指向obj对象。 console.log(this):输出obj对象。this.number *= 5:此时this是obj,obj.number变为3 * 5 = 15。
- 这里调用的是
-
console.log(obj.number):输出obj.number的最终值,即15。 -
console.log(window.number):输出window.number的最终值,即40。
arguments 对象的 length 属性
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
};
obj.method(fn, 1);
输出结果: 10 2
解析:
obj.method(fn, 1):method作为obj的方法被调用,method内部的this指向obj。传入的参数是fn函数和数字1。fn():在method内部,fn是一个独立的函数调用,遵循默认绑定。因此,fn内部的this指向全局对象window。window.length的值是10(全局变量length),所以第一次输出10。arguments[0]():这里是关键!arguments是一个类数组对象,它包含了函数被调用时传入的所有参数。arguments[0]实际上就是fn函数。当通过arguments[0]()这种形式调用时,arguments对象成为了fn函数的调用者。因此,fn内部的this遵循隐式绑定,指向arguments对象。arguments对象有一个length属性,表示传入参数的数量。obj.method函数被调用时传入了两个参数 (fn和1),所以arguments.length的值是2。因此,第二次输出2。
多重调用下的 this 变化
var a = 1;
function printA(){
console.log(this.a);
}
var obj={
a:2,
foo:printA,
bar:function(){
printA();
}
}
obj.foo(); // 2
obj.bar(); // 1
var foo = obj.foo;
foo(); // 1
输出结果: 2 1 1
解析:
obj.foo():foo是obj的方法,遵循隐式绑定。this指向obj。obj.a为2,所以输出2。obj.bar():bar是obj的方法,遵循隐式绑定,bar内部的this指向obj。然而,在bar内部调用printA()时,printA是一个独立的函数调用,遵循默认绑定。this指向全局对象window。window.a为1,所以输出1。var foo = obj.foo;:将obj.foo(即printA函数)赋值给全局变量foo。foo():调用全局变量foo,这是一个独立的函数调用,遵循默认绑定。this指向全局对象window。window.a为1,所以输出1。
嵌套函数中的 this 陷阱
var x = 3;
var y = 4;
var obj = {
x: 1,
y: 6,
getX: function() {
var x = 5;
return function() {
return this.x;
}();
},
getY: function() {
var y = 7;
return this.y;
}
}
console.log(obj.getX()) // 3
console.log(obj.getY()) // 6
输出结果: 3 6
解析:
obj.getX():getX是obj的方法,遵循隐式绑定,getX内部的this指向obj。然而,getX内部返回的是一个立即执行函数(function() { return this.x; })()。这个立即执行函数是一个独立的函数调用,遵循默认绑定。因此,其内部的this指向全局对象window。window.x为3,所以console.log(obj.getX())输出3。obj.getY():getY是obj的方法,遵循隐式绑定。getY内部的this指向obj。obj.y为6,所以console.log(obj.getY())输出6。
call 参数与括号对 this 的影响
var a = 10;
var obt = {
a: 20,
fn: function(){
var a = 30;
console.log(this.a)
}
}
obt.fn(); // 20
obt.fn.call(); // 10
(obt.fn)(); // 20
输出结果: 20 10 20
解析:
obt.fn():fn作为obt的方法被调用,遵循隐式绑定。this指向obt。obt.a为20,所以输出20。obt.fn.call():这里使用了call进行显式绑定。但是call方法没有传入任何参数,这等同于call(undefined)。根据前面提到的规则,当call或apply的第一个参数是null或undefined时,this会被自动替换为全局对象window(非严格模式)。window.a为10,所以输出10。(obt.fn)():这里的括号()仅仅是改变了表达式的运算顺序,它不影响fn函数的调用方式。fn仍然是作为obt的方法被调用,遵循隐式绑定。this指向obt。obt.a为20,所以输出20。
总结与建议:彻底驯服 this 的秘诀 💡
通过以上对 this 的四大绑定规则、箭头函数的特殊性、严格模式的影响以及绑定优先级的深入剖析,相信你对 JavaScript 中 this 的魔幻指向已经有了更清晰的认识。this 确实是一个复杂但又极其重要的概念,它是 JavaScript 灵活性的体现,也是面试官考察你对语言底层理解的利器。
彻底掌握 this 的秘诀在于:
- 理解调用方式:
this的指向完全取决于函数被调用的方式,而不是函数定义的位置。 - 熟记绑定规则:牢记默认绑定、隐式绑定、显式绑定和
new绑定这四种规则。 - 区分箭头函数:箭头函数没有自己的
this,它继承自外层词法作用域。 - 掌握优先级:当多种规则可能同时生效时,记住
new> 显式 > 隐式 > 默认的优先级。 - 多实践,多调试:遇到
this问题时,不要害怕,多动手写代码,多使用console.log(this)进行调试,逐步分析。
希望这篇博客能帮助你彻底驯服 this 这个“磨人精”,在面试中游刃有余,在实际开发中写出更健壮、更可预测的代码!如果你有任何疑问或想分享你的 this 学习心得,欢迎在评论区留言讨论!一起进步!🚀