this可以说是JS最复杂、网上讨论最多的一个话题了。我希望我对this的理解能够帮助到你。
为什么有this
其实,在python等语言中,是没有this的。
class Person:
def __init__(self, birthyear):
self.birthyear = birthyear
def logAge(self):
print(2018 - self.birthyear)
p = Person(1999)
p.logAge()
但是在进行类声明的时候,为了获得对即将创建的对象的引用,python设计者强制规定了类中的方法的第一个参数是 这个对象的引用。这样做很符合python的设计哲学:明确而不隐性。
但是这样做的代价是很容易让代码变得冗长。JS的设计者选择了使用this以使得编程语言更加灵活。
如何判断this?
我把判断this分为这几种情况
- 构造函数创建对象的时候
- Function.prototype.bind()等强制绑定的情况
- 隐式绑定(这部分最难,全靠猜)
- 箭头函数的特殊情况
基本的,除了强制绑定和严格模式下的一个特例(下面会说到),this一定是指代一个对象!关键是找到这个对象究竟是什么。
构造函数的this绑定
正如上述所言,this最初的用途是在构造函数中指代即将创建的对象。
function Person(name, birthyear){
this.name = name
this.age = 2018 - birthyear
}
var p = new Person('lee', 1999)
console.log(p) // Person { name: 'lee', age: 19 }
这就是this
的基本用法,在使用new
关键字的时候,构造函数中this
就是指代即将创建的对象。我们也可以看到有了这种隐性的规定后,确实比python简洁。
隐性绑定
看下面的代码:
代码一:
function foo(){
console.log(this.a)
}
var a = 2
foo()
代码二:
function foo() {
console.log( this.a );
}
var a = 1
var obj = {
a: 2,
foo: foo
}
obj.foo()
var bar = obj.foo
bar()
结果是多少呢?
ES规范:
说人话
this只可能存在于全局作用域或者一个函数中
this存在于全局作用域
参考这种情况
var a = 2
console.log(this.a)
这是最基本的情况,没什么好说的,记住结论就行:这时this就是指代全局变量。在node中运行结果是undefined
,在浏览器中结果是2
。因为在浏览器情况下,全局声明的变量会被挂载到全局对象window
上,所以this.a
的结果是2
,而在node中不会。(一个无关痛痒的知识点,有的时候也挺烦人的啊。)
不过有些人认为这种情况下将this
绑定为全局对象不合理。作为妥协,在严格模式下,上述this
不再指向全局变量,而变成了undefined
,所以执行上述代码会报错。
this存在于一个函数中
而在JS中,所有的函数都是对象,而对象都是引用类型,所以函数都是引用类型。
而一个函数可以分为两个部分:函数名和函数体。比如function foo(){console.log(this.a)}
,就可以分为两个部分:函数名foo
和函数体function(){console.log(this.a)}
。
要执行一个函数,肯定要找到函数的地址,用C语言的话来说,那就肯定要用到一个指向函数地址的指针啊!而这个指针挂载到哪个对象上,this就会指代这个对象。
上述代码一中:
函数名foo指向了函数体 function(){console.log(this.a)}
,而其实foo并没有挂载到任何对象上,如果出现这种情况: 函数体 function(){console.log(this.a)}
中的this会被设定为全局对象!!所以上述代码一在浏览器下打印出2
,node下打印出undefined
。同样的,在严格模式下,this依旧是undefined
。
参考这样的代码:
var a = 1
function foo() {
var a = 2
function bar() {
console.log(this.a)
}
bar()
}
foo()
真正被执行的代码是函数体function(){console.log(this.a)}
,而找到这个函数体的是变量(或者称它为函数指针)bar
,bar
并没有挂载到任何对象上,所以它和上述代码一中的情况一样,这个函数体的this也是全局变量!
再夸张一点:
var a = 1
function foo() {
var a = 2
function bar() {
var a = 3
function biu() {
console.log(this.a)
}
biu()
}
bar()
}
foo()
虽然函数体的嵌套很多,但是结果一样,this依旧指向全局对象!
接下来讨论代码二,是重点。为了读者的观看体验,我把代码二再重复的贴一遍。
代码二:
function foo() {
console.log( this.a );
}
var a = 1
var obj = {
a: 2,
foo: foo
}
obj.foo()
var bar = obj.foo
bar()
无论是倒数第三行,还是倒数第一行,最终执行的函数体都是 function(){console.log(this.a)}
。
执行倒数第三行的时候,是obj.foo
这个'指针变量' 指向了这个函数体,而obj.foo
是挂载到obj
上的。所以说这个函数体中的this
就是obj
,所以最终会打印出2
。
而执行到倒数第一行时,是bar
这个'指针变量'指向了这个函数体,所以此时this
就是全局变量,最终结果会符合我们上面说的情况。
有的同学可能觉得我这种判断this的方法比较麻烦,觉得自己这种方法不是很好。那我们再来加一点难度。
强制绑定
ES提供了一种给函数强制绑定this的方法,即:
- Function.prototype.bind ( thisArg, ...args )
- Function.prototype.call ( thisArg, ...args )
- Function.prototype.apply ( thisArg, argArray )
强制绑定的优先级高于上面三种,所以被称为强制绑定
Function.prototype.bind ( thisArg, ...args )
根据ES标准,function.bind(thisArg)
会返回一个新的函数funcObj
,这个新的函数有一个内置的属性 funcObj.[[BoundThis]]
,它的值为thisArg
。这个属性对JS程序员不可见,属于JS引擎层面考虑的事。而以后调用这个函数的时候,函数体内部的this
就会强制绑定为thisArg
了。这种强制绑定的优先级要高于我们之前说的
function foo() {
console.log( this.a );
}
var a = 1
var obj = {
a: 2,
foo: foo
}
var obj2 = {
a: 3
}
obj.foo() // 2
bindFoo = obj.foo.bind(obj2)
bindFoo() // 3
var bar = bindFoo
bar() // 3
执行到obj.foo()
的时候,执行的函数体是function(){console.log(this.a)}
, 按照我们之前说的隐式绑定,this指向obj
,将会打印2
。
然后执行 bindFoo = obj.foo.bind(obj2)
,这时候其实返回了一个新的函数对象,我们把它叫做funcObj
把。函数对象funcObj
的属性 funcObj.[[BoundThis]]
的值被设定为 obj3
。
接下来执行bindFoo()
,其实就是执行funcObj()
。而它的this已经固定,不再按照隐式绑定的规则查找,所以会打印3
。
接下来执行var bar = bindFoo
。这时,bar其实指向了funcObj
,所以结果依旧是打印3
。
Function.prototype.call ( thisArg, ...args )
这个函数将会在程序执行时,将函数体内的this强行绑定为 thisArg
。它和 Function.prototype.apply ( thisArg, argArray )
的唯一区别在于:后者的参数写成数组的形式。
function foo(x, y) {
console.log( this.a + x + y);
}
var a = 1
var obj = {
a: 2,
foo: foo
}
var obj2 = {
a: 3
}
foo.apply(obj, [0, 0]) // 2
obj.foo.call(obj2, 0, 0) // 3
这几个this绑定的优先级
写几段代码测试即可
var obj1 = {
a: 1
}
var obj2 = {
a: 2
}
function foo() {
console.log(this.a)
}
var bindFoo = foo.bind(obj1)
bindFoo.call(obj2)
最终结果是打印出1
1
,所以.bind
绑定强于.call
.apply
.
var obj1 = {
a: 1
}
function foo() {
this.a = 3
}
var bindFoo = foo.bind(obj1)
var obj = new foo()
console.log(obj.a) // 3
console.log(obj1.a) // 1
上述结果可以看到使用new
关键字的时候,构造函数中的this就是指向新创建的对象,而不在乎之前可能存在的.bind()
绑定。
总结
判断this的时候按照以下步骤按顺序来
- 当this存在于全局作用域时,this指向全局对象,严格模式下this为
undefined
- 当一个函数作为构造函数时,函数体中的this执行新创建的对象
- 如果一个函数被通过
.bind(thisArg)
来强制绑定,以后该函数内部的this总是指向thisArg
- 如果一个函数在执行过程中被通过
.call(thisArg)
.apply(thisArg)
绑定,那么这次执行过程中this指向thisArg
- 在隐式绑定中,可以将这个函数分为两个部分:函数名和函数体。
- 函数名挂载到哪个对象上,函数体中的this就指向这个对象
- 如果函数名没有挂载到任何对象上,函数体中的this就指向全局对象,但是在严格模式下this为
undefined
实战
// 代码节选自《你不知道的JS(上)》
function foo() {
console.log( this.a );
}
function doFoo(fn) {
fn()
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"
doFoo( obj.foo ) // 打印多少?
其实很好理解,最终执行的函数体是 function(){console.log(this.a)}
,指向这个函数体的是fn
,而fn
没有挂载在任何对象上,就会被默认为全局对象,在浏览器中打印oops, global
,在node中打印undefined
。
再来看看
// 代码节选自《你不知道的JS(上)》
function foo() {
console.log( this.a )
}
var obj = {
a: 2,
foo: foo
}
var a = "oops, global"
setTimeout( obj.foo, 100 )
这和上述情况一模一样,只是显得隐蔽一点。