1.函数调用栈和调用位置
在函数执行的时候,会有一个活动记录(也叫执行上下文)来记录函数的调用顺序,这个就是函数调用栈。栈(Stack)是一种先进后出,后出先进的数据结构。
function a(){
console.log('a')
b() //b的调用位置,此时调用栈 a -> b
}
function b(){
console.log('b')
c() //c的调用位置,此时调用栈 a -> b -> c
}
function c(){
console.log('c')
}
a() //a的调用位置
以上代码中,首先执行a函数,此时a函数被push进调用栈的栈顶;在a函数的执行过程中,调用了b函数,b函数push进栈顶;b函数的执行过程中,调用了c函数,c函数push进栈顶。在浏览器环境下,此时便形成了window -> a -> b -> c这样的调用顺序。在chrome中的开发者工具可以更清晰的看清楚这点。
函数的调用位置
要想理解this绑定的过程,首先要弄清楚什么是调用位置。在之前的代码中,如果是在浏览器环境中,window调用了a函数,a的调用位置便是window;b函数调用了c函数,b的调用位置便是a函数;同理c函数的调用位置便是b函数。因此可以理解为栈顶正在的函数的上一个函数便是当前函数的调用位置。
2.this的四种绑定方式
this绑定的方式分为默认绑定,隐式绑定,显式绑定和new绑定。注意以下代码均在非严格模式下运行。
2.1默认绑定
function foo(){
console.log(this.a)
console.log(this===window)
}
var a = 10
foo() //10 true
以上代码中foo()是不带任何修饰被直接调用,因此只能应用默认绑定,此时foo函数内的this对象指向了window(浏览器环境)。
function foo(){
function bar(){
console.log(this===window)
}
bar()
}
foo() //true
函数不带任何修饰被调用,即使是在函数体内,也会应用默认绑定。上面代码中,运行在foo函数内的bar()绑定的this指向window。
2.2 隐式绑定
在函数的调用位置需要考虑函数是否被某个上下文(或对象)所包含。
function foo(){
console.log(this.a)
}
var obj = {
a: 1,
b: 2,
foo: foo,
bar: function(){
console.log(this.b)
}
}
obj.foo() //1
obj.bar() //2
以上代码中,无论函数是先声明再引入(foo),还是在内部定义(bar),这两种情况都会引用对应的函数。obj.foo()和obj.bar()的调用方式让调用位置通过obj对象来引用这两个函数,因此这两个函数的this会隐式绑定到obj上。
隐式丢失
常见于使用回调函数的时候
function useFoo(fn){
fn() //调用位置
}
var obj = {
a: 1,
foo: function(){
console.log(this.a)
}
}
var a = '我是全局a'
useFoo(obj.foo) //我是全局a
以上代码中,虽然useFoo传入参数的是obj.foo的方式,但其实参数传递是隐式赋值的方式,因此此时是将obj.foo赋值给参数fn,在调用位置上执行的fn实际上是foo(),因此应用了默认绑定,this指向全局环境。
这种情况内置的函数也不例外
function foo(){
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var a = '全局a'
setTimeout(obj.foo,1000) //全局a
内置的setTimeout()函数和下面的伪代码类似
function setTimeout(fn, delay){
//等待delay毫秒
fn() //调用位置
}
2.3 显式绑定
显式绑定是通过函数的原型方法 apply()、call()和bind()对this进行绑定。
apply(thisArg,[argsArray])
apply方法的第一个参数是一个对象,它会把函数的this绑定到这个对象上;第二个参数是传入的参数数组。
function foo(){
console.log(this.a)
}
var obj = {
a: 2
}
foo.apply(obj) //2
上面代码中apply方法首先将foo的this绑定到obj对象上,然后执行foo函数,这样就实现了this的显式绑定。
call(thisArg[, arg1[, arg2[, ...]]])
:call方法和apply方法的功能是一样的,只是call方法接受的是若干个参数列表,apply方法接受的是一个参数数组。
硬绑定
function foo(){
console.log(this.a)
}
var obj = {
a: 1,
foo: foo
}
var a='全局a'
var bar = obj.foo
bar.call(obj) //1
bar() //全局a
以上代码bar函数先调用call方法将this绑定到了obj对象上,但是再调用bar()函数,由于隐式丢失的原因,仍然是将this绑定到全局。可以通过硬绑定的方式来解决。
bind(thisArg[, arg1[, arg2[, ...]]])
:硬绑定是bind()方法实现的,它的参数和call方法是一样的,bind方法会返回this绑定了指定对象的原函数拷贝。
function foo(){
console.log(this.a)
}
var obj = {
a: 1
}
var a='全局a'
var bar = foo.bind(obj)
bar() //1
bar.call(window) //1,bind函数绑定的对象this对象不能再修改
上面代码调用foo函数的bind方法将this绑定到obj上,然后返回函数给bar,此时bar的this已经绑定到obj上。注意,通过bind函数绑定的对象this对象不能再修改,因为bind函数的内部会再将this绑定到obj上。
2.4 new绑定
function Person(name){
this.name = name
}
var bar = new Person('bar')
console.log(bar.name) //bar
1.new Person(bar)首先会创建一个全新对象,这个对象继承自Person.prototype,然后会将构造函数的this绑定到这个对象上;
2.如果构造函数返回了一个对象,那么它会取代第一步创建的对象,否则会返回new出来的对象(一般来说构造函数不返回任何值),因此bar是一个new创建的对象。
3.this绑定的优先级
优先级:new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
1.如果函数使用了new关键字,那么this绑定到new新创建的对象
var bar = new Person()
2.函数调用了call()、apply()和bind()方法进行显式绑定,那么this绑定到指定的对象上。
var bar = foo.call(obj)
3.函数调用位置是否被某个对象所包含,如果是则应用隐式绑定,this会绑定到那个对象。
obj.foo()
4.以上都不是,那么应用默认绑定。
4.安全的this
有时候我们使用apply()、call()和bind()方法不是想绑定某个对象的this,而只是传入某些参数。
function foo(a,b){
console.log(`a:${a} b:${b}`)
}
//使用apply展开数组
foo.apply(null,[1,2]) //1,2
//ES6中可以使用...运算符代替apply
foo(...[1,2]) //1,2
//使用bind进行柯里化
var bar = foo.bind(null,1)
bar(2) //1,2
以上代码传递了null作为第一个参数,以此来忽略this绑定。这样的做法实际上是会应用默认绑定的,函数的this可能会绑定到全局对象上,因此这可能会存在潜在的危险。
Object.create(null)
更安全的做法是使用Object.create(null)创建一个空对象,这样做法不会产生Object.prototype这个委托,也不会应用默认绑定。
function foo(a,b){
console.log(this)
console.log(`a:${a} b:${b}`)
}
var _o = Object.create(null)
//使用apply展开数组
foo.apply(_o,[1,2]) //1,2
//使用bind进行柯里化
var bar = foo.bind(_o,1)
bar(2) //1,2
console.dir(_o)
5.箭头函数的this
在ES6中的箭头函数,其this不会应用前文的四条规则,而是取决于其外层作用域。
function foo(){
return () => {
//this取决于foo调用时的this
console.log(this.a)
}
}
var obj = {
a: 1
}
var a = '全局a'
var bar = foo.call(obj)
bar() //1
bar.call(window) //1,无法修改绑定的this
foo()内部的箭头函数会捕获调用foo()时的this。foo的this绑定到了obj,因此bar也会绑定到obj,而且箭头函数的绑定无法修改。
箭头函数最常使用于回调函数中
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id);
}
}
上面init和doSomething方法使用了箭头函数,是this绑定到了handler对象,如果使用普通函数,this.doSomething中的this则会绑定到document对象。
6.小结
要判断一个函数绑定的this,需要找到这个函数的直接调用位置。找到之后,就按照四条绑定规则来判断this绑定的对象。需要特别注意的是,一些调用可能会无意中应用默认绑定规则。
如果是使用箭头函数,那么箭头函数会继承外层函数调用时绑定的this。