「彻底弄懂」this全面解析
关于this
this在JavaScript中很常用,关于this,要弄懂this, 首先就要知道this是什么?为什么要用this?
this是什么
当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在
哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在
函数执行的过程中用到。
this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
为什么要用this
this提供一种更优雅的方式来隐式“传递”一个对象的引用,因此可以将API设计得更加简洁且易复用。如果没有提供this,当然我们可以通过传递上下文方式实现。
function sayHi(context) {
var greeting = `hi, I'm ${sayName(context)}`
console.log(greeting)
}
function sayName(context) {
return context.name.toLowerCase()
}
var me = { name: 'Winfar' }
sayHi(me) // hi, I'm winfar
这样未尝不可,但是随着使用模式越来越复杂,显示传递上下文对象会让代码变得越来越混乱,使用this就不会这样。
function sayHi() {
var greeting = `hi, I'm ${sayName.call(this)}`
console.log(greeting)
}
function sayName() {
return this.name.toLowerCase()
}
var me = { name: 'Winfar' }
sayHi.call(me) // hi, I'm winfar
var you = { name: 'Jack' }
sayHi.call(you) // hi, I'm jack
这段代码可以在不同的上下文对象中重复使用 sayHi()和sayName() 函数。
绑定规则
函数执行过程中调用位置会决定this的绑定对象,大致分为如下4种绑定方式:默认绑定、隐式绑定、显式绑定和new绑定。
默认绑定
我们可以把默认绑定规则看作无法应用其他规则时的兜底规则。最常用的独立函数调用就属于这种规则。
function getName() {
console.log(this.name)
}
var name = 'winfar'
getName() // winfar
从结果发现 this.name 指向全局变量name。这里独立函数调用 getName(),属于默认绑定规则,this指向全局对象 window,而全局变量就是全局对象的一个同名属性,所以,this.name等价于 window.name。
如果是在严格模式下,全局对象无法使用默认绑定,this会绑定到 undefined。
function getName() {
'use strict'
console.log(this)
}
getName() // undefined
隐式绑定
某个对象属性引用某个函数,在执行该函数时,this的绑定使用隐式绑定规则,该规则会把函数调用中的 this 绑定到上下文对象。
function getName() {
console.log(this.name)
}
var name = 'winfar'
var obj = {
name: 'jack',
getName: getName
}
obj.getName() // jack
如果对象属性嵌套有多层,被调用函数中的 this 只会指向最后一层对象,可以理解为指向直接调用它的对象。
function getName() {
console.log(this.name)
}
var name = 'winfar'
var obj = {
name: 'jack',
bar: {
name: 'rose',
getName: getName
}
}
obj.bar.getName() // rose
值得注意,单独将对象中的函数提取出来赋值新变量,再执行这个引用变量,this 的隐式模式会丢失。
function getName() {
console.log(this.name)
}
var name = 'winfar'
var obj = {
name: 'jack',
getName: getName
}
var bar = obj.getName
bar() // winfar
bar是 obj.getName的一个引用,实际上它引用的是 getName 函数本身,此时bar()是在不带任何修饰(上下文对象)的函数调用,这里应用了默认绑定规则。
还有比较常见的情况,作为回调函数传入到另外函数中执行,此时回调函数中的 this指向又如何呢?
function getName() {
console.log(this.name)
}
var name = 'winfar'
var obj = {
name: 'jack',
getName: getName
}
function emitFn(fn) {
fn()
}
emitFn(obj.getName) // winfar
setTimeout(obj.getName) // winfar
不管我们将回调函数传入自定义函数 emitFn,还是内置函数 setTimeout,都是应用默认绑定规则,回调函数中的 this都是指向全局对象。
- 在
emitFn中,fn参数是getName函数的一个引用,fn()不带上下文对象的函数调用方式。 - 在
setTimeout内置函数的实现,伪代码类似function setTimeout(fn, delay) {fn()},fn()也是没有上下文对象调用。
于是我们可以总结,obj.[xxx].bar.getName()形式调用,getName中this都指向 bar对象。如果是单独 getName()形式调用,this指向全局对象。
显式绑定
从隐式绑定我们知道,对象内部包含属性引用函数,从而this间接绑定到这个对象上。如果函数不在对象的属性引用中,想在将this强制绑定到该对象,怎么办呢?
JavaScript提供了bind、call和apply函数上的原型方法可以强制将某个对象绑定到this。
function getName() {
console.log(this.name)
}
var name = 'winfar'
var obj = {
name: 'jack',
getName: getName
}
var bar = {
name: 'rose'
}
obj.getName.call(bar) // rose
obj.getName.apply(bar) // rose
var bar = obj.getName.bind(bar)
bar() // rose
如果传入的是一个原始值(String、Boolean或者Number)当做 this的绑定对象,这个原始值会被转换成它的对象形式,也就是new String()、new Boolean()或者new Number(),这通常被称为”装箱“。
function getName() {
console.log(this)
}
var obj = {
getName: getName
}
obj.getName.call(1) // Number {1}
obj.getName.call('winfar') // String {'winfar'}
obj.getName.call(true) // Boolean {true}
当传入的是 null或者undefined,this绑定到全局对象。
obj.getName.call(undefined) // Window
obj.getName.call(null) // Window
new绑定
在JavaScript中,使用new执行一个函数(构造函数),一般的,函数中的this会指向生成的实例对象。
function GetName() {
console.log(this)
}
new GetName() // GetName {}
为什么说”一般“情况下呢?因为当构造返回数据为引用对象时,this指向返回的对象本身。
function GetName() {
console.log(this)
return {name: 'winfar'}
}
new GetName() // {name: 'winfar'}
下面来模拟实现new操作符
- 首先创建一个对象,对象原型指向构造函数原型;
- 其次调用构造函数,并将
this绑定到该对象; - 最后构造函数执行返回值,如果是非引用类型,返回创建的对象,否则直接返回构造函数的返回值;
function myNew(Fn) {
// ES6 中 new.target 指向构造函数
myNew.target = Fn
// const obj = {}
// obj.__proto__=Fn.prototype
// 创建一个对象,对象原型指向构造函数原型
const obj = Object.create(Fn.prototype)
// 调用构造函数,并将this绑定到该对象
const result = Fn.apply(obj, [...arguments])
// 构造函数执行返回值,如果是非引用类型,返回创建的对象,否则直接返回构造函数的返回值
const type = typeof result
return (type === 'object' && result !== null) || type === 'function' ? res : obj
}
规则优先级
以上我们了解了4种this的绑定规则,那么它们的优先级又如何呢?
首先来看隐式绑定和显示绑定的优先级
function getName() {
console.log(this.name)
}
var obj = {
name: 'winfar',
getName: getName
}
var bar = {
name: 'jack',
getName: getName
}
obj.getName() // winfar
bar.getName() // jack
obj.getName.call(bar) // jack
bar.getName.call(obj) // winfar
可以看到显示绑定的优先级比隐式绑定更高。 再来比较隐式绑定与new绑定的优先级
function getName(name) {
this.name = name
}
var obj = {
getName: getName
}
obj.getName('winfar')
var bar = new obj.getName('jack')
console.log(bar.name) // jack
console.log(obj.name) // winfar
new绑定比隐式绑定优先级高。
显式绑定与new绑定优先级又如何呢?
new操作符与call、apply无法一起使用,比如new obj.getName.call('winfar')。但是bind可以
function getName(name) {
this.name = name
}
var obj = {}
var fn = getName.bind(obj)
fn('winfar')
console.log(obj.name) // winfar
var bar = new fn('jack')
console.log(bar.name) // jack
console.log(obj.name) // winfar
由上面可以看出,bind将this绑定到obj对象上,并给obj对象添加属性name,在new执行函数后this没有执行obj对象,而是新生成一个对象并添加name属性。从而得出new绑定优先级比显示绑定高。
综上可知,this的4种绑定优先级顺序依次为 new绑定 > 显示绑定 > 隐式绑定 > 默认绑定。

