js篇连载之第二期 来认真的探讨一下this问题,以及call、apply等的手写

673 阅读10分钟

this指向问题

一个堕落者的自我救赎之路

深夜赶文,如有误,望指教 。感恩!

临渊羡鱼,不如退而结网。愿我们都有一个美好的前程

此系列博客文章进度

✅js篇

🔲html、css篇

🔲DOM、BOM篇

🔲轮子篇

🔲打包工具篇

🔲数据结构篇

🔲前端必刷小算法篇

🔲HTTP篇

🔲性能优化篇

🔲常见设计模式篇

从一段代码开始(缘起缘落开始便是结束)

小黑:this的指向问题还用说嘛,它不就指向调用它的那个对象吗

小白:黑哥莫急,话是这样说。咱还是先看几段代码

var name = 'gxb'
const obj1 = {
    name'zs',
    getNamefunction() {
        const fn = obj2.getName
        fn()
    }
}
const obj2 = {
    name'zs',
    getNamefunction() {
        console.log(this.name)
    }
}

obj1.getName()
var name='gxb'
const obj1={
  name:'zs',
  obj2:{
    name:'li',
    getName:function(){
      console.log(this.name)
    }
  }
}

const fn=obj1.obj2.getName
fn()

上面两段的最后输出都是gxb,黑哥你能一眼就能准确的判断出来吗?

小黑:你这不是故意刁难吗,哪有代码写成你这个样子的?

小白:嘿嘿,黑哥别生气。这里主要是看看黑哥this指向掌握的怎么样,其实只要抓住了this问题的命脉,这里只要瞅上一样就能看出答案来的

小黑:少废话,快点开始正题

小白:

首先来解释这一句话this就指向调用它的那个对象,不知道你有没有和小白开始一样的疑惑,this指向调用它?为什么是调用它呢?this又不是一个函数何谈调用呢?

这个问题困扰了小白好长一段时间,直到小白js的执行机制

即开始的时候js代码从宏任务队列进入执行栈,开始创建的全局上下文只有两个东西,即全局对象和this。这个this是指向全局对象的

当一个函数进栈的时候,又创建了一个函数上下文。函数上下文不会创建全局对象而是创建参数对象即 arguments 和this。这个this则是在执行期间才能确定

全局上下文中的this我们不用去处理,通常我们要搞清楚的就是这个函数上下文的this指向问题。正是由于这种函数与this的关系,所以你应该就能理解这句话了吧this就指向调用它的那个对象,这个它可以理解为就是指向这个this所在函数上下文的那个函数(额,有点绕口。哈哈)

再向下解释,this是在运行时才能确定的。也就是说和它所在的词法作用域没有半毛钱关系

怎么理解呢?还是看这段代码

var name='gxb'
const obj1={
  name:'zs',
  obj2:{
    name:'li',
    getName:function(){
      console.log(this.name)
    }
  }
}

const fn=obj1.obj2.getName
fn()

按传统的作用链去分析,getName这个函数中没有找到name变量,按理说他会向上级去找,找到了name='li',再不济再向上找还能找到name='zs'。怎么也不能跨过中间直接就去拿全局的name

但是呢,this就是这样它不依赖于它所在的词法作用域。我们只关心是谁调用了它

是谁呢?通过观察上面代码,首先我们先把这个getName赋给了fn,fn直接在全局中调用(a.fn即a调用了fn,没有修饰符即window调用的 非严格模式下)

即拿到的是处于全局下的name

小黑:很就没有看到你使用var了,这里咋又用上了,怀旧呢?

小白:黑哥,这我就得说你两句了。我前两天刚总结了一下var、let、const的区别,你是不是没看。这三个只要var声明的变量能被直接挂到全局window上,我倒是想用let呢。。。

绑定规则

小黑:this还有绑定规则呢?原来没有注意过啊

小白:对啊黑哥,而且这里的绑定规则还不止一个呢。接下来让小弟一一给您解释一下吧

1 默认绑定

这个便是最简单的一种绑定规则了

举个栗子

window.name = 'gxb'

function getName() {
    console.log(this.name)
}
getName()

这种函数前面没有任何修饰直接调用的,其使用的绑定规则就是默认绑定(非严格模式)

2 隐式绑定

这种也十分容易理解,即像这么栗子调用getName函数的对象具有上下文。故getNamethis就指向这个obj的上下文了

举个栗子

const obj = {
    name'gxb',
    getName: getName
}

function getName() {
    console.log(this.name)
}
obj.getName()

再来一个栗子,来体验一把隐式绑定和this就是指向调用它的那个对象这句话

const obj01 = {
    name'gxb',
    obj02: {
        name'zs',
        getNamefunction() {
            console.log(this.name)
        }
    }
}

obj01.obj02.getName()

直接去看是谁调用了getName呢?哦,原来是obj01.obj02这个对象。故this就指向obj01.obj02这个对象的上下文了。这个上下文中找得name变量即zs

再来介绍一个名词——隐式丢失

