JS语言理解07 this的理解

JavaScript里this非常重要,又很让我迷惑,需要反复的学习、体会。

什么是this

this是在运行的时候绑定的,并不是在编写的时候绑定,它的上下文取决于函数调用时候的各种条件。所以this的绑定和函数声明的位置没有关系,只与函数的调用方式有关系。this是上下文的一个属性。

上下文: 当一个函数调用的时候,会创建一个活动记录(上下文),这个记录会包含函数在哪里被调用,调用的方法,传入参数的信息。this就是这个记录(上下文)的一个属性

在全局函数中,this等于window,而当函数被视为某个对象的方法调用时,this等于那个对象。

this实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用

也就是说,只有函数才有this属性,其他的对象并没有这个属性

this的绑定规则

上面提到了,this是在函数被调用时发生的绑定,它指向什么完全取决于在哪里调用。所以说,我们的第一步就是判断函数的调用位置。

分析出了函数的调用位置之后,接下来就是要判断它是应用于哪种绑定规则,this有4种绑定规则:

  1. 默认绑定
  2. 隐式绑定
  3. 显示绑定
  4. new绑定

四种绑定规则的优先级从上到下依次递增,默认绑定的优先级最低,new绑定的优先级最高,而bind方法就是显示绑定的一种

(1) 默认绑定

独立函数调用时,this默认指向全局

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

foo();
// 2

这个例子中,函数的调用位置是全局环境,所以其指向全局即等于window(注意这是在非严格模式下),如果在严格模式下会输出undefined

(2)隐式绑定

调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。

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

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

obj.foo();
// 2

当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象中。当然,也有一些隐式绑定的函数丢失绑定对象的问题,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a = '全局中的a';

function foo() {
console.log(this.a);
}

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

var s = obj.foo;
s();
// 全局中的a;

s只是obj.foo的一个引用,而在当s调用的时候已经没有了上下文对象,因此将会默认绑定,从而输出全局中的a

(3)显示绑定

call/apply/bind方法,我们可以在调用时强制把函数的this绑定到某个对象上。

(4)new绑定

使用new关键字调用一个构造函数时,也会发生this的绑定,具体过程:

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

var bar = new foo(2);
console.log(bar.a);
//2

一种对this的理解方式

来理解一道题:

1
2
3
4
5
6
7
8
9
10
11
12
13
var obj = {
foo: function(){
console.log(this)
}
}

var bar = obj.foo

obj.foo()
// obj

bar()
// window

为什么是这样的结果呢?

先看 bar(),相当于是这样的调用:

1
bar.call(undefined)

在非严格模式下,如果传入的 context 是 context 或者 undefined,那么就默认是 window

而对象的调用 obj.foo(),相当于是这样的调用:

1
obj.foo.call(obj)

结果自然就是 obj

所以可以得出这样的结论:

  1. this 就是 call 一个函数时,传入的第一个参数
  2. 如果函数调用形式不是 call 形式,请转换为 call 形式再来看 this 指向谁

call/apply和函数执行的本质

当我们执行一个函数,一下集中调用方式等价:

1
2
3
4
5
6
7
8
9
10
"use strict"
function fn(a, b) {
console.log(this)
}

fn(1, 2)

// 等价于
fn.call(undefined, 1, 2)
fn.apply(undefined, [1, 2])

在严格模式下, fn里的this就是call的第一个参数,也就是undefined

在非严格模式下, call传递的第一个参数如果是undefined或者null,那this会自动替换为Window对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var obj = {
fn: function(a, b) {
console.log(this)
},
child: {
fn2: function() {
console.log(this)
}
}
}
obj.fn(1, 2)
// 等价于
obj.fn.call(obj, 1, 2)
obj.fn.apply(obj, [1, 2])
// 所以 this 是 obj

obj.child.fn2()
// 等价于
obj.child.fn2.call(obj.chid)
// 所以 this 是 obj.child

练习

以上就是原理,我们根据原理来发散做几道测试题

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
var name = '饥人谷';

var people = {
name: '若愚',
sayName: function() {
console.log(this.name)
}
};

var sayAgain = people.sayName;

function sayName() {
console.log(this.name)
};

sayName()
//解析:相当于 sayName.call(undefined)
//因为是非严格模式,所以 this 被替换成 Window,所以这里输出全局的 name 即 "饥人谷"

people.sayName()
//解析: 相当于 `people.sayName.call(people)`
//所以这里输出 `people.name` 即 "若愚"

