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
(完)