函数中this的指向

8.8k 词

JavaScript函数中this的指向

[toc]

一、函数的基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义函数
function foo(name) {
console.log("foo函数:", this)
}

// 1.方式一: 直接调用
// foo()

// 2.方式二: 通过对象调起
var obj = { name: "why" }
obj.aaa = foo

obj.aaa()

二、this绑定规则

1. this绑定规则一:默认绑定

默认绑定规则描述

默认绑定是最基本的规则,当函数调用不符合其他绑定规则(如隐式绑定、显式绑定或者new绑定)时应用。在默认绑定下,函数的调用方式决定了this的值:

  • 非严格模式下,this默认绑定到全局对象(浏览器环境中是window对象)。
  • 严格模式下,this将绑定到undefined,防止不小心修改全局对象。

(1)普通的函数被独立调用

1
2
3
4
5
6
7
8
// "use strict"
// 定义函数
// 1. 普通的函数被独立调用
function foo() {
console.log("foo:",this)
}

foo() // 在非严格模式下,this指向全局对象window;在严格模式下,this为undefined。

当函数不在任何对象或类中直接调用时,this的值取决于是否在严格模式下运行。在非严格模式下,this会默认绑定到全局对象(浏览器中是window对象),在严格模式下,this会被绑定到undefined

(2) 函数定义在对象中,但独立调用

1
2
3
4
5
6
7
8
9
10
//2. 函数定义在对象中,但是独立调用
var obj = {
name: "why",
bar: function() {
console.log("bar:",this)
}
}

var baz = obj.bar
baz()

在这里,bar函数从其属于的对象obj中脱离出来,并被赋值给baz。当通过baz()独立调用时,this遵循默认绑定规则,即指向全局对象或在严格模式下为undefined

(3)高阶函数

1
2
3
4
5
6
7
8
9
10
11
12
// 3.高阶函数
var obj = {
name: "why",
bar: function() {
console.log("bar:",this)
}
}
function test(fn) {
fn()
}

test(obj.bar)// 传递obj.bar给函数test并调用

即使bar函数最初是obj对象的方法,当它被作为参数传递并在test函数中调用时,this再次遵循默认绑定,指向全局对象或在严格模式下为undefined

(4)严格模式下的默认绑定

严格模式下,独立调用的任何函数中的this值将被设置为undefined。这是一种防止函数意外修改全局对象的安全措施。

2. this绑定规则二:隐式绑定

__隐式绑定规则__:

  1. 调用位置:在确定this的绑定时,关键在于查找函数被调用的位置。具体来说,是查看函数调用时点符号.前面的对象是什么。
  2. 调用方式:如果一个函数是作为对象的方法被调用,那么this就指向这个对象。

在JavaScript中,隐式绑定是指当一个函数作为对象的属性被调用时,this自动指向这个对象。

1
2
3
4
5
6
7
8
9
function foo() {
console.log("foo函数:", this)
}

var obj = {
bar: foo
}

obj.bar() // 调用方式决定了this绑定到obj

在这个例子中,函数foo最初被定义为一个普通的函数。之后,它被赋值给对象objbar属性。当通过obj.bar()调用foo时,this关键字自动绑定到obj对象。这是因为foo是作为obj对象的bar属性被调用的。

__隐式绑定的失效情况__:

尽管隐式绑定似乎很直接,但在某些情况下,它可以失效,使this回到默认绑定(指向全局对象或undefined)。失效的常见情况包括:

  • 间接引用:如果将方法赋值给一个变量,并通过这个变量调用方法,隐式绑定将失效。

    1
    2
    var anotherFunc = obj.bar;
    anotherFunc(); // this不再指向obj,而是全局对象或在严格模式下为undefined
  • 传递给高阶函数:如果你将一个对象的方法作为回调函数传递给另一个函数,除非使用绑定工具(如.bind),否则原有的this绑定通常会丢失。

    1
    setTimeout(obj.bar, 100);  // 当bar被调用时,this通常不会指向obj

3. this绑定规则三:new绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
new绑定的步骤:
1.创建新的空对象
2.将this指向这个空对象
3.执行函数体中的代码
4.没有显示返回非空对象时, 默认返回这个对象
*/

function foo() {
this.name = "why"
console.log("foo函数:", this)
}

new foo()

在这个示例中:

  • new foo()创建了一个新对象。
  • thisfoo函数中指向了这个新创建的对象。
  • foo函数内部,这个新对象被赋予了一个名为name的新属性,并设为"why"
  • 函数执行了console.log("foo函数:", this);语句,打印了这个新对象,显示它现在包含一个name属性。
  • 由于函数没有返回任何值,所以new foo()的结果是新创建的对象。

4. this绑定规则四:显示绑定

在JavaScript中,显式绑定是一种通过特定的函数方法,如 .call(), .apply(), 和 .bind(), 来设定函数执行时this的值的技术。通过显式绑定,我们可以直接指定函数调用的上下文对象,不论函数在何处被声明或被调用。

__显式绑定的机制__:

