初学者关于this
的理解一直很模糊,关于this
的面试题更加令人头大。
this
是函数执行的主体(谁执行的),this 是和执行上下文绑定的。
执行上下文:
- 全局执行上下文:全局执行上下文中的 this 也是指向 window 对象。
- 函数执行上下文:使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的。
- eval 执行上下文:执行 eval 环境内部的上两个情况。
请记住:this是谁和函数在哪儿创建的或者在哪执行的都没有关系
掌握以下几条分清执行的主体(this
)的规律,大多数面试题可迎刃而解。
1. 给元素的某个事件绑定方法,当事件触发方法执行的时候,方法中的this
是当前操作的元素(隐式绑定)
document.body.onclick = function() {
// this: body
}
通常来说this的值是触发事件的元素的引用,这种特性在多个相似的元素使用同一个通用事件监听器时非常让人满意。
当使用 addEventListener() 为一个元素注册事件的时候,句柄里的 this 值是该元素的引用。其与传递给句柄的 event 参数的 currentTarget 属性的值一样。
2. 方法执行,看方法前面是否有点(.),如果有点,点前面是谁this
就是谁(隐式绑定),没有点this
就是window
(在严格模式下("use strict
")没有点this
是undefined
,也成为默认绑定)
注意:自执行函数((function(){...})()
)的this
一般是window
function fn() {
console.log(this)
}
let obj = {
fn: fn
}
fn()
obj.fn()
输出:window对象
obj对象
上述代码等同于:
let obj = {
fn: function fn() {
console.log(this)
}
}
fn()
obj.fn()
即在上文中,函数虽然被定义在对象的内部中,但它和”在对象外部声明函数,然后在对象内部通过属性名称的方式取得函数的引用”,这两种方式在性质上是等价的(而不仅仅是效果上)
在一串对象属性链中,this
绑定的是最内层的对象
如下所示:
var obj = {
a: 1,
obj2: {
a: 2,
obj3: {
a: 3,
getA: function () {
console.log(this.a)
}
}
}
}
obj.obj2.obj3.getA()
输出:3
,题中this:obj.obj2.obj3
3. 在构造函数模式执行中,函数体中的this
是当前类的实例
执行new操作的时候,将创建一个新的对象,并且将构造函数的this指向所创建的新对象。JS编译器会做这四件事情:
-
创建一个新的空的对象
-
把这个对象链接到原型对象上
-
这个对象被绑定为this
-
如果这个函数不返回任何东西,那么就会默认return this
function Fn(name) { // this: Fn类的实例 this.name = name } var f1 = new Fn('qxj') var f2 = new Fn('lx') console.log(f1.name) console.log(f2.name)
输出:
qxj
lx
4. call/bind/apply可以改变this
的指向(显式绑定)
call的基本使用方式: fn.call(object)
fn:你调用的函数,object:你希望函数的this
所绑定的对象。
fn.call(object)的作用:
- 即刻调用这个函数(fn)
- 调用这个函数的时候函数的
this
指向object对象
例子:
var obj = {
a: 1, // a是定义在对象obj中的属性
fire: function () {
console.log(this.a)
}
}
var a = 2 // a是定义在全局环境中的变量
var fireInGrobal = obj.fire
fireInGrobal() // => 2
fireInGrobal.call(obj) // => 1
原本丢失了与obj
绑定的this
参数的fireInGroba
再次重新把this
绑回到了obj
但是,我们其实不太喜欢这种每次调用都要依赖call
的方式,我们更希望:能够一次性返回一个this
被永久绑定到obj的fireInGrobal
函数,这样我们就不必每次调用fireInGrobal
都要在尾巴上加上call
那么麻烦了。
怎么办呢?如果使用bind
的话会更加简单
var fireInGrobal = function () {
fn.call(obj) //硬绑定
}
可以简化为:
var fireInGrobal = fn.bind(obj);
call和bind的区别:在绑定this
到对象参数的同时:
-
call将立即执行该函数
-
bind不执行函数,只返回一个可供执行的函数
【其他】:apply
除了使用方法,它和call
并没有太大差别
5. 箭头函数
箭头函数会无视以上所有的规则,因为箭头函数没有自己的执行上下文,this的值就是函数创建时候所在的词法作用域(lexical scope)中的this(外层函数的 this),而和调用方式无关。
可以对比下面两个例子:
function Person() {
this.age = 0
setTimeout(function () {
console.log(this.age) // => undefined
}, 1000)
}
var p = new Person()
function Person() {
this.age = 10
setTimeout(() => {
console.log(this.age) // => 10
}, 1000)
}
var p = new Person()
在上面没有使用箭头函数的例子当中,setTimeout
内部的函数是被global调用的,而global没有age这个属性,因此输出undefined
。
第二个例子使用了箭头函数,this就会使用lexical scope中的this,就是Person,因此输出10。
根据MDN文档,箭头函数的this
值为它被创建时的环境,虽然箭头函数内的this
指向不可通过bind、call、apply
等操作改变,但是外部的环境的this
是可以的,换句话说要想改变箭头函数的this
,只需改变外部环境的this
即可,参考以下的例子:
const obj = {
name: 'this指向obj',
bar() {
const x = () => this
return x
},
}
console.log(obj.bar()())
const a = {name: 'this指向a'}
const b = {name: 'this指向b而非obj和a'}
console.log(obj.bar.bind(b)().call(a))
输出:
6.绑定优先级
- 箭头函数(
() => {}
) - 关键字new调用(
new Person()
) - 显式绑定(
bind/apply/call
) - 隐式绑定(
obj.foo()
) - 默认绑定(
foo()
)
箭头函数优先级最高,会无视2-5绑定规则。而默认绑定优先级最低,只有其他绑定都不使用的时候,才会使用默认绑定。
7. 几个面试题
eg1:
var fullName = 'language'
var obj = {
fullName: 'javascript',
prop: {
getFullName: function () {
return this.fullName
}
}
}
console.log(obj.prop.getFullName())
var test = obj.prop.getFullName
console.log(test())
输出:undefined
language
此题符合第二条规律,方法执行时obj.prop.getFullName()
的getFullName()
前方有点,this
为obj.prop
,输出obj.prop.fullName
即undefined
;test()
前面没有点,this
为window,输出window.fullName
即language
;
eg2:
var val = 1
var json = {
val: 10,
dbl: function () {
val *= 2
}
}
json.dbl()
console.log(json.val + val)
输出:12
此题符合第二条规律,方法执行时json.dbl()
的dbl()
前方有点,this
为json,但是dbl
方法中的val
前面没有this.
,因此其实用的是全局的val
,json.val == 10
,全局的val==2
,那么json.val + val => 12
;
题目变换一下:
var val = 1
var json = {
val: 10,
dbl: function () {
this.val *= 2
}
}
json.dbl()
console.log(json.val + val)
输出:21
var val = 1
var json = {
val: 10,
dbl: function () {
this.val *= 2 // json.val = 20
}
}
json.dbl() // 方法前面有点,this: json
console.log(json.val + val) // 20 + 1 => 21
eg3:
var num = 10
var obj = {num: 20}
obj.fn = (function (num) {
this.num = num * 3
num++
return function (n) {
this.num += n
num++
console.log(num)
}
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)
输出:22
23
65 30
图解: