this的四种绑定规则,你都知道吗?

《你不知道的JavaScript》系列二——–this绑定详解

关于this的误解

1.指向自身

不管是JavaScript新手开发者还是之前已经接触过其他语言的开发者,都很容易将this理解为指向函数自身。下面简单的例子将证明this远远不止字面意思的这么简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo(num){
console.log("foo:"+num);
//希望用count来记录foo被调用的次数
this.count++;
}
foo.count = 0;
for(let i=0;i<5;i++){
foo(i);
}
//foo:0
//foo:1
//foo:2
//foo:3
//foo:4

//见证奇迹的时候到了
console.log(foo.count);//0 ———— WTF!!?

从上面代码的输出可以看出从字面意思来理解this是错误的,那如果我增加的count属性和预期的不一样,那我增加的count哪去了?看看下图就一目了然了

image-20240310165509618

2.指向函数的作用域

需要明确的是,this在任何情况下都不指向函数的词法作用域。在JavaScript内部,作用域确实是与对象类似,可见的标识符都是它的属性,但是作用域“对象”无法通过JavaScript代码访问,它存在与JavaScript引擎内部————《你不知道的JavaScript》

1
2
3
4
5
6
7
8
function foo(){
var a=2;
this.bar();
}
function bar(){
console.log(this.a);
}
foo();//ReferenceError: a is not defined

这段代码试图使用this来联通foo()和bar()的词法作用域,从而让bar()可以访问foo()作用域里面的变量a。这是不可能实现的,我们不能使用this来引用一个词法作用域内部的东西。

每当你想要把词法作用域和this的查找混合使用时,一定要提醒自己,这是无法实现的

this到底是什么

当一个函数被调用时,会创建一个活动记录(执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。

调用栈:为了到达当前执行位置所调用的所有函数

调用位置:在当前正在执行的函数的前一个调用中

this的四条绑定规则

1.默认绑定

独立函数调用时使用默认绑定规则,绑定到全局对象(非严格模式)或者undefined(严格模式)

1
2
3
4
5
function foo(){
console.log(this.a);
}
var a = 2;
foo(); // 2

分析上述代码:

①:在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,一次只能使用默认绑定,无法使用其他规则。
②:上述代码执行在非严格模式下,因此this绑定到了全局对象上
③:this.a被解析为全局变量a,打印2

Tips:只有在非严格模式下调用foo(),默认绑定才能绑定到全局对象

2.隐式绑定

当调用位置有上下文对象,或者说是否被某个对象拥有或者包含时,考虑隐式绑定。

这个说法非常抽象,具体还是需要通过代码来进行讲解:

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log(this.a);
}

var obj = {
a:2,
foo:foo
};

obj.foo(); // 2

在上述代码中,调用位置会使用obj上下文来引用函数,因此隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因此this.aobj.a是一样的

Tips:对象属性引用链中只有最顶层或者说最后一层会影响调用位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(){
console.log(this.a);
}

var obj2 = {
a:333,
foo:foo
};
var obj1 = {
a:222,
obj2:obj2
};

obj1.obj2.foo(); // 333
==隐式丢失==

隐式丢失是非常常见的this绑定问题,也就是说被隐式绑定的函数丢失绑定对象,应用默认绑定,将this绑定到全局对象(严格模式)或者undefined(非严格模式)上;

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
};
var bar = obj.foo;
var a = "糟糕,我是全局对象";
bar(); // "糟糕,我是全局对象"

虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。值得注意的是,参数传递就是一种隐式赋值,因此在回调函数中,绑定的this也是全局对象。

1
2
3
4
5
6
7
8
9
function foo(){
console.log(this.a);
}
var obj = {
a:2,
foo:foo
}
var a = "糟糕,我是全局对象";
setTimeout(obj.foo,1000); // "糟糕,我是全局对象"

3.显式绑定

使用call()、apply()直接指定this的绑定对象,称之为显式绑定。

显式绑定的两个应用:

(1)硬绑定

观察以下代码,无论之后如何调用函数bar,它总会手动在obj上调用foo

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(){
console.log(this.a);
}
var obj = {
a:2
}
var bar = function(){
foo.call(obj);
}
bar(); // 2
setTimeout(bar,100); // 2
//硬绑定的bar不可能再修改它的this
bar.call(window); // 2

由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置的方法Funtion.prototype.bind,bind()会返回一个硬绑定的新函数,它会将参数设置为this的上下文。

(2)API调用的上下文

第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常称之为“上下文”(context),作用和bind()一样,确保回调函数可以使用指定的this

1
2
3
4
5
6
7
8
9
10
function foo(){
console.log(el,this.id);
}
var obj = {
id : "awesome";
}

//调用foo的时候把this绑定到obj上
[1,2,3].forEach(foo,obj);
//1 awesome 2 awesome 3 awesome

4.new绑定

使用new来调用函数,会自动执行下面的操作:

  1. 创建一个全新的对象
  2. 这个新对象会被执行原型连接
  3. 这个新对象会绑定到函数调用的this
  4. 如果这个函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
