this 关键字原理

this 是指包含它的函数作为方法被调用时所属的对象

  • 简而言之, this 代表存在于函数的一个对象,而这个对象就是调用这个函数的那个对象

  • this 的值取决于函数调用时的上下文运行时绑定

  • 因为函数是在一定的环境中运行的,调用函数时肯定需要知道是谁调用的,就用到了this进行指向

严格模式与非严格模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 严格模式
function logThis() {
"use strict";
console.log(this);
}

[1, 2, 3].forEach(logThis); // undefined、undefined、undefined

// 非严格模式:在浏览器上下文中
function logThis2() {
console.log(this);
}

[1, 2, 3].forEach(logThis2); // window、window、window

使用场景

全局上下文

全局执行上下文中(即,在任何函数之外),this 无论在严格模式还是非严格模式下,都引用全局对象

在 web 浏览器中,全局对象是 window

在 Node.js 环境中,全局对象不是 window 而是 global

在 web 浏览器中,全局对象是 window,所以 this 将引用 window 对象:

1
console.log(this); // 在浏览器上下文中会记录 "[object Window]"

函数上下文

普通函数

在普通函数内部,this 的值取决于函数的调用方式。如果函数在全局上下文中调用this 在严格模式下将为 undefined,在非严格模式下将引用全局对象

1
2
3
4
5
6
7
8
var name = 'globalName';
function func() {
var name = 'varName';
console.log(this.name); // this.name => 'globalName'
console.log(this); // Window {frames: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …}
}

func(); // this 在非严格模式的浏览器上下文中记录 "[object Window]",在严格模式下会记录 "undefined"

函数充当对象的方法

当函数充当对象的方法时,this 将引用调用该方法的对象。这展示了 this 的值不绑定于函数本身,而是由函数被调用的方式和位置决定,这个概念称为执行上下文

一层作用域链

this 指的该对象

1
2
3
4
5
6
7
8
9
var prop = 'globalHello';
var obj = {
prop: "Hello",
func: function() {
console.log(this.prop);
}
}

obj.func(); // 记录 "Hello"
多层作用域链

this 指的是距离方法最近的一层对象

1
2
3
4
5
6
7
8
9
10
11
12
var prop = "globalHello";
var obj = {
prop: "Hello",
obj1:{
prop: "innerHello",
func: function() {
console.log(this.prop); // this.prop => "innerHello"
}
}
}

obj.obj1.func(); // 记录 "innerHello"

注意:如果 obj.obj1.func 这个结果赋值给一个变量 obj2,则 obj2() 的值是多少呢?

1
2
var func1 = obj.obj1.func;
func1(); // this.prop => "globalHello"

答案是 “globalHello”,这个是因为经过赋值操作时,并未发起函数调用obj2()这个才是真正的调用,而 **发起这个调用的是根对象 **window ,所以 this 指的就是 window

匿名函数自调【立即调用(IIFE)】

this 指向的是全局对象

即便在多层作用域链当中,也指向全局对象

注意:在 立即调用(IIFE) 当中

1
2
3
4
5
var obj = {
func: (function(){
console.log(this.prop);
})()
}

此时的 obj.func不是一个函数,而是立即执行函数(IIFE)的结果

1
2
3
4
5
6
7
8
9
10
var prop = "globalHello";
var obj = {
prop: "Hello",
func: (function(){
console.log(this.prop); // this.prop => "globalHello"
})()
}

obj.func; // obj.func 实际上并不是一个函数,而是立即执行函数(IIFE)的结果。
// 因为在对象 obj 中定义 prop 属性时,prop 直接被赋值为一个立即执行函数的返回值,而不是一个函数引用。

