一、this是什么
字面意思
根据字面意思,this表示这个,或者说指向这个。
this.a 就表示这里是这个的a变量。
运行机制
首先,我们很容易想到,this它自己本身就是个变量。
这一点很容易理解,在日常我们撸码的时候,用到this,就是为了让我们的代码可以复用的。
function foo() {
console.log('foo:', this.name)
}
var obj1 = {
name:'CR7',
foo:foo
}
var obj2 = {
name:'Messi',
foo:foo
}
obj1.foo() // foo: CR7
obj2.foo() // foo: Messi
这就是说,this具体指向谁,是在这个this被调用的时候才确定的。
说的书面一点,就是在函数被调用时,会在调用栈里创建一个context,这个里面就包含了this。
疑问
如果使用this就像上面所展示的那样清晰(理想状态)就万事大吉了!实际上,我们在写跟this有关的函数时,不知道在哪里会复用到这个函数。有时候debug就很脑阔痛。那么,在复杂场景下,this到底指向谁?它的绑定逻辑(规则)又是什么?
二、this的绑定规则
调用位置
简单的说,分析调用位置在哪,就是分析调用栈。调用位置就在当前正在执行的函数的前一个调用中。
虽说是栈,但也可以理解为一条链。
function foo1() {
// 当前调用栈是:foo1
// 因此,当前调用位置是全局作用域
console.log( "foo1" );
foo2(); // <-- foo2 的调用位置
}
function foo2() {
// 当前调用栈是 foo1 -> foo2
// 因此,当前调用位置在 foo1 中
console.log( "foo2" );
foo3(); // <-- foo3 的调用位置
}
function foo3() {
// 当前调用栈是 foo1 -> foo2 -> foo3
// 因此,当前调用位置在 foo2 中
console.log( "foo3" );
}
foo1(); // <-- foo1 的调用位置
tips
你可以在你要查看的函数里打个断点,然后再chrome里面查看。
四种绑定规则
一般来说,this的绑定规则有四条
1. 默认绑定
最常见的,当这个函数独立被调用时,就会触发此规则。或者说,再其他规则都不匹配的情况下,会触发此规则。
function foo() {
console.log(this.name)
}
var name = 'CR7'
foo() // CR7
首先,你要知道的是,js中存在全局作用域的概念,一般来说,它叫window。
当我们调用foo函数时,我们没有给foo函数加任何修饰(或者说,这娃没被我们指派到哪个大哥门下),单纯的调用这个函数(纯到不行那种)。那么,在这个时候就会触发默认规则。
默认情况下,this会指向最最最最远古的对象(万物皆对象!)window,而在window中,恰好有个变量是name,那么就直接引用它。
需要注意的是,在严格模式下,this无法默认绑定window,会被绑定成undefined。
function foo() {
'use strict'
console.log(this.name)
}
var name = 'CR7'
foo() // TypeError: this is undefined
这里其实还有一个大坑!!!
虽然强烈不建议严格模式与非严格模式混用,但保不齐就有这种人呢!!!
下面的代码要说的就是上面提到的严格模式对于this的影响是在这个函数运行的域里才有效。
function foo() {
console.log(this.name)
}
var name = 'CR7'
(function() {
'use strict'
foo() // CR7
})()
2. 隐式绑定
正如开篇的例子所示
function foo() {
console.log('foo:', this.name)
}
var obj1 = {
name:'CR7',
foo:foo
}
var obj2 = {
name:'Messi',
foo:foo
}
obj1.foo() // foo: CR7
obj2.foo() // foo: Messi
在这里,obj1和obj2这两个对象里面都有一个叫foo的key,它们对于的value是foo这个函数的引用。
那么,在调用obj1里面的foo这个方法时,我们调用的函数其实还是全局里面的foo函数,但是在这里,我们的执行上下文却成了obj1,自然我们的this就被绑定到了obj1上。
多层调用
还有一个问题,如果调用这个函数时是多层调用的。那么只有最后一个对其起效果。
function foo() {
console.log( this.name )
}
var obj2 = {
name: 'CR7',
foo: foo
}
var obj1 = {
name: 'Messi',
obj2: obj2
}
obj1.obj2.foo() // CR7
隐式丢失
有时候,我们隐式绑定会失效(丢失)。
- 隐式绑定的函数取了个别名并调用别名
这种情况相当于直接调用函数,因此是默认绑定。
function foo() {
console.log( this.name )
}
var obj = {
name: 'Messi',
foo: foo
}
var bar = obj.foo
var name = "CR7"
bar() // CR7
- 作为形参传给其他函数进行回调
function foo() {
console.log( this.name )
}
var obj = {
name: 'Messi',
foo: foo
}
var name = "CR7"
function bar(fn) {
fn()
}
bar(obj.foo) // CR7
3. 显式绑定
在js中有2个方法可以实现显示绑定:call, apply
function foo() {
console.log(this.name)
}
var obj = {
name:'CR7'
}
foo.call(obj) // CR7
这2个方法会立即调用(不在被函数包裹的情况下),但有时候我们想先绑定了放在这,等要用的时候再调用并且传递参数。
- 一种方法是我们可以给他封装一个函数。
function foo(something) {
console.log( this.a, something )
return this.a + something
}
var obj = {
a : 2
}
var bar = function() {
return foo.apply( obj, arguments )
}
var b = bar( 3 ) // 2 3
console.log( b ) // 5
- 另一种方法是我们可以使用bind来进行硬绑定
function foo(something) {
console.log( this.a, something )
return this.a + something
}
var obj = {
a : 2
}
var bar = foo.bind( obj )
var b = bar( 3 ) // 2 3
console.log( b ) // 5
4. new
在new一个实例的时候,这个对象会被重新创建一个并赋值给变量。
function foo(name) {
this.name = name
}
var bar = new foo('CR7')
console.log( bar.name ) // CR7
三、四种绑定规则的优先级
一般情况下
new > 显式 > 隐式 > 默认
例外
1. call/apply/bind传入null/undefined
这种情况下,会使用默认绑定
function foo() {
console.log( this.name )
}
var name = 'CR7'
foo.call( null ) // CR7
2. 间接引用
这一点和上文提到的隐式丢失是一个意思。
function foo() {
console.log( this.a )
}
var a = 2
var o = { a: 3, foo: foo }
var p = { a: 4 }
o.foo() // 3
(p.foo = o.foo)() // 2
3. 软绑定
对于硬绑定,人如其名,够硬!硬到一旦绑定了之后就无法更改。这显然不是我们所想要的。我们想要的是足够灵活,也就是说想要显式的时候就显式,想隐式的时候就隐式。
想要达到上述要求,我们就可以使用软绑定。
if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj) {
var fn = this
// 捕获所有参数
var curried = [].slice.call( arguments, 1 );
var bound = function() {
return fn.apply(
(!this || this === (window || global)) ?
obj : this,
curried.concat.apply( curried, arguments )
)
}
bound.prototype = Object.create( fn.prototype )
return bound
}
}
function foo() {
console.log("name: " + this.name)
}
var obj = { name: "obj" },
obj2 = { name: "obj2" },
obj3 = { name: "obj3" }
var fooOBJ = foo.softBind( obj )
fooOBJ() // name: obj
obj2.foo = foo.softBind(obj)
obj2.foo() // name: obj2
fooOBJ.call( obj3 ) // name: obj3
setTimeout( obj2.foo, 10 ) // name: obj
简单的说,软绑定实现的功能就是,在调用这个函数时检查this,如果this被绑定到了window或者undefined,就是把this绑定到传入的obj上,如果不是,那就根据相应的规则绑定this。
四、ES6
在ES6中,出现了箭头函数。
箭头函数的this根据其所在的函数作用域来决定
function foo() {
setTimeout(() => {
// 这里的 this 继承自 foo()
console.log( this.name )
},100)
}
var obj = {
name : 'CR7'
}
foo.call( obj ) // CR7
五、总结
-
要确定this,根据四条规则来依次判断:
- 是否是new,
- 是否是显式绑定(call/apply/bind),
- 是否是隐式绑定(作为一个对象的方法来调用),
- 默认规则
-
箭头函数的this只根据其所在的函数作用域来判断。