1
2
3
4
5
function foo(a){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2

使用new来调用foo()时,我们会构建一个新对象并将它绑定到foo()调用的this上。

5.四条绑定规则的优先级

优先级:new绑定>显式绑定>隐式绑定>默认绑定

6.绑定例外

(1)被忽略的this

如果将null或者undefined作为this的绑定对象传入call、apply或者bind中时,这些值会被忽略,应用默认绑定规则

(2)间接引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(){
console.log(this.a);
}
var a = 2;
var o = {
a : 3,
foo : foo
};
var p = {
a : 4
};
o.foo(); // 3
p.foo = o.foo;
p.foo(); // 2

赋值表达式p.foo = o.foo;的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo。因此此处应用的是默认绑定

(3)箭头函数

箭头函数不适用this的四种绑定规则,而是根据外层作用域来决定this,并且箭头函数的绑定无法修改,new也不行!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo(){
return (a) => {
//this继承自foo()
console.log(this.a);
}
}
var obj1 = {
a:2
}
var obj2 = {
a:3
}
var bar = foo.call(obj1);
bar.call(obj2); // 2

7.总结

  1. 函数是否在new中调用?如果是的话this绑定的是新创建的对象

    var bar = new foo();

  2. 函数是否通过call、apply显式绑定或者bind硬绑定?如果是的话,this绑定的是指定对象

    var bar = foo.call(obj2);

  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是上下文对象

    var bar = obj1.foo()

  4. 如果以上都不是的话,使用默认绑定。在严格模式下,绑定到undefined,非严格模式下,绑定到全局对象

    var bar = foo()

需要注意的是有些调用,特别是赋值,可能会在无意中使用默认绑定标准。并且箭头函数不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this。

8.课后习题

1.
1
2
3
4
5
6
7
8
9
10
const object = {
message: 'Hello, World!',

getMessage() {
const message = 'Hello, Earth!';
return this.message;
}
};

console.log(object.getMessage()); // ??
2.
1
2
3
4
5
6
7
8
9
10
11
12
function Pet(name) {
this.name = name;

this.getName = () => this.name;
}

const cat = new Pet('Fluffy');

console.log(cat.getName());

const { getName } = cat;
console.log(getName());
3.
1
2
3
4
5
6
7
8
9
const object = {
message: 'Hello, World!',

logMessage() {
console.log(this.message);
}
};

setTimeout(object.logMessage, 1000);
4.

如何调用logMessage函数,让它打印 "Hello, World!" ?

1
2
3
4
5
6
7
const object = {
message: 'Hello, World!'
};

function logMessage() {
console.log(this.message);
}
5.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const object = {
who: 'World',

greet() {
return `Hello, ${this.who}!`;
},

farewell: () => {
return `Goodbye, ${this.who}!`;
}
};

console.log(object.greet());
console.log(object.farewell());
6.
1
2
3
4
5
6
7
8
9
10
11
12
13
var length = 4;
function callback() {
console.log(this.length);
}

const object = {
length: 5,
method(callback) {
callback();
}
};

object.method(callback, 1, 2);
7.
1
2
3
4
5
6
7
8
9
10
11
12
13
var length = 4;
function callback() {
console.log(this.length);
}

const object = {
length: 5,
method() {
arguments[0]();
}
};

object.method(callback, 1, 2);
答案
  1. 'Hello, World!'

    隐式调用,此时的this绑定object上

  2. 'Fluffy''Fluffy'

    当函数作为构造函数new Pet('Fluffy')调用时,构造函数内部的this等于构造的对象

    Pet构造函数中的this.name = name表达式在构造的对象上创建name属性。

    this.getName = () => this.name在构造的对象上创建方法getName。而且由于使用了箭头函数,箭头函数内部的this值等于外部作用域的this值, 即 Pet

    调用cat.getName()以及getName()会返回表达式this.name,其计算结果为'Fluffy'

  3. undefined

    object.logMessage作为回调函数实际上引用的是logMessage函数本身,因此此时的logMessage()其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。

  4. 显式绑定or硬绑定

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    message: 'Hello, World!'
    };

    function logMessage() {
    console.log(this.message); // logs 'Hello, World!'
    }

    // Using func.call() method
    logMessage.call(object);

    // Using func.apply() method
    logMessage.apply(object);

    // Creating a bound function
    const boundLogMessage = logMessage.bind(object);
    boundLogMessage();
  5. 'Hello, World!''Goodbye, undefined!'

    当调用object.greet()时,在greet()方法内部,this值等于 object,因为greet是一个常规函数。因此object.greet()返回'Hello, World!'

    但是farewell()是一个箭头函数,箭头函数中的this值总是等于外部作用域中的this值。

    farewell()的外部作用域是全局作用域,它是全局对象。因此object.farewell()实际上返回'Goodbye, ${window.who}!',它的结果为'Goodbye, undefined!'

  6. 4

    callback()是在method()内部使用常规函数调用来调用的。由于在常规函数调用期间的this值等于全局对象,所以this.length结果为window.length

    第一个语句var length = 4,处于最外层的作用域,在全局对象 window 上创建一个属性length

  7. 3