立即调用的匿名函数有几种常见写法

  • 传统的括号包裹法

    • 这是最常见的 IIFE 写法,通过在函数表达式外面加一对圆括号 (),然后再在最后加一对圆括号 () 来调用它。
    1
    2
    3
    4
    // 先用()包起来,然后再后面跟 (参数) 
    (function(data) {
    console.log(data);
    })("111");
  • 通过 () 直接调用

    • 这种方式与第一种非常相似,只是将调用部分的括号放在函数表达式内部。
    1
    2
    3
    4
    先后面跟(参数),然后再()包起来
    (function(data) {
    console.log(data);
    }("222"));
  • 使用一元操作符

    • 可以使用一元操作符 !+-~ 来触发函数的立即调用。尽管不常用,但这是合法的 JavaScript 语法。
    1
    2
    3
    4
    5
    6
    7
    8
    // 正常函数格式,前面加 一元操作符
    !function(data) {
    console.log(data);
    }("333");

    +function(data) {
    console.log(data);
    }("333");
  • 通过 void 操作符

    • 使用 void 操作符也可以立即调用匿名函数。void 操作符返回 undefined,因此常用于不关心返回值的场景。
    1
    2
    3
    4
    // 正常函数格式,前面加 void
    void function(data) {
    console.log(data);
    }("444");
  • 自执行箭头函数(ES6)

    • 在 ES6 中,也可以用箭头函数来创建 IIFE,不过由于箭头函数的语法特点,需要注意将整个函数表达式包裹起来。
    1
    2
    3
    ((data) => {
    console.log(data);
    }) ("111")

定时器中调用

this 指向的是全局变量

其实定时器的本质,也是一种匿名函数的形式。

1
2
3
4
5
6
var prop = "globalHello";
var obj = setInterval(function(){
prop: "Hello",
console.log(this.prop); // // this.prop => "globalHello"
clearInterval(cat);
}, 1000);

箭头函数调用

箭头函数不具有自己的 this。相反,它们从创建时非箭头函数的父作用域继承 this

在其周围的作用域上创建一个 this 值的闭包:

  • 如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this
  • 否则 this 的值则被设置为全局对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var prop = "globalHello";
var obj = {
prop: "Hello",
func1: function(){
// var self = this;
var anonymousFunc = () => {
// console.log(self.prop); // 与下面相等
console.log(this.prop); // 绑定到 function() 的 this。即函数充当对象的方法时,this 引用调用该方法的对象
}
anonymousFunc();
},
func2: () => {
console.log(this.prop); // 未被非箭头函数包含,this设置为全局对象
}
}
obj.func1(); // "Hello"
obj.func2(); // "globalHello"

回调函数调用

this 指向的全局对象

因为本质上是在函数内 callback ,是以普通函数调用的方式执行的。【存疑,网上并未找到直接相关结果。GPT给出的答案】

下面的 ”勘误“ 部分给出了具体情境的不同详细例子,希望能有所帮助。

勘误

有些博主文章说:this 指向的执行上下文并 不是回调函数定义时 的那个上下文,而是 调用它的函数 所在的上下文。

乍一看有点道理,但是实际代码验证一下就发现不对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var funObj = {
excuteCallbackInner: function(callback){
callback();
},
callback: function(){
console.log("funObj.callback's this is: " + this);
}
}
function excuteCallback(callback) {
callback();
}

