this
问题是JavaScript中最复杂的问题之一,即使是有经验的程序员也会在不经意间掉进this
关键字的坑里。每当定义一个函数时,this
会被自动定义在这个函数里面(即使在函数内部没有去使用this
关键字)。
理解this
之前,先了解一下函数的调用位置。函数的调用位置是函数被调用时所处的位置,不是函数声明的位置。为了理解调用位置,重要的是分析一个函数的调用栈,调用栈是函数执行的一个链表,可以通过以下几个例子,理解调用栈和调用位置。
function f1() {
// 当前调用栈:f1
f2(); // f2的调用位置
console.log("f1");
}
function f2() {
// 当前调用栈为 f1 -- >f2
f3(); // f3的调用位置
console.log("f2");
}
function f3() {
// 当前调用栈为 f1 --> f2 --> f3
console.log("f3");
}
f1();
我们定义了三个函数f1
、f2
和f3
,执行f1()
,因为在f1
内部调用了f2
,f2
内部又调用了f3
,所以程序会相继执行f2()
和f3()
,在执行f1
时调用栈为f1
,在f2
执行时,调用栈为f1 --> f2
,在f3
执行时,调用栈为f1 --> f2 --> f3
,所以调用栈类似一个单链表,从表头依次执行到表尾。
一些调试工具,会有专门的调用栈面板,显示当前程序执行的调用栈。在谷歌浏览器中,你可以按F12
打开开发者工具,切换到Sources
页面,你可以看到call stack
面板,这里会显示程序执行时的调用栈。
下面,我总结了四条关于this
的绑定规则,在应用以下规则之前,需要先分析调用栈,找到函数的调用位置,然后判断应用下面的四条规则之一。
这是this
绑定的默认规则,在无法应用其他规则时,使用该规则。
function f() {
console.log(this.a);
}
var a = 2;
f(); // 输出:2
在JavaScript中声明在全局作用域中的变量就是全局对象的一个同名属性。在浏览器环境中,这个全局对象就是window
,所以var a = 2
等价于window.a = 2
。
当使用默认绑定时,this
会被绑定到全局对象,上面的例子中函数f()
执行时,this
会被默认绑定到全局对象,即此时this
指向全局对象,所以输出结果为2。
注意,当程序声明为"use strict"
时,则不能将全局对象用于默认绑定。
function f() {
"use strict";
// 此时this绑定到undefined
console.log(this.a);
}
var a = 2;
f();
注意:尽量在代码中使用严格模式,非严格模式容易写出难以维护的代码。更不要混合使用严格和非严格模式。
当函数执行时,它属于某个对象Object,那么此时this
被绑定到该对象。
function f() {
console.log(this.a);
}
var obj = {
a: 3,
f: f
};
var a = 2;
obj.f(); // 输出:3
虽然函数f
定义在全局作用域中,但是它被当做引用(reference)添加到obj中,当执行obj.f()
时,此时f
'属于'obj
,所以输出3。
将上述例子更改一下。
var obj = {
a: 3,
f: function() {
console.log(this.a);
}
};
var f = obj.f;
var a = 2;
f(); // 输出:2
此时,输出结果为2,为什么呢?因为严格来说函数并不属于任意一个对象,上例中的obj.f
和f
仅仅是函数的一个引用,所以f
和obj.f
没有直接关系,仅仅是所引用的函数相同,所以执行f()
,此时执行环境在全局作用域,会自动应用默认绑定规则,将this
绑定到全局对象中。第二个例子,是一个比较常见的this绑定问题,叫做this的隐式丢失。
在回调函数中经常会遇到隐式丢失的问题。
function f1() {
console.log(this.a);
}
function f2(f) {
// f引用的是f1
f();
}
var obj = {
a: "obj",
f: f1
};
var a = "global"
f2(obj.f); // 输出:"global"
在执行f2(obj.f)
时,实际上试将f1
赋值给f2
的参数f
,它实际上也只是函数的引用,所以结果是"global"
。
可以通过call
、apply
和bind
函数显示绑定this值。
function f() {
console.log(this.a);
}
var obj = {
a: 2
};
f.call(obj); // 输出:2
通过call
方法,显示指定this绑定到obj
对象,此时输出结果为2。可以给call
传递多个参数,第一个参数指定this
绑定对象,后续参数指定函数执行所需要的的参数。
apply
方法与call
类似,只不过call
方法接受的是参数列表,而apply
方法接受多个参数的数组。
bind
方法创建一个新的函数,在bind
被调用时,这个新函数的this
被指定为bind
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
const module = {
x: 42,
getX: function() {
return this.x;
}
};
const unboundGetX = module.getX;
console.log(unboundGetX()); // The function gets invoked at the global scope
// expected output: undefined
const boundGetX = unboundGetX.bind(module);
console.log(boundGetX());
// expected output: 42
如果你没有接触过JavaScript之外的语言,那么你可能很少会用到new
关键字,因为JavaScript中实际并不存在类
(Es6中的类底层其实也是函数)。JavaScript中的“构造函数”,只是使用new
操作符时被调用的函数,它们并不属于某个类,也不会实例化一个类,就是被new
操作符调用的普通函数。
使用new
调用函数(构造函数),会自动执行下面的步骤。
prototype
属性。this
关键字。上述第三步,就是new绑定。
讲述了四种绑定规则,接下来讲述下完整的步骤,确定几种方式的优先级。
new绑定,如果函数在new
中调用,那么this
绑定的是新创建的对象。
显示绑定,如果函数通过call
、apply
或者bind
调用,那么this
绑定的是指定对象。
隐式绑定,函数是否在某个上下文对象中调用,如果是的,那么this
绑定的是该上下文对象。
以上都不符合的话,使用默认绑定,严格模式下绑定到undefined
,非严格模式下绑定到全局对象。
凡事总有例外,这里介绍一个比较常见的例外——箭头函数=>
。
箭头函数不使用this
的四种绑定规则,而是根据外层(函数或者全局)作用域来确定this
的绑定对象。
const myObject = {
myMethod: () => {
console.log(this);
}
}
myObject.myMethod(); // this === global object
为什么上述myMethod()
执行的this是全局对象呢?因为箭头函数的this继承自它的外层(函数或者全局)作用域。
继续看一个例子。
const myObject = {
myArrowFunction: null,
myMethod: function() {
this.myArrowFunction = () => {console.log(this)};
}
};
myObject.myMethod(); // this === myObject
myObject.myArrowFunctoin(); // this === myObject
const myArrowFunction = myObject.myArrowFunction;
myArrowFunction(); // this === myObject
(完)