显式绑定主要通过三种函数方法实现:

  1. **.call(thisArg, arg1, arg2, ...)**:调用一个函数,其this值被显式设置为call的第一个参数thisArg,其余参数依次传递给函数。
  2. **.apply(thisArg, [argsArray])**:与.call()类似,但接受一个参数数组作为函数执行时的参数。
  3. **.bind(thisArg[, arg1[, arg2[, ...]]])**:创建一个新函数,当这个新函数被调用时,其this值永久地被绑定为.bind()的第一个参数thisArg,其余参数将作为新函数的预设参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var obj = {
name: "why"
};

function foo() {
console.log("foo函数:", this);
}

// 使用.call()方法,将this显式绑定到obj对象
foo.call(obj); // 输出 "foo函数: { name: 'why' }"

// 将this显式绑定到数字123
foo.call(123); // 输出 "foo函数: Number {123}"

// 将this显式绑定到字符串"abc"
foo.call("abc"); // 输出 "foo函数: String {'abc'}"
  • **foo.call(obj)**:在这个调用中,this被显式设置为指向obj对象。因此,当foo执行时,它打印出this指向的对象,显示对象中的name属性。

  • foo.call(123) 和 **foo.call("abc")**:在这两个调用中,this不指向一个常规的对象。JavaScript将这些原始值(数值和字符串)转换为它们对应的包装对象(NumberString对象)。因此,函数内部的this实际上是一个对象,而这个对象是原始值的一个包装形式。

三、this绑定规则优先级

1. 绑定优先级概述:

JavaScript中this的绑定规则有四种主要形式,且它们具有不同的优先级:

  1. new绑定
  2. 显式绑定 (call, apply, bind)
  3. 隐式绑定 (作为对象的方法调用)
  4. 默认绑定 (独立函数调用)

2. 显式绑定高于隐式绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function foo() {
console.log("foo:", this)
}

// 测试一: apply/call高于默认绑定
var obj = { foo: foo };
obj.foo.apply("abc"); // this 绑定到 "abc"
obj.foo.call("abc"); // this 绑定到 "abc"

// 测试二: bind高于默认绑定
var bar = foo.bind("aaa");
var obj = {
name: "why",
baz: bar
};
obj.baz(); // this 仍然绑定到 "aaa", 即使是作为 obj 的方法调用

无论是.apply(), .call()还是.bind(),显式绑定的this值都会覆盖隐式绑定。

3. new 绑定高于隐式绑定

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log("foo:", this)
}

var obj = {
name: "why",
foo: function() {
console.log("foo:", this);
console.log("foo:", this === obj);
}
};
new obj.foo(); // this 指向新创建的对象,而不是 obj

即使foo函数作为obj的方法被定义和调用,new操作符创建了一个新的对象,并且this被绑定到这个新对象上。

4. new绑定高于bind

1
2
3
4
5
function foo() {
console.log("foo:", this);
}
var bindFn = foo.bind("aaa");
new bindFn(); // this 指向新创建的对象,而不是绑定的 "aaa"

尽管foo.bind()方法绑定到"aaa",但new bindFn()的调用创建了一个新的对象,this指向这个对象,显示new绑定优先级高于.bind()

5. bind和apply/call的比较

1
2
var bindFn = foo.bind("aaa");
bindFn.call("bbb"); // this 仍然绑定到 "aaa"

这个例子中,尽管.call()试图将this绑定到"bbb"bindFnthis已被永久绑定到"aaa",表明.bind()的效果是不可被后续的.call().apply()覆盖的。

6. 总结

this绑定的优先级从高到低依次是:new绑定、显式绑定(bind优先于call/apply)、隐式绑定、默认绑定。

四、this绑定之外的情况

1. 显式绑定nullundefined

当使用.call(), .apply(), 或 .bind()进行显式绑定时,如果第一个参数是nullundefined,则这些方法不会将this绑定到任何特定值上,而是采用默认绑定规则(非严格模式下绑定到全局对象,严格模式下绑定到undefined)。

1
2
3
4
5
6
7
function foo() {
console.log("foo:", this)
}

foo.apply("abc"); // "foo:", String {'abc'}
foo.apply(null); // "foo:", Window(浏览器环境下的全局对象)或 global(Node.js)
foo.apply(undefined); // "foo:", Window 或 global

这里,.apply("abc")this绑定到字符串”abc”的包装对象。但是,.apply(null).apply(undefined)则回落到默认绑定规则。在严格模式下,这些调用将导致this绑定到undefined

2.情况二:间接函数引用

间接引用发生时,函数虽然是从某个对象上取得,但由于赋值或其它操作,函数的调用已经与原对象分离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var obj1 = {
name: "obj1",
foo: function() {
console.log("foo:", this)
}
};
var obj2 = {
name: "obj2"
};

// obj2.foo = obj1.foo
// obj2.foo() // 正常隐式绑定,"foo:", obj2

(obj2.foo = obj1.foo)(); // "foo:", Window 或 global

