likes
comments
collection
share

「彻底弄懂」this全面解析

作者站长头像
站长
· 阅读数 38

关于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

barobj.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()形式调用,getNamethis都指向 bar对象。如果是单独 getName()形式调用,this指向全局对象。

显式绑定

从隐式绑定我们知道,对象内部包含属性引用函数,从而this间接绑定到这个对象上。如果函数不在对象的属性引用中,想在将this强制绑定到该对象,怎么办呢? JavaScript提供了bindcallapply函数上的原型方法可以强制将某个对象绑定到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

如果传入的是一个原始值(StringBoolean或者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或者undefinedthis绑定到全局对象。

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操作符与callapply无法一起使用,比如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

由上面可以看出,bindthis绑定到obj对象上,并给obj对象添加属性name,在new执行函数后this没有执行obj对象,而是新生成一个对象并添加name属性。从而得出new绑定优先级比显示绑定高。 综上可知,this的4种绑定优先级顺序依次为 new绑定 > 显示绑定 > 隐式绑定 > 默认绑定。 「彻底弄懂」this全面解析

箭头函数中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函数内部创建的箭头函数会捕获调用时getNamethis,由于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继承外层作用域thisfn是内部箭头函数引用,在执行fn时,这里是默认绑定,但是不适用箭头函数,箭头函数内部this仍是外层this。 同理,第二次getName函数中this绑定barfn1箭头函数内部this也是继承于外层this

this指向判断流程

我们已经知道了this指向的四种绑定规则和箭头函数中的this绑定。现在可以从整体来看this指向的判断流程

  • 箭头函数内的this继承外层作用域的this
  • 函数通过new调用,this绑定新创建的对象;
  • 函数通过bind绑定或者call``apply调用,this指向被绑定对象(非undefinednull);
  • 函数通过某个上下文对象调用,this绑定该上下文对象;
  • 在严格模式下,this指向undefined,否则绑定到Window对象;

「彻底弄懂」this全面解析

分析一道综合题

实践是检验真理的唯一标准,接下我们再来看一道综合题检验一下我们的学习成果。

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)

fngetAge函数引用,fn.call(null)将函数getAgethis显式绑定到window对象,此时作用域中变量分布情况

// 全局作用域
this = window
age = 1

// getAge函数作用域
this = window
age = 3

this.age *= 2改变的是全局的ageage *= 3改变的是getAge函数局部变量age

// 全局作用域
this = window
age = 1 * 2 = 2

// getAge函数作用域
this = window
age = 3 * 3 = 9

getAge函数中的箭头函数中this继承getAge函数thisbar是箭头函数的引用,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
评论
请登录