共计 8923 个字符,预计需要花费 23 分钟才能阅读完成。
这是一个前端面试常常考的根底考点,很多初学者在这个问题上都容易踩坑,包含我也是经常性蒙圈。所以这次决定将他们梳理下来,加深本人的了解。如果有出错的中央,欢送斧正。
this 是什么
this 关键字是 Javascript ES5 中最简单的机制之一。ES6 中新增的箭头函数,很大水平上防止了应用 this 所产生的谬误。然而在 ES5 中,有时候咱们会谬误的判断了 this 的指向。其实对于 this 的指向,始终保持一个原理:this 永远指向最初调用它的那个对象,记住了这句话,this 的指向你曾经理解一半了。
想要理解 this 的指向,咱们首先要理解 this 的四种绑定形式:隐式绑定、显示绑定、window 绑定、new 绑定。
this 的四种绑定形式
隐式绑定
执行绑定的第一个也是最常见的规定为 隐式绑定,它 80% 的状况下会通知你 this 指向的对象是什么。
咱们先来看一个简略的例子:
const user = {
name: 'Cherry',
age: 27,
getName() {console.log(`Hello, my name is ${this.name}`)
}
}
user.getName() // Hello, my name is Cherry
当咱们执行 user.getName()
时,会打印出Hello, my name is Cherry
。
如果你要调用 user 对象上的 getName 办法,你会用到点.
这就是所谓隐式绑定,函数被调用时先看一看点号左侧。如果有“点”就查看“点”左侧的对象,这个对象就是 this 的援用。
在下面的例子中,user 在“点号左侧”意味着 this 援用了 user 对象。所以就如同 在 getName 办法的外部 JavaScript 解释器把 this 变成了 user。
所以,你能够得出这样的论断:应用对象来调用其外部的一个办法,该办法的 this 是指向对象自身的 。这就是所谓隐式绑定,你也能够这样认为:JavaScript 解释器在执行 user.getName()
时,将其转化为了:
user.getName.call(user);
咱们将代码减少一层调用:
const user = {
name: 'Cherry',
age: 27,
getName() {console.log(`Hello, my name is ${this.name}`)
},
mother: {
name: 'Susan',
getName() {console.log(`Hello, my name is ${this.name}`)
}
}
}
user.getName() // Hello, my name is Cherry
user.mother.getName() // Hello, my name is Susan
正如方才所说:this 永远指向最初调用它的那个对象,那么“点”左侧的对象即为后调用该办法的对象,this 指向该对象。然而,如果没有点呢?这就为咱们引出了下一条规定:
显示绑定
对于显示绑定,咱们能够通过 call 来设置函数执行上下文的 this 指向,比方上面这段代码:
function getName () {console.log(`Hello, my name is ${this.myName}`)
}
let user = {
myName: 'Cherry',
age: 27,
}
getName.call(user) // Hello, my name is Cherry
执行这段代码,而后察看输入后果,你会发现 getName 函数外部的 this 曾经指向了 user 对象。
其实除了 call 办法,咱们还能够应用 bind 和 apply 办法来设置函数执行上下文中的 this,它们在应用上有一些区别,文章的第六大节会对 call、apply、bind 进行具体的介绍,这里我就不过多赘述了。
window 绑定
咱们在方才的例子的根底上批改一下:
function getName () {console.log(`Hello, my name is ${this.myName}`)
}
let user = {
myName: 'Cherry',
age: 27,
}
getName();
置信大家都晓得为什么打印进去的是 My name is undefined,因为正如后面所说的,如果你想用 user 做上下文调用 getName,你能够应用 .call、.apply 或 .bind。但如果咱们没有用这些办法,而是间接和平时一样间接调用,JavaScript 会默认 this 指向 window 对象。然而 window 对象中并没有 myName 属性,所以会打印 “My name is undefined“。
在 ES5 增加的 严格模式 中,JavaScript 不会默认 this 指向 window 对象,而会正确地把 this 放弃为 undefined。
例如:
'use strict'
age = 27
function sayAge () {console.log(`Hello, my age is ${this.age}`)
}
sayAge() // TypeError: Cannot read property 'age' of undefined
new 绑定
第四条判断 this 援用的规定是 new 绑定。每当用 new 调用函数时,JavaScript 解释器都会在底层创立一个全新的对象并把这个对象当做 this。
这看起来就像创立了新的函数,但实际上 JavaScript 函数是从新创立的对象。
例如:
function User (name, age) {
/*
JavaScript 会在底层创立一个新对象 `this`,它会代理不在 User 原型链上的属性。如果一个函数用 new 关键字调用,this 就会指向解释器创立的新对象。*/
this.name = name
this.age = age
}
const me = new User('Cherry', 27)
伪代码示意:
var me = new User("Cherry","27");
new User{var object = {};
object.__proto__ = User.prototype;
var result = User.call(object,"Cherry","27");
return typeof result === 'object'? result : object;
}
new 的过程:
- 创立一个空对象 object;
- 将新创建的空对象的隐式原型指向其构造函数的显示原型;
- 应用 call 扭转 this 的指向;
- 如果无返回值或者返回一个非对象值,则将 object 返回作为新对象;如果返回值是一个新对象的话那么间接间接返回该对象。
所以咱们能够看到,在 new 的过程中,其实是应用 call 扭转了 this 的指向。
this 的指向
后面讲了对于 this 的四种绑定形式,咱们对于 this 的指向应该也有了一些本人的了解,还记得咱们之前说的吗?this 永远指向最初调用它的那个对象,咱们记好这句话来练习上面的例子:
练 1:
var name = "window";
function fn() {
var name = "Cherry";
console.log(this.name); // window
console.log("inner:" + this); // inner: Window
}
fn();
console.log("outer:" + this) // outer: Window
咱们看最初调用 fn 的中央 fn();
,后面没有“点”,Javascript 调用的对象默认指向了全局对象 window,这就相当于是 window.fn();
所以依据刚刚的那句话“this 永远指向最初调用它的那个对象”,this 指向的就是 window。绑定规定是 Window 绑定。
留神,这里咱们没有应用严格模式,如果应用严格模式的话,全局对象就是 undefined,那么就会报错 Uncaught TypeError: Cannot read property ‘name’ of undefined。
练 2:
var name = "window";
var user = {
name: "Cherry",
fn: function () {console.log(this.name); // Cherry
}
}
user.fn();
依据上文所说,咱们看到函数 fn 左侧有“点”,“点”的左侧是 user,所以 fn 是对象 user 调用的。所以打印的值就是 user 中的 name 的值。绑定规定是隐式绑定。
练 3:
var name = "window";
function fnA(){
var name = "Cherry";
function fnB(){console.log(this.name); // window
}
// 在 A 函数外部调用 B 函数
fnB();}
// 调用 A 函数
fnA();
嵌套函数中的 this 不会从外层函数中继承。在函数执行环境中应用 this 时, 如果函数没有显著的作为非 window 对象的属性,而只是定义了函数,这个函数中的 this 依然默认指向 window 对象。
练 4:
var name = "window";
var user = {
name: "Cherry",
fn: function () {console.log(this.name); // Cherry
}
}
window.user.fn();
这里打印 Cherry 的起因也是因为刚刚那句话“this 永远指向最初调用它的那个对象”,最初调用它的对象依然是对象 user。
咱们改变一下:
var name = "window";
var user = {
// name: "Cherry",
fn: function () {console.log(this.name); // undefined
}
}
window.user.fn();
这是因为调用 fn 的是 user 对象,也就是说 fn 的外部的 this 是对象 user,而对象 user 中并没有对 name 进行定义,所以 log 的 this.name 的值是 undefined。
这个例子还是阐明了:this 永远指向最初调用它的那个对象,因为最初调用 fn 的对象是 user,所以就算 user 中没有 name 这个属性,也不会持续向上一个对象寻找 this.name,而是间接输入 undefined。
练 5:(这个例子稍稍有点坑)
var name = "window";
var user = {
name : null,
// name: "Cherry",
fn : function () {console.log(this.name); // window
}
}
var f = user.fn;
f();
这里你可能会有疑难,为什么不是 Cherry?因为这里尽管将 user 对象的 fn
办法赋值给变量 f
了,然而 没有调用,再接着跟我念这一句话:“this 永远指向最初调用它的那个对象”,因为刚刚的 f
并没有调用,所以 fn()
最初依然是被 window 调用的。所以 this 指向的也就是 window。
由以上五个练习咱们能够看出,this 的指向并不是在创立的时候就能够确定的,在 es5 中,this 永远指向最初调用它的那个对象。
如何扭转 this 的指向
扭转 this 的指向我总结有以下几种办法:
- 应用 ES6 的箭头函数
- 在函数外部应用 _this = this
- 应用 apply、call、bind
- new 实例化一个对象
咱们看上面的例子:
var name = "window";
var user = {
name : "Cherry",
fn1: function() {console.log(this.name)
},
fn2: function() {setTimeout(function () {this.fn1()
},100);
}
};
user.fn2() // this.fn1 is not a function
咱们逐个细说一下这个例子:fn2()
是被 user调用的,所以 fn2
中的 this 应该指向 user。然而 fn2
中又调用了 window 中的 setTimeout 办法。所以在 setTimeout 办法中的 this 指向的是后调用它的对象 window。然而在 window 中并没有 fn1 函数。所以抛出谬误:this.fn1 is not a function。
如果咱们想正确的调用 user 中的 fn1()
,应该怎么做呢?咱们把这个例子作为 demo 进行革新。
箭头函数
家喻户晓,ES6 的箭头函数是能够防止 ES5 中应用 this 的坑的。“所有的箭头函数都没有本人的 this,都指向外层。”– 这句话就是箭头函数的精华。箭头函数的 this,总是指向定义时所在的对象,而不是运行时所在的对象。这句话说的太含糊了,最好改成:总是指向所在函数运行时的 this。
下面例子咱们应用 箭头函数 扭转 this 的指向如下:
var name = "window";
var user = {
name : "Cherry",
fn1: function () {console.log(this.name)
},
fn2: function () {setTimeout( () => {this.fn1()
},100);
}
};
user.fn2() // Cherry
对于箭头函数,咱们还须要留神以下几点:
- 函数体内的 this 就是定义时所在的对象,而非调用时所在的对象,和一般函数相同。
- 箭头函数无奈用做构造函数,即不能应用 new 调用
- 不能应用 arguments 对象,函数中不存在这个对象。
- 不可应用 yield 命令,即无奈用做 Generator 函数。
其中第一点尤其值得注意,之所以 this 是固定的,是因为箭头函数自身没有 this,箭头函数的 this 不是本人的。所以不能批改,也正因为没有 this,所以不能用作构造函数。这些限度都是因为没有 this 导致的。
在函数外部应用 _this = this
如果不应用 ES6,那么这种形式应该是最简略的不会出错的形式了,咱们是先将调用这个函数的对象保留在变量 _this 中,而后在函数中都应用这个 _this,这样 _this 就不会扭转了。
var name = "window";
var user = {
name : "Cherry",
fn1: function () {console.log(this.name)
},
fn2: function () {
var _this = this;
setTimeout(function() {_this.fn1()
},100);
}
};
user.fn2() // Cherry
这个例子中,在 fn2 中,首先设置 var _this = this;,这里的 this 是调用 fn2
的对象 user,为了避免在 fn2
中的 setTimeout 被 window 调用而导致的在 setTimeout 中的 this 为 window。咱们将 this(指向变量 user) 赋值给一个变量 _this,这样,在 fn2
中咱们应用 _this 就是指向对象 user 了。
应用 apply、call、bind
应用 apply、call、bind 函数也是能够扭转 this 的指向的,成为显示绑定,咱们先来看一下是怎么实现的:
应用 apply()
var user = {
name: "Cherry",
fn1: function() {console.log(this.name)
},
fn2: function() {setTimeout(function () {this.fn1()
}.apply(user), 100);
}
};
user.fn2() // Cherry
应用 call()
var user = {
name: "Cherry",
fn1: function() {console.log(this.name)
},
fn2: function() {setTimeout(function () {this.fn1()
}.call(user), 100);
}
};
user.fn2() // Cherry
应用 bind()
var user = {
name: "Cherry",
fn1: function() {console.log(this.name)
},
fn2: function() {setTimeout(function () {this.fn1()
}.bind(user)(), 100);
}
};
user.fn2() // Cherry
apply、call、bind 的区别
刚刚咱们曾经介绍了 apply、call、bind 都是能够扭转 this 的指向的,然而这三个函数稍有不同。
在 MDN 中定义 apply 如下;
apply() 办法调用一个函数, 其具备一个指定的 this 值,以及作为一个数组(或相似数组的对象)提供的参数
apply 和 call 的区别
其实 apply 和 call 根本相似,他们的区别只是传入的参数不同。
call 的语法为:
fun.call(thisArg[, arg1[, arg2[, ...]]])
所以 apply 和 call 的区别是 call 办法承受的是若干个参数列表,而 apply 接管的是一个蕴含多个参数的数组。
apply()的应用办法:
var user ={
name: "Cherry",
fn: function(a,b) {console.log(a + b)
}
}
var newUser = user.fn;
newUser.apply(user,[1,2]) // 3
call()的应用办法:
var user ={
name: "Cherry",
fn: function(a,b) {console.log(a + b)
}
}
var newUser =user.fn;
newUser.call(user, 1, 2) // 3
但凡事都有例外:
若将 null、undefined 等值作为 call、apply 的第一个参数,那么理论调用时会被疏忽,从而利用到 Window 绑定规定,即绑定到 window 上,有些时候咱们不关怀上下文,只关怀参数时,能够这样做。
但这样其实存在这一些潜在的危险,绑定到 window 很可能无心中增加或批改了全局变量,造成一些荫蔽的 bug。所以为了避免这种状况呈现,能够将第一个参数绑定为一个空对象。当然具体还是看需要,这只是倡议。
bind 和 apply、call 区别
咱们先应用 bind 试一下刚刚的例子:
var user ={
name: "Cherry",
fn: function(a,b) {console.log(a + b)
}
}
var newUser = user.fn;
nreUser.bind(user,1,2)
咱们会发现并没有输入,这是为什么呢,咱们来看一下 MDN 上的文档阐明:
bind()办法创立一个新的函数, 当被调用时,将其 this 关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
所以咱们能够看出,bind 是创立一个新的函数,咱们必须要手动去调用:
var user ={
name: "Cherry",
fn: function (a,b) {console.log( a + b)
}
}
var newUser = user.fn;
newUser.bind(user,1,2)() // 3
以上就是三种显示绑定的办法,但有三点须要留神:
- call 和 apply 是立刻执行,bind 则是返回一个绑定了 this 的新函数,只有你调用了这个新函数才真的调用了指标函数
- bind 函数存在屡次绑定的问题,如果屡次绑定 this,则以第一次为准。
- bind 函数实际上是显示绑定(call、apply)的一个变种,称为 硬绑定。因为硬绑定是一种十分罕用的模式,所以在 ES5 中提供了内置的办法
Function.prototype.bind
为什么屡次应用 bind 绑定 this,以第一次为准呢?咱们看上面的例子:
function foo() {console.log( this.name);
}
var obj1 = {name: 'obj1'};
var obj2 = {name: 'obj2'}
var fn = foo.bind(obj1).bind(obj2)
fn() // => 'obj1'
fn.call(obj2) // => 'obj1'
也就是说 bind 函数只能绑定一次,屡次绑定是没有用的,绑定后的函数 this 无奈扭转,即便 call/apply 也不行,所以才称作硬绑定。
但凡事总有例外,且看 new 绑定。
绑定的优先级
如果显示绑定和 new 绑定同时存在,或者更宽泛的说:在某个调用地位多条绑定规定同时存在怎么办呢?为了解决这个问题就必须给这些规定设定优先级,这就是咱们接下来要介绍的内容。
毫无疑问,Window 绑定的优先级是最低的,显式绑定和隐式绑定的优先级,通过下面的例子也能够证实,显式大于隐式。所以目前程序是:显式 > 隐式 > Window
那咱们来测试下显示绑定和 new 绑定的优先级程序。因为 call/apply 无奈和 new 一起应用,咱们能够应用 bind(硬绑定)来验证。
function foo() {this.name = 'Cherry';}
var obj = {name: 'obj'};
var fn = foo.bind(obj)
var result = new fn()
console.log(obj.name) // => 'obj'
console.log(result.name) // => 'Cherry'
不言而喻的,new 的优先级,大于显示绑定。最终程序为:new > 显式 > 隐式 > Window
。
于是咱们判断 this,就有了一个程序:
- 函数是否在 new 中调用?
- 是否通过 call、apply、bind 等调用?
- 是否在某个上下文对象中调用?
- 都不是则是 Window 绑定。且严格模式下绑定到 undefined。
小结
- this 的四种绑定形式:隐式绑定、显示绑定、window 绑定、new 绑定
-
扭转 this 的指向有以下几种办法:
- 箭头函数
- 在函数外部应用 _this = this
- 应用 apply、call、bind
- 应用 new
-
判断 this 次要有以下步骤:
- 函数是否在 new 中调用?
- 是否通过 call、apply、bind 等调用?
- 是否在某个上下文对象中调用?
- 都不是则是默认绑定。且严格模式下绑定到 undefined。
- 绑定优先级:new > 显式 > 隐式 > Window
另外还要留神箭头函数的特殊性、在 call/apply 中应用 undefined 和 null 会被疏忽这一个性、bind 的硬绑定以及:this 永远指向最初调用它的那个对象。
以上就是对于判断 this 指向的总结,理解了以上几个关键点,this 的指向你曾经很理解啦~
对于
作者齐小神,前端程序媛一枚。
有点文艺,喜爱摄影。尽管当初朝九晚五,埋头苦学,但幻想是做女侠,扶贫济穷,仗剑走咫尺。心愿有一天能改完 BUG 去实现本人的幻想。
公众号:大前端 Space,不定时更新,欢送来玩~