(obj2.foo = obj1.foo)()的调用中,函数通过赋值操作被取得,并立即调用。这种调用方式使得函数的执行脱离了任何对象的上下文(默认绑定),因此在非严格模式下,this指向全局对象。这是因为赋值表达式(obj2.foo = obj1.foo)返回的是函数本身,并且这个返回值不持有原始的对象引用。

五、箭头函数

箭头函数在ECMAScript 6中引入,提供了一种更简洁的函数写法,主要用于匿名函数表达式。与传统的函数表达式相比,箭头函数具有几个特点,包括更短的语法、没有自己的thisargumentssupernew.target,这些值由外围最近一层非箭头函数决定。

箭头函数的注意事项:

  • 不适合用作方法:由于箭头函数没有自己的this,如果将箭头函数作为对象的方法,this将不会指向该对象。
  • 不能用作构造函数:箭头函数不能使用new关键字,因为它们没有[[Construct]]方法,不具备构造对象的能力。
  • 没有arguments对象:不能直接访问命名参数之外的参数,除非使用剩余参数语法。

1. 箭头函数基本使用

(1) 传统函数和箭头函数的对比:

1
2
3
4
5
6
7
8
9
10
11
12
// 1.函数传统写法
function foo1() {}
var foo2 = function(name, age) {
console.log("函数体代码", this, arguments)
console.log(name, age)
}

// 2.箭头函数完整写法
var foo3 = (name, age) => {
console.log("箭头函数的函数体")
console.log(name, age)
}

(2)forEach中使用箭头函数

1
2
3
4
5
6
// 3.箭头函数的练习
// 3.1. forEach
var names = ["abc", "cba", "nba"]
names.forEach((item, index, arr) => {
console.log(item, index, arr)
})
  • 箭头函数作为回调函数传递给forEach方法,可以直接访问item, index, 和 arr
  • 这种场景下箭头函数非常方便,因为它可以继承外围作用域的this

(3)setTimeout中使用箭头函数

1
2
3
4
// 3.2. setTimeout
setTimeout(() => {
console.log("setTimeout")
}, 3000)
  • setTimeout调用中使用箭头函数,无需担心this的指向问题,因为箭头函数不绑定this,可以安全地用在定时器和延时调用中。

2. 箭头函数的简写

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
34
35
36
37
38
39
var names = ["abc", "cba", "nba"]
var nums = [20, 30, 11, 15, 111]

// 1.优化一: 如果箭头函数只有一个参数, 那么()可以省略
names.forEach(item => {
console.log(item)
})
var newNums = nums.filter(item => {
return item % 2 === 0
})

// 2.优化二: 如果函数体中只有一行执行代码, 那么{}可以省略
names.forEach(item => console.log(item))

// 一行代码中不能带return关键字, 如果省略, 需要带return一起省略(下一条规则)
var newNums = nums.filter(item => {
return item % 2 === 0
})

// 3.优化三: 只有一行代码时, 这行代码的表达式结果会作为函数的返回值默认返回的
var newNums = nums.filter(item => item % 2 === 0)
var newNums = nums.filter(item => item % 2 === 0)


// 4.优化四: 如果默认返回值是一个对象, 那么这个对象必须加()
// 注意: 在react中我会经常使用 redux

var arrFn = () => ["abc", "cba"]
var arrFn = () => {} // 注意: 这里是{}执行体
var arrFn = () => ({ name: "why" })
console.log(arrFn())

// 箭头函数实现nums的所有偶数平方的和
var nums = [20, 30, 11, 15, 111]
var result = nums.filter(item => item % 2 === 0)
.map(item => item * item)
.reduce((prevValue, item) => prevValue + item)
console.log(result)

3. 箭头函数中this的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1.普通函数中是有this的标识符
function foo() {
console.log("foo", this);
}
foo(); // 在非严格模式下,this指向全局对象(浏览器中的window,Node.js中的global)
foo.apply("aaa"); // 显式绑定this到字符串"aaa"的包装对象



// 2. 箭头函数中不存在this
var bar = () => {
console.log("bar:", this);
}
bar(); // 箭头函数中的this继承自创建时的上下文
bar.apply("aaaa"); // .apply()和.call()无法改变箭头函数的this,它仍然指向创建时的上下文

箭头函数bar被定义在全局作用域中,因此其this指向全局对象(在浏览器中是window,在Node.js中可能是global或模块的exports对象),即便使用.apply().call()也无法改变this的指向。

4. this查找规则

1
2
3
4
5
6
7
8
9
10
11
var obj = {
name: "obj",
foo: () => {
var bar = () => {
console.log("bar:", this);
}
return bar;
}
}
var fn = obj.foo();
fn.apply("bbb"); // 嵌套的箭头函数中的this仍然指向创建时的上下文,不会被.apply()改变

在这个例子中,即使foo是作为obj的属性定义的,由于foo是箭头函数,其this并不指向obj,而是继承自其创建时的上下文(全局作用域)。因此,嵌套的箭头函数bar中的this同样指向全局作用域,而非obj或通过.apply()尝试指定的任何对象。

留言