this 详解
this 详解
在没有特别指明的情况下,下文的例子都是在浏览器环境。
关于严格模式和非严格模式的区别,可以参考:
全局上下文
严格模式和非严格模式:
函数上下文
函数上下文中的
this
都是在函数调用的那一刻才知道的,也就是运行时决定的。
默认绑定
通常指的是独立函数调用,没有具体的调用者,典型的形式为
fun()
。
非严格模式:
非严格模式中默认绑定中函数的this --> window
。
严格模式:
严格模式中默认绑定中函数的this --> undefined
。
扩展知识:
参考:https://es6.ruanyifeng.com/#docs/let#%E9%A1%B6%E5%B1%82%E5%AF%B9%E8%B1%A1%E7%9A%84%E5%B1%9E%E6%80%A7
var
在全局上下文中声明的变量会挂载到 window
上。
let, const
不会挂载到 window
上。
var name = 'xzq'
。
let name = 'xzq'
。
这里有一点需要注意,当你在 window
上声明的属性之后,你需要把浏览器当前网页关掉,重新打开,这个对应的属性才会消除。
setTimeout(cb, xx), setInterval(cb, xx)...
这种类型的cb
的调用方式也看做没有明确的调用者,那么其中cb
的this
的指向也可以应用默认绑定的规则。
隐式绑定
函数是由某个对象上调用的,例如:
xxx.foo()
。
非严格模式和严格模式:
this
指向调用者,和函数自身所处的位置无关。
注意:
如果将对象中的函数赋值出来,然后直接调用,那么就等同于第一种默认调用模式了。
显式绑定
指的是通过
call, apply, bind
的第一个参数进行显式指定函数的this
。例如:
foo.call(thisArg, ...args), foo.apply(thisArg, [args]), foo.bind(thisArg, ...args)()
。
关于这三个方法具体的用法可以参考:
其中 call 和 apply 类似,只有一个区别,就是 call 方法接受的是一个参数列表,而 apply 方法接受的是一个包含多个参数的数组。
其中 call 和 bind 的参数列表是一样的,但是 call 会调用函数;而 bind 不会调用函数,而是返回绑定this
之后的含函数。
关于这三者的第一个参数thisArg
需要做出一些特殊说明:
严格模式下:
传入什么,这个函数的this
就指向什么。
非严格模式下:
传入引用类型的值,那么这个函数的this
就指向这个引用类型的值。
传入原始类型的值,那么这个函数的this
就指向这个原始类型的值对应的包装对象。
传入null, undefined
,那么这个函数的this
就指向window
。
综合示例(非严格模式下):
new 绑定
指的是通过
new
调用一个函数,此时的this
指向一个新的对象。例如:
new foo()
。
参考:new(MDN)
通过new
调用函数时,JS
引擎内部会对该函数有隐式的如下4
步操作:
- 创建一个空的
JS
对象(即{}
); - 为步骤
1
新创建的对象添加属性__proto__
,将该属性链接至构造函数的原型对象; - 将步骤
1
新创建的对象作为该函数的this
,也就是改变该函数的this
指向,这里可以通过显式绑定的方式,例如:call, apply, bind
; - 如果该函数没有返回对象(包含
Functoin, Array, Date, RegExg, Error...
),则返回this
;
按照这个规则,我们自己实现以下new
运算符:
调用new func()
之后的返回值有两种情况:
func
中没有显式返回一个引用类型的值(包含Functoin, Array, Date, RegExg, Error...
),那么直接返回一个实例化后的对象;func
中显式返回一个引用类型的值(包含Functoin, Array, Date, RegExg, Error...
),那么直接返回该值;
但是this
的指向却和new func()
的返回值无关,new
调用函数中this
的指向是指向一个新创建的对象,也就是我们上例中传入的tmpObj
。
箭头函数
() => {}
,这是ES6
的一种定义函数的方式,该函数没有自己的this
,它的this
是由它的上级作用域中的this
决定。
关于箭头函数具体可以参考:
箭头函数需要注意的几点:
- 箭头函数没有自己的
this
对象,箭头函数的this
取决于它的上级作用域中的this
; - 由于箭头函数没有自己的
this
指针,调用显式绑定方式调用该函数时(call, apply, bind
),它们的第一个参数会被忽略,而只能正常识别传递后面的参数列表,也就是说无法绑定函数的this
,这也就意味着箭头函数的this
是无法通过显式绑定方式改变的; - 不可以当作构造函数,也就是说,不可以对箭头函数使用
new
运算符,否则会抛出一个错误; - 没有自己的
arguments
对象,当然,如果父级作用域有arguments
,它是可以正常和访问变量一样使用的。如果要用,可以用rest
参数代替; - 不可以使用
yield
命令,因此箭头函数不能用作Generator
函数;
举一个例子:
参考:let 具有块级作用域
在ES6
之前只有两种作用域,分别是全局作用域和函数作用域,在ES6
时多出来一种块级作用域。
我们这里需要知道的就是前两种就够了。
person.show1()
:箭头函数是没有自己的this
的,它依赖于的上级作用域中的this
,这里它的上级作用域是全局作用域,也就是说不管是严格模式还是非严格模式,它的this
都是指向window
。
person1.show1.call(person2)
:箭头函数通过显式绑定的方式(call, apply, bind
)指定this
时,指定是无效的,也就是说这里的箭头函数的this
依旧是寻找上级作用域中的this
,同样指向window
。
person1.show2()()
:
const func = person1.show2()
得到一个箭头函数;func()
这个func
就是箭头函数,它的this
和它上级作用域的this
绑定的,这里我们发现它的上级作用域是函数作用域,也就是show2
这个函数的作用域,show2
函数的是由person1
对象调用的,也就是隐式绑定的方式指定的this
,那么show2
函数的this
指向person1
,因此func
的this
指向person1
。
person1.show2().call(person2)
:
const func = person1.show2()
得到一个箭头函数;func.call(person2)
这个func
就是箭头函数,箭头函数通过显式绑定的方式(call, apply, bind
)指定this
时,指定是无效的,也就是说这里的箭头函数的this
依旧是寻找上级作用域中的this
,同样指向show2
函数中的this
,也就是person1
。
person1.show2.call(person2)()
:
const func = person1.show2.call(person2)
得到一个箭头函数;func()
这个func
就是箭头函数,它的this
和它上级作用域的this
绑定的,这里我们发现它的上级作用域是函数作用域,也就是show2
这个函数的作用域,show2
函数的是由call(person2)
的方式调用,也就是显式绑定的方式指定的this
,那么show2
函数的this
指向call
的第一个参数person2
,因此func
的this
指向person2
。
注意:
有很多人箭头函数中的this
是固定的,在定义时就决定了的。而普通函数的this
是在运行时才知道的,但是经过上面我们的例子发现,其实箭头函数的this
不是固定的。
因为箭头函数的this
和它的上级作用域中的this
是绑定的,如果它的上级作用域是普通函数中的函数作用域,那么该箭头函数中的this
也是在该普通函数调用时才决定。
DOM 事件绑定函数
通过
addEventerListener(eventName, cb)
或者onEventName --> onclick = cb
的方式给DOM
元素注册的回调函数中的this
指向绑定该事件的元素,也就是和ev.target
相同。
关于currentTarget, target
可以参考:
currentTarget 指向当前绑定事件的元素,它是不可变的,而 target 指向触发当前事件的元素,因此可变。
总结
判断this
的指向可以总结为如下几步:
- 当前
this
在全局上下文还是在函数上下文,如果是全局上下文,那么直接就是window
,如果是函数上下文就接着往下走; - 对于函数上下文,我们可以看函数的调用方式被划分为哪一种类别:
- 如果是
foo()
的形式,或者setTimeout(foo, timeout), setInterval(foo, timeout)
,那么采用默认绑定的规则; - 如果是
xxx.foo()
的形式,那么采用隐式绑定的规则; - 如果是
foo.call(thisArg, ...args), foo.apply(thisArg, [args]), foo.bind(thisArg, ...args)()
的形式,那么采用显式绑定的规则; - 如果是
new foo()
的形式,那么采用new
绑定的规则; - 如果该函数是
() => xxx
,那么采用箭头函数的规则; - 如果当前函数是通过
addEventerListener(eventName, foo)
或者onEventName --> onclick = foo
的方式注册的,那么采用DOM
事件绑定函数的规则;
- 如果是
经典面试题
测试题 1
测试题 2
测试题 3(最难)
知识点:
- 原型链
- 变量提升
this
- 运算符优先级