var obj = {
data: 100,
func: function () {
// 第1个例子
excuteCallback(function () {
console.log("global excuteCallback" + this); // global excuteCallback[object global/window]
})
// 第2个例子:排除 调用者 上下文的影响
funObj.excuteCallbackInner(function () {
console.log("funObj.excuteCallbackInner" + this); // funObj.excuteCallbackInner[object global/window]
})
// 第3个例子:排除 回调函数自身 上下文的影响
excuteCallback(funObj.callback); // funObj.callback's this is: [object global/window]
// 第4个例子:排除 回调函数自身 和 调用者 上下文的影响
funObj.excuteCallbackInner(funObj.callback); // funObj.callback's this is: [object global/window]

// 函数充当对象的方法,this 指向距离最近的对象
funObj.callback(); // funObj.callback's this is: [object Object] 【this => funObj】
}
}
obj.func();
  • 所有的例子都在 obj.func() 内部完成。可以看到所有回调函数当中的 this 都指向全局对象

  • excuteCallback() :该全局函数接受一个匿名回调函数的参数并直接调用它。

    • 符合 “this指向调用它的函数所在的上下文” 的说法。
    • 此时的 excuteCallback() 函数处于全局上下文,因此 this 指向 全局对象
  • funObj.excuteCallbackInner():该方法中的函数接受一个匿名回调函数的参数并直接调用它。

    • 不符合 “this指向调用它的函数所在的上下文” 的说法。
    • 排除 调用者 上下文的影响
    • 此时调用它的函数应该是 funObj.excuteCallbackInner() ,调用者内 this 指向 funObj 对象的上下文。
    • 然而回调函数当中 this 仍然指向全局对象
  • excuteCallback(funObj.callback) :该全局函数接受一个 funObj.callback的参数 并直接调用它。

    • 排除 回调函数自身 上下文的影响
    • 此时传入的参数为 funObj 对象的函数,回调函数自身内部 this 指向 funObj 对象的上下文。【严格来说此时还不叫回调函数】
    • 然而回调函数当中 this 仍然指向全局对象
  • funObj.excuteCallbackInner(funObj.callback):该方法中的函数接受一个 funObj.callback的参数 并直接调用它。

    • 排除 回调函数自身 和 调用者 上下文的影响
  • funObj.callback() :函数充当对象的方法,this 指向距离最近的对象。详见上文。

得出结论:无论从什么角度,都可以说明回调函数当中 this 指向全局对象

如有异议,欢迎联系博主讨论。

this指向问题解决
  • 使用箭头函数:利用箭头函数的继承机制【绑定最近一层非箭头函数的 this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function excuteCallback(callback) {
    callback();
    }
    var obj = {
    data: 100,
    func: function () {
    excuteCallback(() => console.log(this)); // this => { data: 100, func: [Function: func] }
    }
    }
    obj.func();
    • 箭头函数内的 this 绑定成了 function() 内的 this指向对象 obj
  • 使用 var self = this :在回调函数调用之前,保存一下 this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function excuteCallback(callback) {
    callback();
    }
    var obj = {
    data: 100,
    func: function () {
    var self = this;
    excuteCallback(function(){
    console.log(self); // self => { data: 100, func: [Function: func] }, this => global
    });
    }
    }
    obj.func();
    • 使用变量 self 提前保存 this 以供后续调用。
  • 使用 .bind() 方法永久绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function excuteCallback(callback) {
    callback();
    }
    var obj = {
    data: 100,
    func: function () {
    excuteCallback(function(){
    console.log(this); // this => { data: 100, func: [Function: func] }
    }.bind(this));
    }
    }
    obj.func();
    • 在参数传入之前,对匿名回调函数进行处理
    • 使用 .bind() 方法永久绑定 该回调函数内部 的 this

构造函数上下文

this 引用实例化的新对象

在构造函数内部,this 引用新创建的对象。但是,这里的“新创建”是什么意思呢?要理解这一点,我们需要探讨 JavaScript 中的 new 关键字。

当你在函数调用之前使用 new 时,它告诉 JavaScript 进行四个操作:

  1. 创建一个新的空对象。这不是一个函数、数组或 null,只是一个空对象。
  2. 使函数内部的 this 引用这个新对象。新对象与构造函数内的 this 关联起来。这就是为什么 Person(name) 内的 this.name 实际上修改了新对象。
  3. 正常执行函数。它像通常情况下执行函数代码一样执行。
  4. 如果函数没有返回自己的对象,则返回新对象。如果构造函数返回一个对象,那个对象将被返回,而不是新对象。如果返回其他任何内容,将返回新对象
1
2
3
4
5
6
7
8
9
var name = "globalName";
function Person(name) {
// 当使用 `new` 调用时,这是一个新的、空的对象
this.name = name; // `this` 现在有一个 `name` 属性
// 函数结束后,将返回 `this`,因为没有其他对象被函数返回
}

var p = new Person('Fred'); // `p` 现在是函数 `Person` 返回的对象,包含一个值为 'Fred' 的 `name` 属性
console.log(p.name); // 记录 "Fred"

原型方法上下文

this 指向调用该方法的实例

1
2
3
4
5
6
7
8
9
10
function Person(name) {
this.name = name;
}

Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};

const person = new Person('Alice');
person.sayHello(); // 输出: Hello, my name is Alice

类上下文

在类中,方法内部的 this 引用类的实例

1
2
3
4
5
6
7
8
9
10
11
12
class ExampleClass {
constructor(value) {
this.value = value;
}

logValue() {
console.log(this.value); // this.value => 类的实例的 value 属性
}
}

const exampleInstance = new ExampleClass('Hello');
exampleInstance.logValue(); // 记录 "Hello"

显式绑定 this

可以使用函数上的 .call().apply().bind() 方法来明确设置 this 的上下文

callapply 是函数的内置方法,显式的修改了prototype原型,用于调用一个函数并显式地设置该函数内部的 this。这两个方法的功能非常相似,区别在于它们处理参数的方式不同

  • .call() 方法

    • 语法function.call(thisArg, arg1, arg2, ...)
    • 用途call 方法可以调用一个函数,并且可以指定 this 的值,还可以直接传递多个参数
  • .apply() 方法

    • 用于调用一个函数,并显式设置 this 的值,同时传递参数。

    • 语法function.apply(thisArg, [argsArray])

    • 用途apply 方法与 call 类似,但不同的是,apply 接受一个包含多个参数的数组,而不是单独传递参数。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      //此处声明若用let,则第一个调用函数输出undefined,
      //因为此处let声明的变量虽然也是全局变量但其不会成为全局对象window的属性,故say()直接调用时为undefined
      var word = "我是window";

      function say(params1,params2){
      console.log(params1 + " " + params2 + ","+ this.word)
      }
      let obj = {
      word: "我是obj"
      }
      let newObj= {
      word: "我是newObj"
      }
      say("Hi","friend"); // Hi friend,我是window
      say.call(obj,"Hi","friend") // Hi friend,我是obj
      say.apply(newObj,["Hi","friend"]) // Hi friend,我是newObj
  • .bind() 方法

    • 语法function.bind(thisArg, arg1, arg2, ...)
    • 用途返回一个新的函数,这个新函数的 this 永远绑定到提供的值,并且可以预设部分参数
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var word = "我是window";
    function say(params1,params2){
    console.log(params1 + " " + params2 + ","+ this.word)
    }
    let obj = {
    word: "我是obj"
    }
    var bindSay = say.bind(obj, "Hi"); // "a1" 是预设的参数,会在 bindFunc 被调用时作为第一个参数传递给 func。
    bindSay("friend"); // Hi friend,我是obj

    let obj1 = {
    word: "我是obj1"
    }
    // 尝试使用 call() 方法更改上下文;但是,它仍然使用 obj 作为 `this` 上下文。并且预设参数已经确定
    console.log(bindSay.call(obj1, "Hi", "friend")); // Hi Hi,我是obj

另外的,.apply() 方法有一些典型的使用场景

  • 借用对象的方法

    • apply()person.greet 使用 anotherPersonthis 上下文。
    1
    2
    3
    4
    5
    6
    7
    8
    const person = {
    name: 'Alice',
    greet: function(greeting, punctuation) {
    return `${greeting}, ${this.name}${punctuation}`; // 模板字符串
    }
    };
    const anotherPerson = { name: 'Bob' };
    console.log(person.greet.apply(anotherPerson, ['Hello', '!'])); // Hello, Bob!
  • 应用于函数参数的转换

    • apply 可以方便地将数组元素作为函数参数传递,常用于需要将参数数组展开的情况,比如 Math.max

      1
      2
      const numbers = [1, 2, 3, 4, 5];
      const max = Math.max.apply(null, numbers); // 输出: 5
    • 可以用于合并数组

      1
      2
      3
      4
      5
      6
      7
      8
      9
      var array1 = [1, 2, 3];
      var array2 = [4, 5, 6];
      var mergedArray1 = array1.push.apply(array1, array2); // 第一个参数是 this 值,即 push 方法的上下文
      console.log(mergedArray1)

      // 其他合并方式: 使用拓展运算符 ...
      var mergedArray2 = [...array1, ...array2];
      console.log(mergedArray2); // mergedArray2 => [1, 2, 3, 4, 5, 6]

