前言
js中的this指向灵活,使用场景多样,概念虽然基础,但却非常重要,非常多隐蔽的bug都来源于它,搞懂this的指向是非常有必要的,如果可以熟练驾驭this,那么代码自然会更为简洁优雅
this指向谁?
关于this指向谁,有一个最通俗的说法
谁调用它,this就指向谁
这样说没什么大的问题,但是并不是特别全面,但是总的来说,他的方向是对的,this的指向是在调用时被确定的,this会指向上下文里调用它的对象,这是最基本的用法
const person = {
name:'xz',
sayName:function(){
console.log(this.name)
}
}
person.sayName()
这样是没什么问题,也非常好理解,但是如果换个写法
var person = {
name:'xz',
sayName:function(){
console.log(this.name)
}
}
var name = 'jack'
var sayName = person.sayName
sayName()
这里实际会log出jack,这里就会涉及到一个概念
非显式或隐式调用函数时候,this在严格模式下指向undefined,在非严格模式下指向window
也就是直接调用sayName时,它的this是指向window的,所以他log出了挂在window下的name也就是’jack’
再来看一道升级版的题目,如果搞懂了,this的上下文调用也就没什么问题了
const o1 = {
text:'o1',
fn:function(){
return this.text
}
}
const o2 = {
text:'o2',
fn:function(){
return o1.fn()
}
}
const o3 = {
text:'o3',
fn:function(){
var fn = o1.fn
return fn()
}
}
console.log(o1.fn())
console.log(o2.fn())
console.log(o3.fn())
答案是o1,o1,undefined
第一个最简单
第二个o2.fn实际上最终执行的还是o1.fn,所以是o1
第三个实际上是裸奔调用,所以指向了window,最终是undefined
call,apply,bind三剑客
call,apply,bind这3个方法都是用来改变this指向的,但是他们具体有什么区别呢
call与apply类似,都是重新绑定this后调用函数,bind则是返回一个绑定好了的函数,需要手动调用
一些基础的用法这里就不再重复说明
const target = {name:'xz'}
const fn = function(arg1,arg2){
console.log(arg1,arg2)
console.log(this.name)
}
// 以下3种方式等价,最终都会log出参数1,参数2,xz
fn.call(target,'参数1','参数2')
fn.apply(target,['参数1','参数2'])
fn.bind(target,'参数1','参数2')()
当然,现在面试关于这三剑客的问法已经不再局限于怎么用了,而是如何实现一个,仔细思考其实并不难,fn.bind(obj)实际就是需要让调用方式变成obj.fn,下面是一个简单的bind实现
function myBind (){
// this是需要绑定的方法
const me = this
// 参数转化为数组
var args = Array.from(arguments)
// 第一个参数是要bind的上下文
const ctx = args[0]
// 其余的是函数的参数
const fnArgs = args.slice(1,args.length)
// 返回绑定后的函数
return ()=>{
// 使用symbol作为key避免覆盖原属性
const fn = Symbol('fn')
// 给需要绑定的上下文添加这个函数作为属性
ctx[fn] = me
// 调用函数
ctx[fn](...fnArgs)
// 调用后移除该属性
delete ctx[fn]
}
}
Function.prototype.myBind = myBind
const obj1 = {name:'xz'}
function fn(age){
console.log(`Hi,I am ${this.name},I am ${age} years old`)
}
fn.myBind(obj1,18)()
// Hi,I am xz,I am 18 years old
一个建议的bind就完成了,如果是call和apply则不需要return一个function,直接在内部挂载属性调用函数然后删除属性就可以了,三剑客只要掌握一个,另外2个自然也就触类旁通
构造函数的this
说到构造函数,就不得不提new 关键字了,这2者密不可分
new做了什么事,主要有以下4件
- 创建一个新对象
- 将构造函数的this指向这个新对象
- 给这个对象添加属性方法等
- 返回这个新的对象
理解了new做了什么,构造函数的this指向哪里自然也就一目了然了,会指向构造函数新构建的对象
function Foo(){
this.name = 'xz'
}
const me = new Foo()
console.log(me.name) // xz
但是要注意一个特殊情况:构造函数有返回的情况,这种情况又要分为2种情况讨论
返回一个对象或或返回的不是一个对象
function Foo1(){
this.name='xz'
const o = {name:'zed'}
return o
}
function Foo2(){
this.name = 'xz'
return 1
}
const me1 = new Foo1()
const me2 = new Foo2()
console.log(me1.name)
// zed
console.log(me2.name)
// xz
可以看到,如果返回的是一个对象,那么this就会指向这个返回的对象
如果返回的不是对象,那么this仍然指向构造的实例
箭头函数的this
箭头函数本身没有this,他的this完全依赖于外层作用域来决定
const foo1 = {
fn:function(){
setTimeout(function(){
console.log(this)
})
}
}
const foo2 = {
fn:function(){
setTimeout(() => {
console.log(this)
})
}
}
foo1.fn()
// window
foo2.fn()
// foo2
单纯的箭头函数this指向很简单,但如果综合各种情况,并结合this的优先级,就相对没那么容易确定了
this优先级
首先思考如下问题
function foo(){
console.log(this.a)
}
const obj1 = {
a:1,
foo
}
const obj2 = {
a:2,
foo
}
obj1.foo.call(obj2)
这个最终会log出2,为什么不是1呢,这就涉及到优先级的问题了
通常我们把根据call,apply,bind,new等关键字进行绑定的情况称为显式绑定,根据调用关系绑定this的方式称为隐式绑定
优先级的先后顺序为隐式绑定 < call,apply,bind < new < 箭头函数this
所以这里通过call绑定的obj2覆盖了隐式绑定的obj1
再来看一个
function Foo(a){
this.a=a
}
const obj = {}
const bar = Foo.bind(obj)
bar(2)
console.log(obj.a)
const baz = new bar(3)
console.log(baz.a)
这里会log出2和3
首先通过bind将函数的this绑定为obj对象,然后进行赋值,所以obj.a为2
但当他作为构造函数通过new来构建时,new的优先级大于bind,所以他的指向又变成了他构造出的对象
再看最后一个
function foo(){
return ()=>{
console.log(this.a)
}
}
const obj1 = {
a:2
}
const obj2 = {
a:3
}
const bar = foo.call(obj1)
bar.apply(obj2)
这里最终会输出2,foo通过call绑定到了obj1上,返回的箭头函数this同时被绑定到obj1
这时再通过apply想改变这个箭头函数的this则是不行的,因为箭头函数的优先级最高,所以this依旧执行obj1,最终log出2
小结
this用法繁多,确实不容易彻底掌握,但是只要能理解他的绑定方式,优先级,则再复杂的场景,也可以抽丝剥茧的找到this具体的指向,this的灵活性使他无法被死记硬背,更多的重在理解