JavaScript中的this
实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
先来列举一下都有哪些函数调用方式:
- 普通函数调用
- 对象方法调用
- call()、apply() 调用
- bind()
- 构造函数调用
- ES6箭头函数
通俗来说,“函数被谁调用,this
就指向谁”。
普通函数调用
函数在全局作用域下运行;在非严格模式下,this
指向全局对象window
,在严格模式下,this
会变成undefined
。
function foo1(){
return this;
}
foo1() === window; // true
function foo2(){
'use strict';
return this;
}
foo2() === undefined; // true
对象方法调用
当以对象里的方法的方式调用函数时,它们的this
是调用函数的对象。
function foo() {
console.log(this); // {x: 1, foo: ƒ}
console.log(this.x); // 1
}
var obj = {
x: 1,
foo: foo
};
obj.foo();
上面代码中,调用foo
的是对象obj
,所以这里的this
指向对象obj
。
如果将对象里的foo
方法赋给一个变量:
function foo() {
console.log(this); // window
console.log(this.x); // undefined
}
var obj = {
x: 1,
foo: foo
};
var fn = obj.foo;
fn();
将obj.foo
赋给了fn
变量,而fn
是在全局作用域中运行,因此this
指向window
,由于window
下没有x
这个变量,所以this.x
为undefined
。
如果把obj.foo
传递给一个自己声明的函数。
function foo() {
console.log(this); // window
console.log(this.x); // undefined
}
var obj = {
x: 1,
foo: foo
};
// 自定义函数
function todoFn(cb) {
cb();
};
todoFn(obj.foo);
将obj.foo
传递给自定义函数作为回调函数执行。结果一样。因为它仍然是在全局作用域中运行。
最后来看看在setTimeout()
中又会输出怎样的结果。
function foo() {
console.log(this); // {x: 1, foo: ƒ}
setTimeout(function(){
console.log(this); // window
}, 0);
}
var obj = {
x: 1,
foo: foo
}
obj.foo();
第一个this
还是对象方法调用方式;但在setTimeout()
里的this
指向的是全局作用域window
。为什么会这样?
原因是,由setTimeout()
调用的代码运行在与所在函数完全分离的执行环境上,该函数是在全局作用域中执行的,所以this
就指向 window
。
思考一个问题。
上面代码的后三段代码中,都出现了同样的问题,就是它们的this
都丢失了。怎么做才能让它们的this
仍然指向原来的对象?
后面即将提到的call()
、apply()
及bind()
方法都能解决这一问题。
call()、apply() 调用
使用call()
或者apply()
可以改变上下文的this
值。
// 定义一个对象
var obj = {
x: 'local'
};
// 定义一个全局变量
var x = 'global';
// 定义函数,返回值取决于调用方式
function speak(){
return this.x;
}
speak(); // "global"
speak.call(obj); // "local"
speak.apply(obj); // "local"
直接调用sayName()
时,相当于普通函数调用,this
指向window
,值为global
;使用call()
或apply()
作用一致,都让this
指向了对象obj
。只不过在给call()
和apply()
传递参数时有区别,如:
function add(c, d) {
return this.a + this.b + c + d;
}
var obj = {
a: 1,
b: 2
};
// 第一个参数作为 this 使用的对象,而后续参数作为参数传递给函数
add.call(obj, 3, 4); // 10
// 第一个参数同样是作为 this 使用的对象
// 第二个参数是传递一个数组
add.apply(obj, [3, 4]); // 10
bind() 方法
Function.prototype.bind()
会创建一个新的包装函数,这个函数会忽略它当前的this
绑定(无论绑定的对象是什么), 并把我们提供的对象绑定到this
上。
var x = 9;
var obj = {
x: 81,
getX: function() {
return this.x;
}
};
// 相当于对象方法调用,this指向对象 o
obj.getX(); // 81
// 将对象 o 的方法getX赋值给 f,f 执行时是在全局环境中,this指向全局作用域
var f = obj.getX;
f(); // 9
// 创建一个新函数,将this绑定到对象 o 中
var f2 = obj.getX.bind(o);
f2(); // 81
接着上面【对象方法调用】里的几个丢失this
的例子。如何用call()
、apply()
或bind()
来绑定this
到指定的对象。
先看看这个例子:
function foo() {
console.log(this); // {x: 1, foo: ƒ}
console.log(this.x); // 1
}
var obj = {
x: 1,
foo: foo
};
// 使用call()或apply()
var fn1 = obj.foo;
fn1.call(obj);
//fn.apply(obj);
// 使用bind()
var fn2 = obj.foo.bind(obj);
fn2();
再看这个回调函数的例子:
function foo() {
console.log(this); // {x: 1, foo: ƒ}
console.log(this.x); // 1
}
var obj = {
x: 1,
foo: foo
};
// 自定义函数
function todoFn(cb) {
cb.call(obj);
// cb.apply(obj);
};
todoFn(obj.foo);
只需要在执行回调函数的时候使用call()
或apply()
将this
绑定到obj
对象即可。
最后看定时器的例子:
function foo() {
console.log(this); // {x: 1, foo: ƒ}
setTimeout(function(){
console.log(this); // {x: 1, foo: ƒ}
}.bind(this), 0);
}
var obj = {
x: 1,
foo: foo
}
obj.foo();
这里是使用了bind()
方法,将上下文的this
绑定到定时器里的匿名函数,它最终还是指向对象obj
。
构造函数调用
如果函数或者方法调用之前带有关键字new
,它就构成构造函数调用。使用new
来调用函数, 或者说发生构造函数调用时,会自动执行下面的操作。
- 创建(或者说构造) 一个全新的对象。
- 这个新对象会被执行 [[ 原型 ]] 连接。
- 这个新对象会绑定到函数调用的
this
。 - 如果函数没有返回其他对象, 那么
new
表达式中的函数调用会自动返回这个新对象。
来看一个例子:
function foo(x){
this.x = x;
console.log(this); // foo {x: 2}
}
var f = new foo(2);
console.log(f.x); // 2
使用new
来调用foo()
时, 会构造一个新对象并把它绑定到foo()
调用中的this
上。即构造函数的this
指向它实例化出来的对象。
箭头函数
箭头函数本身没有this
,是根据外层(函数或者全局)作用域来决定this
。
先来看一个例子:
var x = 1;
function testFn() {
var fn1 = function() {
console.log(this); // window
console.log(this.x); // 1
}
var fn2 = () => {
console.log(this); // window
console.log(this.x); // 1
}
fn1();
fn2();
}
testFn();
两段代码输出的结果一致。但运行的机制不一样。
-
fn1
是普通函数调用,前面说过普通函数调用this
指向window
; -
fn2
是箭头函数,它会继承外层函数testFn
的this
,而testFn
是在全局作用域中运行的,所以它的this
是window
,因此fn2
箭头函数的this
也是window
。
对上面对例子做一点改动:
var x = 1;
function testFn() {
var fn1 = function() {
console.log(this); // window
console.log(this.x); // 1
}
var fn2 = () => {
console.log(this); // {x: 2}
console.log(this.x); // 2
}
fn1();
fn2();
}
testFn.call({ x: 2 });
改变了testFn
的调用方式。
-
fn1
依然是普通函数调用,this
指向window
; -
fn2
箭头函数依然是继承它外层函数testFn
的this
,但这里testFn
通过call
使this
指向了对象{id: 2}
,所以fn2
的this
也指向该对象。
接着上面【对象方法调用】里的setTimeout
例子。如果使用箭头函数会怎样?
function foo() {
console.log(this); // {x: 1, foo: ƒ}
console.log(this.x); // 1
setTimeout(() => {
console.log(this); // {x: 1, foo: ƒ}
console.log(this.x); // 1
}, 0);
}
var obj = {
x: 1,
foo: foo
}
obj.foo();
这里定时器回调的this
由它外层函数foo
的作用域决定,而foo
是由obj
调用,所以它的this
指向对象obj
,所以定时器的this
也指向对象obj
。
另外,由于箭头函数没有自己的this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。
小结
JavaScript的this
取决于被调用的方式。
- 如果是普通函数调用方式。非严格模式下,
this
指向window
,严格模式下,this
是undefined
; - 如果是对象调用方式,
this
指向该对象; - 如果是
call()
、apply()
或bind()
方式调用,this
指向被绑定的对象; - 如果是构造函数调用方式,
this
指向实例化出来的新对象; - 如果是箭头函数,是根据当前的词法作用域来决定
this
, 具体来说,箭头函数会继承外层函数调用的this
绑定。
原文地址:https://github.com/daijingfeng/blog/issues/2
参考资料:
1、https://developer.mozilla.org/zh-CN/docs/Web/API/Window/setTimeout
2、https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions
3、http://es6.ruanyifeng.com/#docs/function#箭头函数
4、《JavaScript权威指南(第6版)(中文版)》
5、《你不知道的JavaScript(上卷)》