sayAgain()
//解析: 相当于 `sayAgain.call(undefined)` ,
//因为是非严格模式,所以 this 被替换成 Window,所以这里输出全局的 name 即 "饥人谷"

第二道:(这道题有点意思)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var arr = []
for (var i = 0; i < 3 ; i++) {
arr[i] = function () {
console.log(this)
}
}
var fn = arr[0]

arr[0]()
// 解析: 因为函数是个特殊的对象,所以 arr 相当于
// {
// '0': function () {},
// '1': function () {},
// '2': function () {},
// length: 3
// }
// arr[0] 相当于 arr.0 (当然这种写法不符合规范)
// 所以 arr[0] 等价于 arr.0.call(arr), this 就是 arr

fn()
// 解析: 相当于 `fn.call(undefined)`, 所以 fn 里面的 this 是 Window

bind

bind的作用和callapply类似,区别在于使用上。bind的执行的结果返回的是绑定了一个对象的新函数

先看一个使用例子:

1
2
3
4
5
6
7
8
9
10
11
var obj = { name: '饥人谷' };

function sayName(){
console.log(this.name);
}

var fn = sayName.bind(obj) ;
// 注意 这里 fn 还是一个函数,功能和 sayName 一模一样,区别只在于它里面的 this 是 obj

fn()
// 输出: '饥人谷'

bind()方法创建了一个新的函数,在bind()被调用时,这个新函数的this会被设定为bind方法传入的第一个参数,其余的参数将作为新函数的参数供调用时使用。

从上面的定义来看,bind函数的功能包括:

  1. 改变原函数的this指向,绑定this
  2. 返回原函数的拷贝,并预传参数

要注意的,当使用new调用bind返回的参数时,bind绑定this失效。这是由于上面提到过的this绑定的优先级,new的优先级高于bind

箭头函数

箭头函数没有自己的this,它的this是继承而来,默认指向在定义它时,它所处的对象(宿主对象),而不是执行时的对象,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let app = {
fn1: function(a) {
console.log(this)
}
fn2 (a) {
consoel.log(this)
},
fn3: (a) = > {
console.log(this)
}
};

app.fn1();
// app

app.fn2();
// app

app.fn3();
// window

粗略一看,fn1fn2fn3貌似都一样,实际上fn1fn2完全等价,但fn3是有区别的

以上代码等同于

1
2
3
app.fn2.call(app)
app.fn2.call(app)
app.fn3.call(/* 它的上一级的 this */ window)

再看一道题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const app = {
init() {
const menu = {
fn1: () => {
console.log(this);
// 相当于 menu.fn1.call(/* menu 所在的环境下的 this */)
// 所以 fn1 里面的 this 也就是 app。
},
fn2 () {
console.log(this);
// 相当于 menu.fn2.call(/* menu */)
// 所以 fn1 里面的 this 也就是 app。
}
};
menu.fn1();

menu.fn2()
}
};
app.init()

再看一道题目:

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
const app = {
fn1() {
setTimeout(function () {
console.log(this)
}, 10)
},
fn2() {
setTimeout(() => {
console.log(this)
}, 20)
},
fn3() {
setTimeout((function () {
console.log(this)
}).bind(this), 30)
},
fn4: () => {
setTimeout(() => {
console.log(this)
}, 40)
}
};

app.fn1();
// fn.call(undefined) ,所以输出 window
// window

app.fn2();
// 箭头函数没有自己的 this,借用 setTimeout 外面的 this,也就是 app
// app

app.fn3();
// 创建了一个新函数,这个新函数里面绑定了外面的 this,也就是 app
// app

app.fn4();
// 箭头函数没有 this,用 setTimeout 外面的 this
// setTimeout 所在的 fn4 也是箭头函数,没有自己的 this,借用外面的 this ,也就是 window
// window

易犯错误

(1)this对象指向自身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo(num) {
this.count++;
}

var count = 0;
foo.count = 0;

for (var i = 0; i < 5; i++) {
foo(i)
}
console.log(foo.count);
// 0

console.log(count);
// 5

this并不是指向foo自身的,因此当输出foo.count值的时候,其依然是0。此时foo()作为一个全局函数被调用,this指向的window

这种错误是对this根本没有理解,this代表的是函数调用时的上下文环境,而不是从字面意义的理解,表示函数自己

(2)this指向自身的作用域

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

foo();
// 1

仍然是比较初级的错误,foo里面定义的var a是函数的局部作用于。

参考