this到底指向啥

this总是让人感到有些费解,在阅读一些代码时时不时就冒出来让人丈二和尚摸不着头脑。由于受到一些其他语言的影响,比如Java类中的this是引用自身的指针,曾让我一度认为一切皆对象的JavaScript中函数的this也指向函数本身,这是完全错误的!一个很简单的测试:

1
2
3
4
5
6
function test() {
console.log(this); // 输出 window 对象, 'use strict'模式下是undefined
this.x = 2;
}
test();
console.log(window.x); // 2, 'use strict'模式下TypeError

我们可以很明显地看到this指向的是全局window对象,而不是函数自己。实际上,this是在运行时进行绑定的,并不是在编写时绑定的,它的上下文取决于函数调用时的各种条件。this的绑定与函数声明的位置没有任何关系,只取决于函数的调用方式。

那这里就来梳理一下一个函数调用时,究竟发生了什么。一个函数被执行时,会创建一个执行环境 ExecutionContext ,函数的所有的行为均发生在此执行环境中,构建该执行环境时,JavaScript首先会创建 arguments 变量,其中包含调用函数时传入的参数。接下来创建作用域链。然后初始化变量,首先初始化函数的形参表,值为 arguments 变量中对应的值,如果 arguments变量中没有对应值,则该形参初始化为 undefined。如果该函数中含有内部函数,则初始化这些内部函数。如果没有,继续初始化该函数内定义的局部变量,需要注意的是此时这些变量初始化为 undefined,赋值操作需要在函数执行时才进行,这就是所谓的JavaScript的变量提升,我们总是说尽量不要直接定义 function ,会造成全局的变量污染,也是这个道理。最后为this变量赋值,会根据函数调用方式的不同,赋给this全局对象,当前对象等。至此函数的执行环境 ExecutionContext 创建成功,函数开始逐行执行,所需变量均从之前构建好的执行环境 ExecutionContext 中读取。下面例子包含了大多数情况:

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
function test(c) {
var d = 1;
console.log(a); // function a
console.log(b); // undefined
console.log(test2); // function test2
console.log(c); // function c
console.log(d); // 1
var a, b = 1;
console.log(b); // 1
function a() { return 1; }
b = function() { return 1; }
console.log(b); // function b
function c() { return 1; }
function d() { return 1; }
function test2() { return 1; }
}
test(3);

下面总结一下this的几种情况:

默认情况

默认的this与本文开头一致,为全局window对象,严格模式下为undefined,让我们再看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test() {
console.log(this); // 依然为window对象
}
var test3 = function() {
this.a = 1;
this.b = 2;
test2();
}
var test2 = function() {
this.a = 2;
this.b = 3;
test();
}
test3();
console.log(window.a); // 2
console.log(window.b); // 3

作为对象方法调用

在上下对象中调用,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
function foo() {
console.log(this);
}
function doFoo(fn) {
fn(); // 实际也是一种引用
}
function doFoo2(fn) {
fn.call(obj);
}
var obj = {
a: 2,
foo: foo
}
obj.foo(); // 输出 Object {a: 2}
var test = obj.foo; // 直接引用
test(); // 又输出window!
doFoo( obj.foo ); // 回调函数
doFoo2( obj.foo ); // 输出 Object {a: 2}

apply、call与this

我们可以用 Function.prototype.applyFunction.prototype.call

apply与call的作用是完全一样的,这是参数略有不同,fun.call(this, arg1, arg2, …),fun.apply(this, [arg1, arg2, …])

这里的this就是apply与call的第一个参数所指定的对象

