JS中的this
本文是本人阅读MDN文档和Dmitri Pavlutin的博客后写下的this学习笔记, 主要翻译自Dmitri Pavlutin的博客 Gentle Explanation of “this” in JavaScript,并添加了自己的理解和例子。本文只讨论浏览器环境下的this,且不保证正确。
1. JS中的this是什么
JS中的this是函数调用的上下文。 在我看来,这个定义包括两方面:1.在一个函数中,this是调用这个函数的对象;2.一个函数可能会被以不同的方式调用,不同的调用方式下,this的含义不同,因此,函数中的this的含义可以视为是在运行时确定的。
2. 不同函数调用情形下的this
JS中,函数被调用方式包括(但可能不限于)以下几种:
2.1 作为函数调用(function invocation)
“函数调用”就是“直接”在函数对象后加双引号(双引号中可以有用逗号隔开的参数)调用函数的方式(以“f(a1, a2)”形式最为常见)。直接的意思就是函数变量名前没有“xxx.”的语句,也就是说,函数不作为某个对象的方法被调用。
函数调用情形下,在非严格模式中,函数中的this指向全局对象(在浏览器中就是window对象);在严格模式下,this为undefined。
例如:
var b = 2;
console.log(this); // 全局对象window
var that = this;
function f1(){
console.log(that === this); // true
console.log(this.b); //2
}
function f2(){
'use strict'
console.log(this); // undefined
}
f1()
f2()
> Window {..., b:1, ...} // var b 被添加为window的属性
> true // 函数调用下,函数内部的this等于全局变量window
> 2 // this.b 即 window.b
> undefined // 严格模式
2.2 作为方法调用(method invocation)
“方法调用”是在一个以属性访问器形式表示的表达式(其计算结果是函数对象)后跟一个开括号(、一个逗号分隔的参数表达式列表和一个右括号)时执行的。最常见的就是直接在对象名后加点再加函数名和括号的形式(即 obj.method(a1, a2)形式)。在方法调用情况下,方法中的this指向拥有方法的对象。例如:
this.a1 = 33
var obj = {
f: function(num){
this.a1 = num
}
}
obj.f(44) //f作为obj的方法被调用,this指向obj,this.a1 即 obj.a1
console.log(obj.a1) //输出44
一个常见的误区是认为,即使将对象的方法抽离对象,其this仍不变。 实际上,以函数调用的形式调用对象方法,this指向全局变量。应时刻注意,js的this代表包含其的函数的调用方法,而非函数的定义方式。闭包返回的函数(即内部函数)中的this也不指向对象本身。例如:
this.a1 = 33 // 全局变量:window.a1
var obj = {
a1: function(){
this.a1 = 11;
return function(){
this.a1 = 22
}
}
}
f = obj.a1 //抽取方法
f() //函数调用,f中的this指向全局变量,所以上面a1方法中的this.a1 = 11 等价于window.a1 = 11
console.log(obj.a1) //这里的a1是obj的方法, 输出函数定义
console.log(window.a1) // 11
f = obj.a1() // f 为返回的匿名函数。此时,a1作为obj的方法进行调用
//这种情况下this为obj(具体阐述见2.2), this.a1 = 11, 即obj.a1 = 11
//也就是说, obj.a1 自此不是方法了,而是一个属性,其值为11
console.log(obj.a1) // 11
f() //以函数调用的形式调用f,f中的this为全局对象
console.log(window.a1) //因此,window.a1 = 22
再举一个例子:
function Pet(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = function() {
console.log(this === myCat);
console.log(`The ${this.type} has ${this.legs} legs`);
}
}
const myCat = new Pet('Cat', 4);
setTimeout(myCat.logInfo, 1000);
//输出:
// false
// ... undefined ... undefined
//因为,上述操作相当于:
// var f = myCat.logInfo
// setTimeOut(f, 1000)
2.3 作为构造函数调用(constructor invocation)
当new关键字后跟一个计算结果为函数对象的表达式、一个开括号(、一个逗号分隔的参数表达式列表和一个右括号)时,将执行构造函数调用。最常见的形式就是“new Class(a1, a2)”
在此情形下,函数中的this指向新构造的对象。例如:
var a = "b"
var C = function(){
this.a = "a";
}
C.prototype.b = function(){
return this.a
}
var obj = new C() // this 指向obj
console.log(obj.a) //a
console.log(obj.b()) //a 虽然b是顺着原型链找到的,但是其调用者仍是obj
注意,“new”后面接方法调用的形式也是构造函数调用。即: "new C.m()"也是构造函数调用。例如:
var C = function(){
this.a = "a";
}
C.prototype.b = function(){
this.c = "c"
}
var obj1 = new C() // this 指向obj
var obj2 = new obj1.b() //用C.prototype.b创造了一个新的对象obj2
//obj2没有属性a,只有属性c, 因为构造函数中只设置了c属性。
//C的构造函数并未被调用
//由于obj1.b被作为构造函数调用,其内部的this指向新的对象而不是obj1
//因此,没能给obj1设置c属性
console.log(obj1.a) //a
console.log(obj1.c) //undefined
console.log(obj2.a) //undefined
console.log(obj2.c) //c
有时,我们想要定义一个构造函数,但是定义或使用阶段出现了错误。比如,有人让构造函数返回值,且返回值不是新构造的对象,this也不指向新构建的对象。又比如,调用函数时忘记使用new。例如:
function Vehicle(type, wheelsCount) {
this.type = type;
this.wheelsCount = wheelsCount;
}
const car = Vehicle('Car', 4);
//由于忘记使用new, 函数Viechle被以函数形式调用
//没有新创建对象,构造函数里的this也指向全局对象, 因此给window添加了属性
console.log(car); // undefined
console.log(window.type); //Car
function Vehicle1(type, wheelsCount) {
this.type = type;
return this
}
console.log(Vehicle1("Car",5)) //window对象
function Vehicle2(type, wheelsCount) {
this.type = type;
return {
type: this.type
}
}
console.log(Vehicle1("Car",5)) //window对象
2.4 箭头函数(arrow function)
箭头函数中的this等于其定义环境的this。如果箭头函数在某个函数中被定义,其this值等于外部函数被执行时的this值。如果箭头函数的定义不在任何函数中,则其this为全局对象。箭头函数一经构建,其this指针就被绑定,且不能被任何方法改变。例如:
var Point = function(x, y){
this.x = x;
this.y = y;
this.getX = ()=>{return this.x;};
this.getY = ()=>{return this.y;};
}
var p1 = new Point(1,2) //构造函数调用; getX、getY方法被创建
//箭头函数的this被固定为p1
console.log(p1.getX()) // 1
f = p1.getY//将箭头函数抽离
console.log(f())//2 箭头函数的this被永久绑定,不受后续的调用方式影响
var a = 0
var f = ()=>{
this.a = 1
return this.a
}
console.log(f()) //1, 箭头函数是在最外围创造的,因此其this为window
在ES5中,如果把类的方法定义为箭头函数,则方法内的指针很可能不为新构建对象。例如:
var a = 11
var C = function(){
this.a = 1
}
C.prototype.method = ()=>{return this.a;}
var c = new C()
console.log(c.method()) // 11
2.5 非直接调用(indirect invocation)
非直接调用是指以function.call(argthis, args1, arg2, …), function.apply(argthis,[args])的形式调用函数。call和apply的第一个参数是fuction的调用上下文,也就是函数中的this的值。例如:
var obj1 = {
a:1,
b:2
}
var obj2 = {
a:3,
b:4
}
var f = function(){
return this.a + this.b;
}
console.log(f.call(obj1))
console.log(f.apply(obj2))
2.6 绑定函数(bound function)
绑定函数是指使用bind(argThis)函数新建一个函数,且将其上下文永久绑定到argThis的形式调用函数。一旦使用bind(argThis)绑定成功,函数的上下文(this)就不能再改变。但是,构造函数调用可以改变经过bind绑定的函数的上下文。例如:
var a = "b"
var obj = {
a: "a",
m: function(){
console.log(this.a);
}
}
var f = obj.m.bind(obj);
f(); //a
f.call(window); //a
f.apply(window); //a
f.bond(window)(); //a, 重绑定无效
var obj2 = new f(); //undefined, 因为new 使得this指向新构建的对象,而对象无a属性
3 函数以外的调用
如果一个this出现在函数以外的地方,则其值为window,或为undefined(在严格模式下)
'use strict'
console.log(this) //undefined
console.log(this) //window
4 总结
这已经不是我第一次学习js中的this了。之前学得迷糊的原因是:1. 误以为JS中的this与C++中的this的归属不同,其实二者都指向一个对象。2. 下意识地认为JS中的嵌套函数是在编码完成时就被实例化了,其实应该理解为嵌套函数是在外层函数被调用后产生的。3. 下意识地认为JS函数中的this在函数构建完后就确定了,实际上函数在被调用时,this才被确定。我把后两点归结为对JS即时编译的特点体会不够深。
其实JS中的this的原理很简单。除箭头函数和构造函数以外的函数调用情形下,this的值是函数的调用者,特别是在严格模式下,以函数调用的方式调用函数,函数是被看作没有调用者的,这符合人的观感,也符合编程需求。这些情形下,this的值与函数定义的位置、函数的归属无关。而箭头函数的this则是函数被创建时的上下文,与调用方式无关,且无法改变。构造函数调用使函数的this指向新构造的对象。ES6虽然出了一些关于构建类和对象的新特性,但this的原理没有改变。仅此而已。