「彻底弄懂」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