1
2
3
4
5
6
7
8
9
function test() {
// 我利用apply方法,将一个空对象传入,{}即为函数的this
this.a = 1;
this.b = 2;
return this;
}
console.log(test.apply({}));
// Object {a: 1, b: 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
var test = {}
console.log('toString' in test); // true
console.log('push' in test); // false
console.log('sort' in test); // false
Array.prototype.push.apply(test, [1, 2, 98, 3, 98, 23, 56, 12, 12]);
// Object {0: 1, 1: 2, 2: 98, 3: 3, 4: 98, 5: 23, 6: 56, 7: 12, 8: 12, length: 9}
console.log(test);
function compare(value1, value2) {
if(value1 < value2) {
return 1;
} else if(value1 > value2) {
return -1;
} else {
return 0;
}
}
/*
sort是toString后进行字符串比对排序的,所以默认不按大小排序
Array.prototype.sort.call(test, compare);
*/
var ans = [].slice.call(test).sort(compare);
console.log(ans);
// [98, 98, 56, 23, 12, 12, 3, 2, 1]

new与this

JavaScript中的”构造函数”与其他语言完全不同。new只是作为语法糖,实际是在new时调用了构造函数。

new来调用函数时,会自动执行下列操作:

  • 创建一个全新的对象
  • 新对象执行原型的链接
  • 新对象绑定到函数调用的this
  • 自动返回这个新对象

这里的this指的就是新创建的对象

看一下下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test(a) {
this.a = a;
console.log(this);
// test {a: 2}
}
test.prototype.add = function() {
this.a++;
console.log(this);
// test {a: 3}
}
var bar = new test(2);
bar.add();

关于bind

在ES5中引入了Function.prototype.bind方法,将bind方法的第一个参数作为当前函数的执行上下文:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(!('bind' in Function.prototype)){
Function.prototype.bind = function(){
// 这里是对象方法的调用,this就是调用者,比如下面的例子中就是this.onClick
var fn = this,
// bind的第一个参数
context = arguments[0],
// arguments只是类数组对象,利用slice提取顺便转换
args = Array.prototype.slice.call(arguments, 1);
return function(){
// 利用apply,改变函数执行上下文
return fn.apply(context, args.concat(arguments));
}
}
}

情况1:

1
2
3
4
5
6
7
8
9
10
function MyClass(){
this.element = document.getElementsByTagName('div')[0];
this.element.addEventListener('click', this.onClick.bind(this), false); // this.onClick 绑定了this
}
MyClass.prototype.onClick = function(e){
console.log(this); // 输出 MyClass
};
var test = new MyClass();

情况2:

1
2
3
4
5
6
7
8
9
10
function MyClass(){
this.element = document.getElementsByTagName('div')[0];
this.element.addEventListener('click', this.onClick, false); // this.onClick 没有绑定了this
}
MyClass.prototype.onClick = function(e){
console.log(this); // 输出 this.element
};
var test = new MyClass();

这也让我明白了,利用ES6的React为何要在constructor中进行手工绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
constructor(props) {
super(props);
this.state = {
searchQuery: 'initSearchQuery'
};
// this绑定后,使handleChange内部this指向当前组件实例,不绑定的话,this为null
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
console.log('handleChange', event.target.value);
// 这里需要用到this
this.setState({
searchQuery: event.target.value
});
}

而在ES6中新增了箭头函数,似乎是默认做了bind绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
constructor(props) {
super(props);
this.state = {
searchQuery: 'initSearchQuery'
};
// this.handleChange = this.handleChange.bind(this);
}
handleChange = (event) => {
console.log('handleChange', event.target.value);
this.setState({
searchQuery: event.target.value
});
}

当然从本质上讲的话,其实是向函数内部隐式地传送了外部的this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ES6
function foo() {
setTimeout( () => {
console.log("id:", this.id);
},100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log("id:", _this.id);
}, 100);
}

今天在JS群里看到一道不错的笔试题,刚好来分享一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function fun() {
this.a = 0;
this.b = function(){
console.log(this.a);
}
}
fun.prototype = {
b: function() {
this.a = 20;
console.log(this.a);
},
c: function() {
this.a = 30;
console.log(this.a);
}
}
var my_fun = new fun(); // new绑定,fun()中的this指向本身
my_fun.b(); // b方法有两个,一个是my_fu本身,一个位于原型链上,先调用本身的,答案是0
my_fun.c(); // 本身没有,沿着原型链向上查找,30