this:JavaScript中的“戏精”,一篇拿捏!

90 阅读19分钟

引言: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() 是一个独立函数调用。没有明确的对象来调用它,也没有使用 callapplynew。因此,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。但仔细分析:

  1. obj.doFoo():这里 doFoo 是作为 obj 的方法被调用,所以 doFoo 内部的 this 指向 obj。但 doFoo 内部并没有使用 this
  2. doFoo() 内部调用 foo():这里的 foo() 是一个独立的函数调用,它没有被任何对象“点”出来。因此,foo() 内部的 this 遵循默认绑定规则,指向全局对象 window。全局变量 a 的值为 2,所以最终输出 2

这个例子完美地展示了 this 绑定只取决于函数被调用的那一刻,而不是函数在哪里定义,也不是函数被哪个函数调用。它只关心谁最终调用了它

3. 显式绑定:强行指定 this 的“归属”

显式绑定是指通过 call()apply()bind() 这三个方法,明确地指定函数执行时 this 的指向。它们就像 this 的“红娘”,强制 this 绑定到你想要的对象。

底层原理: callapplybind 是 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 规范,如果 callapply 的第一个参数是 nullundefined,那么 this 会被自动替换为全局对象(在浏览器中是 window)。这可以看作是显式绑定的一种特殊情况,它最终会回退到默认绑定。

严格模式下的显式绑定:

'use strict';

function a() {
    console.log(this);
}
a.call(null);      // null
a.call(undefined); // undefined

解析: 在严格模式下,callapply 的第一个参数如果传入 nullundefinedthis 不会再被转换为全局对象,而是保持为 nullundefined。这是严格模式对 this 行为的修正,使得代码更加可预测。

4. new 绑定:构造函数的新生之力

当函数作为构造函数,使用 new 关键字调用时,this 会被绑定到新创建的实例对象。这是 this 绑定中优先级最高的一种情况(除了箭头函数)。

底层原理: 当使用 new 关键字调用一个函数时,JavaScript 引擎会执行以下四个步骤:

  1. 创建一个全新的空对象:这个对象就是即将返回的实例。
  2. 链接原型:将这个新对象的 [[Prototype]](即 __proto__)链接到构造函数的 prototype 属性。
  3. 绑定 this:将这个新对象绑定为函数调用的 this。这意味着在构造函数内部,this 指向的就是这个新创建的对象。
  4. 返回新对象:如果构造函数没有显式地返回其他对象,那么 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

解析:

  1. obj.fun():这里 fun 是作为 obj 的方法被调用,遵循隐式绑定this 指向 obj。所以 this.nameobj.name,输出 cuggz
  2. new obj.fun():这里 fun 是作为构造函数被 new 调用,遵循 new 绑定new 会创建一个新的空对象,并将 fun 内部的 this 绑定到这个新对象。由于这个新对象上没有 name 属性,所以 this.name 输出 undefined

这个例子再次强调了 this 的绑定只取决于函数被调用的方式,而不是函数定义的位置。

箭头函数的“特立独行”:没有自己的 this

ES6 引入的箭头函数(Arrow Function)在 this 的处理上与传统函数有着根本性的不同。它没有自己的 this 绑定,而是捕获其所在(定义时)上下文的 this 值,作为自己的 this。这意味着箭头函数的 this 在定义时就已经确定,并且永远不会改变,即使你尝试使用 callapplybind 也无法改变。

底层原理: 箭头函数在被定义时,会“记住”其外层(词法)作用域的 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

解析:

  1. obj.say()say 是一个箭头函数。它定义在全局作用域下(因为对象字面量不创建独立作用域),所以它的 this 捕获的是全局对象 window。因此,this.a 访问的是 window.a,输出 10
  2. obj.say.apply(anotherObj):即使我们尝试使用 apply 显式绑定 thisanotherObj,但由于 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();

输出结果:

image.png