箭头函数中this
在ES6中使用箭头=>函数简化function关键字,它不适用上面this的四种绑定规则,这里我们顺便回顾一下箭头函数的几个使用注意点。
(1)箭头函数没有自己的this对象。
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
针对于箭头函数没有自己的this对象,根据外层作用域(函数或者全局)来决定this,来看下面这个例子。
function getName() {
return () => {
console.log(this.name)
}
}
var name = 'winfar'
var obj = {
name: 'jack'
}
var bar = {
name: 'rose'
}
var fn = getName.call(obj)
fn.call(bar) // jack
getName函数内部创建的箭头函数会捕获调用时getName的this,由于getName在调用时this绑定了obj对象,箭头函数中的this绑定obj对象,即使后面显示绑定其他对象执行也不能改变它的指向。
还有一点值得注意的,如果箭头函数外层作用域this指向是变化的,箭头函数内部this也会跟着变化。
function getName() {
return () => {
console.log(this.name)
}
}
var name = 'winfar'
var obj = {
name: 'jack'
}
var bar = {
name: 'rose'
}
var fn = getName.call(obj)
fn() // jack
var fn1 = getName.call(bar)
fn1() // rose
第一次getName函数中this绑定obj,内部箭头函数的this继承外层作用域this,fn是内部箭头函数引用,在执行fn时,这里是默认绑定,但是不适用箭头函数,箭头函数内部this仍是外层this。
同理,第二次getName函数中this绑定bar,fn1箭头函数内部this也是继承于外层this。
this指向判断流程
我们已经知道了this指向的四种绑定规则和箭头函数中的this绑定。现在可以从整体来看this指向的判断流程
- 箭头函数内的
this继承外层作用域的this; - 函数通过
new调用,this绑定新创建的对象; - 函数通过
bind绑定或者call``apply调用,this指向被绑定对象(非undefined、null); - 函数通过某个上下文对象调用,
this绑定该上下文对象; - 在严格模式下,
this指向undefined,否则绑定到Window对象;

分析一道综合题
实践是检验真理的唯一标准,接下我们再来看一道综合题检验一下我们的学习成果。
var age = 1
var obj = {
age: 2,
getAge: function() {
var age = 3
this.age *= 2
age *= 3
return () => {
var g = this.age
this.age *= 4
console.log(g)
age *= 5
console.log(age)
}
}
}
var fn = obj.getAge
var bar = fn.call(null)
bar.call(obj)
console.log(window.age)
fn是getAge函数引用,fn.call(null)将函数getAge的this显式绑定到window对象,此时作用域中变量分布情况
// 全局作用域
this = window
age = 1
// getAge函数作用域
this = window
age = 3
this.age *= 2改变的是全局的age,age *= 3改变的是getAge函数局部变量age
// 全局作用域
this = window
age = 1 * 2 = 2
// getAge函数作用域
this = window
age = 3 * 3 = 9
getAge函数中的箭头函数中this继承getAge函数this,bar是箭头函数的引用,bar.call(obj)虽然将this显示绑定到obj,但是箭头函数不适用该绑定原则,依旧是getAge函数this。
// 全局作用域
this = window
age = 2
// getAge函数作用域
this = window
age = 9
// 箭头函数作用域
this = window
g = 2
箭头函数中this.age *= 4改变的全局age,箭头函数内没有声明自己的环境变量age,继承getAge函数变量,age *= 5改变的是外层函数变量
// 全局作用域
this = window
age = 2 * 4 = 8
// getAge函数作用域
this = window
age = 9 * 5 = 45
// 箭头函数作用域
this = window
g = 2
所以,最终结果是 2 45 8。还有很多变种,比如
obj.getAge().call(obj)
console.log(window.age)
结果又是怎样呢?哈哈哈,留给大家自己思考了。完~
转载自:https://juejin.cn/post/7166136148660584461