学习JavaScript——this绑定

什么是 this

  this是当前执行上下文(global、function 或 eval)的一个属性。在非严格模式下,总是指向一个对象;在严格模式下可以是任意值。

  1. 无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this都指向全局对象window
1
2
// 在浏览器中, window 对象同时也是全局对象:
console.log(this); // window
  1. 在函数内部,this的值取决于函数被调用的方式。非严格模式下,函数内部的this指向window;严格模式下,如果进入执行环境时没有设置this的值,this会保持为undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 非严格模式
function f1() {
return this;
}
// 在浏览器中:
f1() === window;
// 在Node中:
f1() === globalThis;

// 严格模式
function f2() {
"use strict";
return this;
}
f2() === undefined; // true
  1. this不能在执行期间被赋值。

this 绑定规则

  this绑定有四大规则:默认绑定、隐式绑定、显式绑定、new 绑定。

默认绑定

  又可以叫函数调用,在没有其它规则的情况下默认使用的绑定规则,一般就是直接调用函数的情况。默认绑定一般绑定到window上,严格模式下绑定到undefined

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

  解析:直接调用时,this指向了window,所以this.a其实就是window.a,而函数外部执行的代码var a = 10也把a设置到了window对象上,所以最后输出的结果为10。也可以这么认为,foo()实际调用的是window.foo(),而 foo 函数内部的this指向的是window,所以this.a指向的是window.a,也就是10

1
2
3
4
5
6
7
"use strict";
function foo() {
var a = 1;
console.log(this.a);
}
var a = 10;
foo();

  解析:严格模式下this指向的是undefined,所以this.a实际上访问的是undefined.aundefined上不存在a,所以会报错。

1
2
3
4
5
6
7
8
9
10
11
var a = 10;
function f() {}
console.log(window.a); // 10
console.log(window.f); // f(){}

let b = 1;
const c = 2;
console.log(window.b); // undefined
console.log(window.c); // undefined
console.log(b); // 1
console.log(c); // 2

  解析:在 ES5 中,能在window上看到var命令和function命令声明的全局变量;在 ES6 中,全局对象的属性和全局变量脱钩,但为了保持兼容性,旧的不变,var命令和function命令声明的全局变量依旧可以在window对象上看到,而letconst声明的全局变量在window对象上看不到,可以直接访问。

隐式绑定

  又可以叫对象方法调用,即作为对象方法使用。既然是对象调用,那么就少不了对象的使用。

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
console.log(this.a);
}
var obj = {
a: 20,
foo: foo,
};
foo(); // undefined
obj.foo(); // 20

obj.a = 30;
obj.foo(); // 30

  解析:foo()运行时,相当于运行了window.foo(),函数内部的this指向的是window,而window上并没有属性a,所以结果为undefinedobj.foo()运行时,函数内部的this指向的是obj对象,所以this.a实际上指的是obj.a,结果为20。因为函数内部的this.a指向的是obj.a,所以给obj.a赋值为30,输出的结果也是30

显式绑定

  显示绑定就是通过内置的call()apply()bind()方法来主动改变函数中this的指向。三种方法的thisObj如果未传,那么Global对象被用作thisObj

call()

  语法:call([thisObj[,arg1[, arg2[, [,.argN]]]]])call从第二个参数开始所有的参数都是原函数的参数。

apply()

  语法:apply([thisObj[,argArray]])apply只接受两个参数,且第二个参数必须是数组,这个数组代表原函数的参数列表。

bind()

  语法:bind([thisObj[,arg1[, arg2[, [,.argN]]]]])bind只有一个函数,且不会立刻执行,只是将一个值绑定到函数的this上,并将绑定好的函数返回,因此需要再一次调用。

1
2
3
4
5
6
7
8
9
10
11
var o = {
a: 1,
};
function fn(b, c) {
console.log(this.a + b + c);
}

fn.call(o, 2, 3); // 6
fn.apply(o, [2, 3]); //6
var fn2 = fn.bind(o, 2, 3); // 需要再次调用
fn2(); // 6

new 绑定

  也叫构造函数调用。语法:new constructor[([arguments])]

1
2
3
4
5
6
7
var a = 0;
function Foo() {
this.a = 10; // 当作为构造函数时,这里可以看作 a: 10 的声明形式
}
var foo = new Foo();
console.log(foo.a); // 10,foo此时是一个对象而不是函数
console.log(Foo.a); // undefined

优先级

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

箭头函数的 this 绑定

  箭头函数体内的 this 对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。也就是说箭头函数是根据外部作用域来决定this的,且箭头函数的this绑定无法被修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
