this全面解析
每个函数的 this 是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)
调用位置
调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
最重要的是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就在当前正在执行的函数的前一个调用中,调用位置决定了this的绑定。
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar 的调用位置
}
function bar() {
// 当前调用栈是 baz -> bar
// 因此,当前调用位置在 baz 中
console.log( "bar" );
foo(); // <-- foo 的调用位置
}
function foo() {
// 当前调用栈是 baz -> bar -> foo
// 因此,当前调用位置在 bar 中
console.log( "foo" );
}
baz(); // <-- baz 的调用位置
绑定规则
在找到调用位置后,应用四条规则中的某一条,完成this的绑定
1. 默认绑定
我们可以把这条规则看做无法使用其他规则时的默认规则;
直接使用不带任何修饰的函数引用, 在调用时, 只能使用 默认绑定
;
默认绑定
会将 this
绑定到 window
对象上;(this === window
)
function foo() {
console.log(this === window)
bar() // true
}
function bar() {
console.log(this === window)
}
foo() // true
但在严格模式下, 默认绑定会绑定一个
undefined
:
"use strict"
function foo() {
console.log(this)
}
foo() // undefined
2. 隐式绑定
通过考虑 调用位置是否具有上下文对象, 或者说是否被某个对象拥有包含, 来判断是否运用这条规则:
如下示例, 我们声明一个
foo
函数, 并将其赋值给obj
对象的foo
属性:
function foo() {
console.log(this.a)
}
var obj = {
a: 1,
foo: foo
}
obj.foo() // 1
// 当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象
foo() // undefined
对象属性引用链中只有最后一层在调用位置中起作用, 如下:
function fn() {
console.log(this.msg)
}
var child = {
msg: 'child',
fn: fn
}
var parent = {
msg: 'parent',
child: child
}
parent.child.fn() // child
隐式丢失
如下示例, o.foo
的 this
隐式绑定在了 o
对象上, 而 bar
引用了 o.foo
函数本身,相当于把foo()函数的地址值赋给了变量bar, 所以此时的 bar()
其实是一个不带任何修饰的函数调用, 因此使用了 默认绑定 规则:
var o = {
a: 1,
foo() {
console.log(this.a)
}
}
var bar = o.foo
o.foo() // 1
bar() // undefined
另一个很出乎意料的例子, 示例中, bar(o.foo)
实际上采用了隐式赋值: callback = o.foo
, 事实上跟上面的例子一样, 都是直接引用了 o.foo
函数本身, 所以造成了 隐式丢失:
造成隐式丢失的根本原因是,虽然用的是obj.fn,但由于js值传递的特性,实际赋值给变量的是函数的地址值,也就是变量本身引用的是函数的定义,与obj无关。
function bar(callback) {
callback()
}
var o = {
a: 1,
foo() {
console.log(this.a)
}
}
bar(o.foo) // undefined
3. 显式绑定
在分析隐式绑定时,我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。 那么如果我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
当我们并不想通过 隐式 或者 默认 的方式来间接绑定 this
的指向, 我们需要强制的为函数指定一个绑定对象,通过使用 call()
和 apply()
方法来实现;
fn.call(obj Object [, ...arguments])
第一个参数接收一个对象, 作为 this
关键字绑定的对象, 第二个参数是该函数传递的参数;
function foo(a, b) {
console.log((a + b) * this.c)
}
var obj = {
c: 2
}
foo.call(obj, 1, 2) // 6
// foo.apply(obj, [1, 2])
// call() 与 apply() 的效果完全一致, 唯一不同的只是传递参数的格式不同。apply接收数组,call接收,拼接的参数
但是显式绑定仍然无法解决丢失绑定问题。
使用显示绑定时, 我们重复进行绑定, 仍然会让之前的绑定值丢失:
var obj = { name: 'muzi' }
function foo() {
console.log(this.name)
}
foo.call(obj) // muzi
setTimeout(() => (
foo.call({ name: 'yaya' } // yaya
)), 100)
如果我们想避免这种情况, 就需要使用 硬绑定;
硬绑定
硬绑定的典型应用场景就是创建一个包裹函数, 传入所有的参数并返回接收到的所有值:
var obj = {
name: 'muzi'
}v
function foo() {
console.log(this.name)
}
function wrapper() {
foo.call(obj)
}
wrapper() // muzi
wrapper.call({ name: 'yaya' }) // muzi
虽然 wrapper
绑定了一个新的对象, 但当 wrapper
每次被调用时,
foo
都会显示绑定 obj
对象;
所以无论 wrapper
如何调用, foo
函数的绑定值都不会被改变.
我们可以根据这个特性, 创建一个辅助绑定函数:
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments)
}
}
function foo(b, c) {
return this.a + b + c
}
var bar = bind(foo, { a: 1 })
bar(2, 3) // 6
bar.call({ a: 10 }, 2, 3) // 6
事实上, 早在
ES5
就提供了原生的硬绑定方法:Function.prototype.bind
var bar = foo.bind({ a: 1 })
bar(2, 3) // 6
bar.call({ a: 10 }, 2, 3) // 6
Function.prototype.bind
的实现:
Function.prototype._bind = function(obj) {
var self = this
return function() {
return self.apply(obj, arguments)
}
}
new绑定
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log( bar.a ); // 2
使用 new 来调用 foo(…) 时,我们会构造一个新对象并把它绑定到 foo(…) 调用中的 this 上。new 是最后一种可以影响函数调用时 this 绑定行为的方法,我们称之为 new 绑定。
优先级
new
> 显式绑定
> 隐式绑定
> 默认绑定
ES6 箭头函数
箭头函数不使用
this
的四种标准规则, 而是根据外层(函数或者全局)作用域
来决定 this
function foo() {
// 返回一个箭头函数
return (a) => {
//this 继承自 foo()
console.log( this.a );
};
}
var obj1 = {
a:2
};
var obj2 = {
a:3
};
var bar = foo.call( obj1 );
bar.call( obj2 ); // 2, 不是 3 !
foo() 内部创建的箭头函数会捕获调用时 foo() 的 this。由于 foo() 的 this 绑定到 obj1, bar(引用箭头函数)的 this 也会绑定到 obj1,箭头函数的绑定无法被修改。(new 也不行!)
箭头函数最常用于回调函数中,例如事件处理器或者定时器:
function foo() {
setTimeout(() => { // 这里的 this 在此法上继承自 foo()
console.log( this.a );
},100);
}
var obj = { a:2 };
foo.call( obj ); // 2
箭头函数可以像 bind(…) 一样确保函数的 this 被绑定到指定对象,此外,其重要性还体现在它用更常见的词法作用域取代了传统的 this 机制。实际上,在 ES6 之前我们就已经在使用一种几乎和箭头函数完全一样的模式。
function foo() { // foo()作用域
var self = this; // 普通函数作为回调函数,由于函数作用域的不同,会发生this隐式丢失
setTimeout( function(){ //内部函数作用域
console.log( self.a );
}, 100 );
}
var obj = { a: 2 };
foo.call( obj ); // 2
小结
如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置(当前执行的函数的前一个调用中)。找到之后就可以顺序应用下面这五条规则来判断 this 的绑定对象。
-
由 new 调用?绑定到新创建的对象。
-
由 call 或者 apply(或者 bind)调用?绑定到指定的对象。
-
由上下文对象调用?绑定到那个上下文对象。
-
默认:在严格模式下绑定到 undefined,否则绑定到全局对象。
-
箭头函数的this? 由外层作用域的this来决定
更新一个比较绕的关于this的读程题,运行一下看结果,按照上面对this指向的解读仔细想一下
const obj = {
dev: 'bfe',
a: function() {
return this.dev
},
b() {
return this.dev
},
c: () => {
return this.dev
},
d: function() {
return (() => {
return this.dev
})()
},
e: function() {
return this.b()
},
f: function() {
return this.b
},
g: function() {
return this.c()
},
h: function() {
return this.c
},
i: function() {
return () => {
return this.dev
}
}
}
console.log( obj.a() )
console.log( obj.b() )
console.log( obj.c() )
console.log( obj.d() )
console.log( obj.e() )
console.log( obj.f()() ) // obj.f() 得到 this.b,这里其实就是拿到了 b方法的引用,跟this没关系了
console.log( obj.g() )
console.log( obj.h()() )
console.log( obj.i()() )
参考文献
你不知道的js上