解析:

  1. o()oobj.say 的引用,并在全局作用域中独立调用。say 是一个普通函数,此时 say 内部的 this 遵循默认绑定,指向 windowf1 是一个箭头函数,它定义在 say 函数内部,所以 f1this 捕获的是 say 函数的 this,即 window。因此,console.log(1111", this) 输出 1111 window对象
  2. obj.say():这里 say 是作为 obj 的方法被调用,遵循隐式绑定,所以 say 内部的 this 指向 objf1 作为箭头函数,其 this 捕获的是 say 函数的 this,即 obj。因此,console.log("1111", this) 输出 1111 obj对象
  3. obj.pro.getPro()getPro 是一个箭头函数,它定义在对象字面量 pro 内部。然而,对象字面量不构成独立的作用域pro 对象本身没有 this。因此,getProthis 会继续向上捕获,直到找到最近的非箭头函数父级作用域的 this。在这个例子中,getPro 的定义上下文是全局作用域(因为它没有被包裹在任何普通函数中),所以它的 this 最终指向全局对象 window。因此,console.log(this) 输出 window对象

箭头函数的 this词法作用域this,即它在定义时就确定了,并且会穿透对象字面量,向上寻找最近的非箭头函数父级作用域的 this

this 的“排位赛”:绑定优先级大揭秘 🏆

当一个函数可能同时符合多种 this 绑定规则时,JavaScript 引擎会按照一个固定的优先级顺序来决定 this 的最终指向。理解这个优先级是彻底掌握 this 的关键所在。我们可以将 this 的绑定优先级总结为:

new 绑定 > 显式绑定 (call/apply/bind) > 隐式绑定 > 默认绑定

让我们逐一深入理解这个优先级链:

  1. new 绑定 (最高优先级)

    • 如果函数是通过 new 关键字调用的,那么 this 总是指向新创建的对象。这是因为 new 操作符在执行时会创建一个全新的对象,并将该对象绑定为函数内部的 this。这个绑定优先级最高,甚至可以覆盖 bind 带来的显式绑定。
  2. 显式绑定 (call/apply/bind)

    • 如果函数通过 call()apply()bind() 方法调用,那么 this 会被强制绑定到这些方法传入的第一个参数。其中,bind 创建的新函数,其 this 绑定优先级高于隐式绑定和默认绑定。
  3. 隐式绑定

    • 如果函数作为对象的方法被调用(即通过 obj.method() 的形式),那么 this 指向调用该方法的对象。
  4. 默认绑定 (最低优先级)

    • 如果以上所有规则都不适用,函数以独立形式调用,那么 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

解析:

  1. obj1.foo(2)foo 作为 obj1 的方法被调用,遵循隐式绑定this 指向 obj1,所以 obj1.a 被设置为 2console.log(obj1.a) 输出 2
  2. obj1.foo.call(obj2, 3):这里使用了 call 进行显式绑定foo 函数的 this 被强制绑定到 obj2。所以 obj2.a 被设置为 3console.log(obj2.a) 输出 3
  3. console.log(obj1.a):此时 obj1.a 仍然是 2,因为上一步的操作是针对 obj2 的,没有影响 obj1。输出 2
  4. var bar = new obj1.foo(4):这里使用了 new 关键字。new 绑定优先级最高。new 会创建一个新的对象 bar,并将 foo 函数内部的 this 绑定到 bar。所以 bar.a 被设置为 4console.log(bar.a) 输出 4

这个例子清晰地展示了隐式绑定、显式绑定和 new 绑定在不同调用方式下的优先级和效果。

bindnew 的优先级

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

解析:

  1. var bar = foo.bind(obj1)bind 方法返回了一个新的函数 bar,这个 bar 函数的 this 永远被绑定到 obj1
  2. bar(2):调用 bar 函数,由于 bar 已经通过 bind 绑定到 obj1,所以 this 指向 obj1obj1.a 被设置为 2console.log(obj1.a) 输出 2
  3. var baz = new bar(3):这里是关键!虽然 bar 已经通过 bind 绑定到 obj1,但是当 barnew 关键字调用时,new 绑定的优先级会高于 bind 带来的显式绑定。因此,new 会创建一个全新的对象 baz,并将 foo 函数内部的 this 绑定到 baz。所以 baz.a 被设置为 3
  4. console.log(obj1.a):此时 obj1.a 仍然是 2,因为 new bar(3) 的操作是针对新创建的 baz 对象的,没有影响 obj1。输出 2
  5. 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

解析:

  1. myObject.func()func 作为 myObject 的方法被调用,遵循隐式绑定,所以 func 内部的 this 指向 myObject。因此,console.log(this.foo) 输出 myObject.foo,即 "bar"
  2. var self = this;:在 func 函数内部,self 变量被赋值为当前的 this(即 myObject)。这是一个非常常见的技巧,用于在嵌套函数中“保存”外部 this 的指向。
  3. console.log(self.foo):由于 self 指向 myObject,所以输出 myObject.foo,即 "bar"
  4. (function() { ... }()):这是一个立即执行函数 (IIFE)。这个 IIFE 是一个普通的函数调用,它没有被任何对象“点”出来,也没有使用 callapplynew。因此,它遵循默认绑定。在非严格模式下,IIFE 内部的 this 指向全局对象 window。由于 window 对象上没有 foo 属性,所以 console.log(this.foo) 输出 undefined
  5. 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 的指向,就能迎刃而解。

  1. db1: (function(){ ... })()

    • 这是一个立即执行函数 (IIFE)。这个 IIFE 是在全局作用域中被定义的,并且是立即执行的。因此,IIFE 内部的 this 遵循默认绑定,指向全局对象 window
    • console.log(this):输出 window 对象。
    • this.number *= 4:此时 thiswindow,所以 window.number 变为 2 * 4 = 8
    • IIFE 返回了一个匿名函数 function(){ ... },这个匿名函数被赋值给了 obj.db1
  2. var db1 = obj.db1;

    • obj.db1(即 IIFE 返回的匿名函数)赋值给全局变量 db1
  3. db1()

    • 这里调用的是全局变量 db1,它是一个独立的函数调用,遵循默认绑定。因此,匿名函数内部的 this 指向全局对象 window
    • console.log(this):输出 window 对象。
    • this.number *= 5:此时 thiswindowwindow.number 变为 8 * 5 = 40
  4. obj.db1()

    • 这里调用的是 obj 对象的 db1 方法,遵循隐式绑定。因此,匿名函数内部的 this 指向 obj 对象。
    • console.log(this):输出 obj 对象。
    • this.number *= 5:此时 thisobjobj.number 变为 3 * 5 = 15
  5. console.log(obj.number):输出 obj.number 的最终值,即 15

  6. 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

解析:

  1. obj.method(fn, 1)method 作为 obj 的方法被调用,method 内部的 this 指向 obj。传入的参数是 fn 函数和数字 1
  2. fn():在 method 内部,fn 是一个独立的函数调用,遵循默认绑定。因此,fn 内部的 this 指向全局对象 windowwindow.length 的值是 10(全局变量 length),所以第一次输出 10
  3. arguments[0]():这里是关键!arguments 是一个类数组对象,它包含了函数被调用时传入的所有参数。arguments[0] 实际上就是 fn 函数。当通过 arguments[0]() 这种形式调用时,arguments 对象成为了 fn 函数的调用者。因此,fn 内部的 this 遵循隐式绑定,指向 arguments 对象。
    • arguments 对象有一个 length 属性,表示传入参数的数量。obj.method 函数被调用时传入了两个参数 (fn1),所以 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

解析:

  1. obj.foo()fooobj 的方法,遵循隐式绑定this 指向 objobj.a2,所以输出 2
  2. obj.bar()barobj 的方法,遵循隐式绑定bar 内部的 this 指向 obj。然而,在 bar 内部调用 printA() 时,printA 是一个独立的函数调用,遵循默认绑定this 指向全局对象 windowwindow.a1,所以输出 1
  3. var foo = obj.foo;:将 obj.foo(即 printA 函数)赋值给全局变量 foo
  4. foo():调用全局变量 foo,这是一个独立的函数调用,遵循默认绑定this 指向全局对象 windowwindow.a1,所以输出 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

解析:

  1. obj.getX()getXobj 的方法,遵循隐式绑定getX 内部的 this 指向 obj。然而,getX 内部返回的是一个立即执行函数 (function() { return this.x; })()。这个立即执行函数是一个独立的函数调用,遵循默认绑定。因此,其内部的 this 指向全局对象 windowwindow.x3,所以 console.log(obj.getX()) 输出 3
  2. obj.getY()getYobj 的方法,遵循隐式绑定getY 内部的 this 指向 objobj.y6,所以 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

解析:

  1. obt.fn()fn 作为 obt 的方法被调用,遵循隐式绑定this 指向 obtobt.a20,所以输出 20
  2. obt.fn.call():这里使用了 call 进行显式绑定。但是 call 方法没有传入任何参数,这等同于 call(undefined)。根据前面提到的规则,当 callapply 的第一个参数是 nullundefined 时,this 会被自动替换为全局对象 window(非严格模式)。window.a10,所以输出 10
  3. (obt.fn)():这里的括号 () 仅仅是改变了表达式的运算顺序,它不影响 fn 函数的调用方式。fn 仍然是作为 obt 的方法被调用,遵循隐式绑定this 指向 obtobt.a20,所以输出 20

总结与建议:彻底驯服 this 的秘诀 💡

通过以上对 this 的四大绑定规则、箭头函数的特殊性、严格模式的影响以及绑定优先级的深入剖析,相信你对 JavaScript 中 this 的魔幻指向已经有了更清晰的认识。this 确实是一个复杂但又极其重要的概念,它是 JavaScript 灵活性的体现,也是面试官考察你对语言底层理解的利器。

彻底掌握 this 的秘诀在于:

  1. 理解调用方式this 的指向完全取决于函数被调用的方式,而不是函数定义的位置。
  2. 熟记绑定规则:牢记默认绑定、隐式绑定、显式绑定和 new 绑定这四种规则。
  3. 区分箭头函数:箭头函数没有自己的 this,它继承自外层词法作用域。
  4. 掌握优先级:当多种规则可能同时生效时,记住 new > 显式 > 隐式 > 默认的优先级。
  5. 多实践,多调试:遇到 this 问题时,不要害怕,多动手写代码,多使用 console.log(this) 进行调试,逐步分析。

希望这篇博客能帮助你彻底驯服 this 这个“磨人精”,在面试中游刃有余,在实际开发中写出更健壮、更可预测的代码!如果你有任何疑问或想分享你的 this 学习心得,欢迎在评论区留言讨论!一起进步!🚀