小黑:这个是啥玩意呢?

小白:哈哈。黑哥不要害怕。这个名词也就是一个名词而已,其实开始我们就接触过了

var name='gxb'
const obj1={
  name:'zs',
  obj2:{
    name:'li',
    getName:function(){
      console.log(this.name)
    }
  }
}

const fn=obj1.obj2.getName
fn()

这个就算是隐式丢失,你本来好好的我可以用obj1.obj2.getName()去调用,直接可以让this绑定obj1.obj2对象的上下文。你非把它给赋值一个新的变量去调用。即此时的fn就是getName函数,你这样fn()不就相当于getName()前面没有修饰,不就恰好匹配到了默认绑定的规则,直接把this绑定到window对象的上下文中了吗(非严格模式)

还有一些隐式丢失的栗子,我就只放到下面不解释了

window.name = 'gxb'
const obj = {
    name'zs',
    foo1
}

function foo1() {
    console.log(this.name)
}

function foo2(fn) {
    fn()
}
foo2(obj.foo1)
window.name = 'gxb'
const obj = {
    name'zs',
    foo1
}

function foo1() {
    console.log(this.name)
}

setTimeout(obj.foo11000)

这俩最后都指向了全局对象的上下文

3 显示绑定

拿上面隐式绑定的第一个栗子开头

const obj = {
    name'gxb',
    getName: getName
}

function getName() {
    console.log(this.name)
}
obj.getName()

在这个栗子中,我们想让函数getName的this指向对象obj的上下文,还需要在obj对象中搞一个方法属性用于引用这个getName函数

这样是不是有些麻烦呢?

这个时候就要引出三个我们非常熟悉,且非常重要的函数了。分别是call、apply、bind

用这三个函数可以明确指定this的指向,故称其为显示绑定

栗子:直接把getName函数中的this指向obj对象的上下文中

const obj = {
    name'gxb'
}

function getName() {
    console.log(this.name)
}
getName.call(obj)

小黑:这三个函数谁不会使喔,给我实现一下吧

小白:遵命,我亲爱的黑哥

手写一些call、apply、bind

其实它们的实现都是基于隐式绑定的,知道了上面的东西这里就十分简单了。

首先这三个函数为保证所有函数均能使用,我们直接将他写到函数构造函数的原型对象上吧

call

简单分析,call函数需要传入所要绑定对象和一些数据参数,返回的是调用call的那个函数的执行结果

Function.prototype.myCall = function(context = window) {
    context.fn = this
    let res = context.fn(...[...arguments].slice(1))
        // 用完了fn,还要卸磨杀驴。我们不能给人家传进来的对象瞎加东西啊
    delete context.fn
    return res

}
apply

与call的区别在于,第二个参数是一个数组

Function.prototype.myApply = function(context = window, arr) {
    context.fn = this

    // 这里多了一步判断arr,要是arr没有我们就不传给this了,这个this指的谁呢?要绑定的那个函数啊,黑哥思考一下下面调用的时候为啥不这样this()?
    //白小子快点写,隐式丢失刚小爷可没忘
    let res
    if (arr) {
        res = context.fn(...arr)
    } else {
        res = context.fn()
    }
    delete context.fn
    return res
}

bind

与上面两的区别在于不是立即执行调用bind的那个函数,而是把执行时机放到返回的那个函数调用执行时

用一下上面写好的没有myApply

Function.prototype.myBind = function(context = window) {
    let that = this
    let args = [...arguments].slice(1)
    return function() {
        return that.myApply(context, args)
    }
}

4 new绑定

new绑定就不用多说了吧,咱上一期刚好有说过new

new做的事情

  1. 首先创建一个新的对象
  2. 链接到原型
  3. 绑定this
  4. 返回这个新的对象

new的实现如果忘了的话就回头看看吧

箭头函数中的this问题

小黑:箭头函数的this有什么问题吗

小白:黑哥,这你就孤陋寡闻了吧。它可不和传统函数一样。可以这理解它本身是没有this的,但是却可以捕获它父级作用域的this来使用

接着看栗子

var name='gxb'
const obj={
  name:'zs',
  getName:function(){
    console.log(this.name)
  }
}
obj.getName()

这个我们一眼就能看出了,最后输出的是zs

这样呢?

var name='gxb'
const obj={
  name:'zs',
  getName:()=>{
    console.log(this.name)
  }
}
obj.getName()

哈哈,我们分析一下。上面说了箭头函数本身是没有this的,但是却可以捕获它父级作用域的this来使用

来根据代码来看首先箭头函数getName没有this,它的this是用的它父级的,即向上级作用域去找,它的上级作用域是一个块级作用域。块级的东西也不配有this,再向上找,到了全局,全局上下文可有this,且this指向的是全局变量window的上下文(浏览器环境)即最后输出gxb

缘灭也即缘起

this的指向就是调用它的那个对象开始,同样以this的指向就是调用它的那个对象结束

写到最后

星光不问赶路人,时光不负有心人

期待着我们的下一次邂逅