事件处理程序上下文

this 引用附加了事件监听器的元素,与 event.currentTarget 相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<button class="button">onclick</button>
<ul class="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>

<script>
var button = document.querySelector('button'); // 选择第一个 <button> 元素
button.onclick = function(ev){
console.log(this);
console.log(this === ev.currentTarget); // true
}
var list = document.querySelector('.list'); // 通过类名选择
list.addEventListener('click', function(ev){
console.log(this === list); // true
console.log(this === ev.currentTarget); // true
console.log(this);
console.log(ev.target);
}, false);
</script>

注意!

它不引用常用的 event.target 属性。让我们澄清 event.currentTargetevent.target 之间的区别。

  • event.currentTarget:该属性引用附加了事件处理程序(如 addEventListener)的元素。这是在事件处理程序函数的上下文中 this 引用的内容。

  • event.target:该属性引用引发事件的实际 DOM 元素对于会冒泡的事件特别重要。如果你点击内部元素,事件将冒泡到外部元素,触发它们的事件监听器。对于这些外部元素,event.target 将是实际被点击的最内层元素,而 event.currentTarget(或 this)将是当前处理程序附加到的元素。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <div id="outer">点击我
    <div id="inner">或者点击我</div>
    </div>

    <script>
    document.getElementById('outer').addEventListener('click', function(event) {
    console.log("currentTarget: ", event.currentTarget.id);
    console.log("this: ", this.id);
    console.log("target: ", event.target.id);
    });
    </script>
    • 在这种情况下,如果你点击外部 div,所有三个日志都将打印 “outer”,因为点击的元素(target)和处理程序附加的元素(currentTargetthis)是相同的。
    • 但是,如果你点击内部 div 中的 “或者点击我” 文本,event.target 将是 “inner”(因为这是你点击的元素),而 event.currentTarget(或 this)仍将是 “outer”(因为这是事件处理程序附加的元素)。

this 指针判定

这段代码会输出什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var funObj = {
data: 200,
func: function(){
console.log(this.data);
}
}

var obj = {
data: 100,
func: function(){
funObj.func();
}
}

funObj.func(); // 输出什么
const o = funObj.func;
o() // 输出什么

obj.func(); // 输出什么
const f = obj.func;
f(); // 输出什么

Output:
200
undefined
200
200

代码解析

  • funObj.func();

    • 这里直接调用了 funObj.func(); 方法。
    • 由于 func 是通过 funObj 调用的,因此 this 指向 funObj 对象。
    • this.data 访问的是 funObj 中的 data 属性,其值为 200
    • 输出: 200
  • const o = funObj.func; o();

    • 这里将 funObj.func 赋值给了变量 o,然后调用了 o()
    • o() 被独立调用时,this 不再指向 funObj,而是指向全局对象(在非严格模式下)或 undefined(在严格模式下)。
    • 在浏览器环境中,如果 this 指向全局对象,全局对象的 dataundefined(因为全局对象通常没有 data 属性)。
    • 输出: undefined
  • obj.func();

    • 这里调用了 obj.func 方法。
    • obj.func() 内部调用 funObj.func(),而 funObj.func() 是通过 funObj 调用的,因此 this 仍然指向 funObj 对象。
    • 输出: 200
  • const f = obj.func; f();

    • 这里将 obj.func 赋值给了变量 f,然后调用了 f()
    • 由于 f() 被独立调用,因此在 obj.func()this 指向全局对象或 undefined(在严格模式下)。
    • 然而,func 内部调用 funObj.func(),此调用依然通过 funObj 对象,因此 funObj.func 内部的 this 指向 funObj 对象。
    • 输出: 200