return () => {
console.log(this.a);
};
}
foo.a = 10;

// 1. 箭头函数关联父级作用域this
var bar = foo(); // foo默认绑定
bar(); // undefined

var baz = foo.call(foo); // foo 显性绑定
baz(); // 10

// 2. 箭头函数this不可修改,这边使用了上面的已经绑定了foo 的 baz
var obj = {
a: 999,
};
baz.call(obj); // 10

常见面试题

题目 1

1
2
3
4
5
6
7
8
9
10
11
12
var x = 10;
var obj = {
x: 20,
f: function () {
console.log(this.x); // 20,隐性绑定,这里 f 的this指向上下文 obj ,即输出 20
var foo = function () {
console.log(this.x);
};
foo(); // 10,默认绑定,这里this绑定的是window
},
};
obj.f();

题目 2

1
2
3
4
5
6
7
8
9
10
11
12
function foo(arg) {
this.a = arg;
return this;
}

var a = foo(1);
var b = foo(10);

console.log(a.a); // undefined
console.log(b.a); // 10
// 本题中所有变量的值,a = window.a = 10 , a.a = undefined ,
// b = window , b.a = window.a = 10

题目 3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var x = 10;
var obj = {
x: 20,
f: function () {
console.log(this.x);
},
};
var bar = obj.f;
var obj2 = {
x: 30,
f: obj.f,
};
obj.f(); // 20
bar(); // 10
obj2.f(); // 30

题目 4

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
function foo() {
getName = function () {
console.log(1);
};
return this;
}
foo.getName = function () {
console.log(2);
};
foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}

foo.getName(); // 2
getName(); // 4
foo().getName(); // 1
getName(); // 1
new foo.getName(); // 2
new foo().getName(); // 3
new new foo().getName(); // 3

题目 4 解析:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function foo() {
getName = function () {
console.log(1);
};
//这里的getName 将创建到全局window上
return this;
}
foo.getName = function () {
console.log(2);
};
//这个getName和上面的不同,是直接添加到foo上的
foo.prototype.getName = function () {
console.log(3);
};
// 这个getName直接添加到foo的原型上,在用new创建新对象时将直接添加到新对象上
var getName = function () {
console.log(4);
};
// 和foo函数里的getName一样, 将创建到全局window上
function getName() {
console.log(5);
}
// 同上,但是这个函数不会被使用,因为函数声明的提升优先级最高,所以上面的函数表达式将永远替换
// 这个同名函数,除非在函数表达式赋值前去调用getName(),但是在本题中,函数调用都在函数表达式
// 之后,所以这个函数可以忽略了

foo.getName(); // 2
// 下面为了方便,使用输出值来简称每个getName函数
// 这里的疑惑是在 2 和 3 之间,觉得应该是3 , 但其实直接设置
// foo.prototype上的属性,对当前这个对象的属性是没有影响的,如果要使
// 用的话,可以foo.prototype.getName() 这样调用 ,这里需要知道的是
// 3 并不会覆盖 2,两者不冲突 ( 当你使用new 创建对象时,这里的
// Prototype 将自动绑定到新对象上,即用new 构造调用的第二个作用)

getName(); // 4
// 这里涉及到函数提升的问题, 5 会被 4 覆盖,

foo().getName(); // 1
// 这里的foo函数执行完成了两件事, 1. 将window.getName设置为1,
// 2. 返回window , 故等价于 window.getName(); 输出 1
getName(); // 1
// 刚刚上面的函数刚把window.getName设置为1,故同上 输出 1

new foo.getName(); // 2
// new 对一个函数进行构造调用 , 即 foo.getName ,构造调用也是调用
// 该执行还是执行,然后返回一个新对象,输出 2 (虽然这里没有接收新
// 创建的对象但是可以猜到,是一个函数名为 foo.getName 的对象
// 且__proto__属性里有一个getName函数,是上面设置的 3 函数)

new foo().getName(); // 3
// new 是对一个函数进行构造调用,它直接找到了离它
// 最近的函数,foo(),并返回了应该新对象,等价于 var obj = new foo();
// obj.getName(); 这样就很清晰了,输出的是之前绑定到prototype上的
// 那个getName 3 ,因为使用new后会将函数的prototype继承给新对象

new new foo().getName(); // 3
// var obj = new foo();
// var obj1 = new obj.getName();
// 仔细看看, 就是上两题的合体吗,obj 有getName 3, 即输出3
// obj 是一个函数名为 foo的对象,obj1是一个函数名为obj.getName的对象