乐趣区

关于javascript:从底层理解this是什么

何为 this

对于 this,我提出了上面几个问题:

  • this 寄存在哪里?
  • this 是如何呈现,又是如何隐没的?
  • this 有什么作用?

要齐全明确这些,咱们要先了解 JavaScript 执行上下文和调用栈是什么

执行上下文

为清晰讲述 this 联合了《浏览器工作原理与实际》局部内容

咱们先看下这段代码的函数调用过程:

var a = 2
function add(){
    var b = 10
    return  a+b
}
add()

这段代码很简略,先是创立了一个 add 函数,接着在代码的最上面又调用了该函数。

在执行到函数 add() 之前,也就是第 6 行之前,JavaScript 引擎会为下面这段代码创立全局执行上下文,蕴含了申明的函数和变量,你能够参考下图:

从图中能够看出,代码中全局变量和函数都保留在全局上下文的变量环境中。

执行上下文筹备好之后,便开始执行全局代码,当执行到 add 这儿时,JavaScript 判断这是一个函数调用,那么将执行以下操作:

  • 首先,从全局执行上下文中,取出 add 函数代码。
  • 其次,对 add 函数的这段代码进行编译,并创立该函数的执行上下文和可执行代码。
  • 最初,执行代码,输入后果。

就这样,当执行到 add 函数的时候,咱们就有了两个执行上下文了——全局执行上下文和 add 函数的执行上下文。

也就是说在执行 JavaScript 时,可能会存在多个执行上下文,那么 JavaScript 引擎是如何治理这些执行上下文的呢?

答案是 通过 JavaScript 调用栈来治理的,接下来咱们来看下什么是 JavaScript 调用栈。

JavaScript 调用栈

咱们晓得,JavaScript 执行过程中,内存空间次要分为栈空间和堆空间(代码空间先不必管)。

什么是 JavaScript 的调用栈:代码执行过程中,JavaScript 引擎会将执行上下文压入栈空间中,通常把这种用来治理执行上下文的栈称为执行上下文栈,又称调用栈。

接下来咱们一步步地剖析在上面代码的执行过程中,JavaScript 调用栈的状态变动状况:

var a = 2

function add(b,c){return b+c}

function addAll(b,c){
    var d = 10
    result = add(b,c)
    return  a+result+d
}

addAll(3,6)

第一步,创立全局上下文,并将其压入栈底。
如下图所示:

从图中你也能够看出,变量 a、函数 add 和 addAll 都保留到了全局执行上下文的变量环境对象中。

全局执行上下文压入到调用栈后,JavaScript 引擎便开始执行全局代码了。首先会执行 a=2 的赋值操作,执行该语句会将全局上下文变量环境中 a 的值设置为 2。

设置后的全局上下文的状态如下图所示:

第二步是调用 addAll 函数。

当调用该函数时,JavaScript 引擎会编译该函数,并为这个函数创立一个执行上下文,最初将该函数的执行上下文压入栈中,如下图所示:

第三步,当执行到 add 函数 调用语句时,同样会为其创立执行上下文,并将其压入调用栈,如下图所示:

当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9。如下图所示:

紧接着 addAll 执行最初一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。最终如下图所示:

至此,整个 JavaScript 流程执行完结了。

好了,当初你应该晓得了调用栈是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就可能追踪到哪个函数正在被执行以及各函数之间的调用关系。

重看 this

置信依据上文内容大家应该曾经明确什么是 JavaScript 执行上下文和调用栈了

咱们再来看 this,其实它也寄存在执行上下文中。

执行上下文包含了:变量环境、词法环境、outer、this。如下图所示:

从图中能够看出,this 是和执行上下文绑定的,也就是说 每个执行上下文中都有一个 this

执行上下文次要分为三种

  • 全局执行上下文
  • 函数执行上下文
  • eval 执行上下文

所以对应的 this 也只有这三种

  • 全局执行上下文中的 this
  • 函数中的 this
  • eval 中的 this(先不解说此状况)

全局执行上下文中的 this

在控制台中输出

console.log(this) // window

console.log(this === window) // true

咱们能够看进去:全局执行上下文中的 this 也是指向 window 对象。

函数执行上下文中的 this

执行上面代码:

function foo(){console.log(this) // window
}
foo()

能够看到输入了 window,阐明在 默认状况 下调用一个函数,其执行上下文中的 this 也是指向 window 对象的。

能够认为 JavaScript 引擎在执行 foo()时,将其转化为了:

function foo(){console.log(this) // window
}
window.foo.call(window)

显然大家发现了能够通过 call 来扭转 this 指向。

咱们来列举下设置函数执行上下文中的 this 值的办法:

1. 通过函数的 call、apply、bind 办法设置

用法如下:

let bar = {myName : "dell",}
function foo(){this.myName = "dellyoung"}
foo.call(bar)
console.log(bar) // {myName:"dellyoung"}
console.log(myName) // 报错 myName 未定义

执行下面代码,会打印出 {myName:"dellyoung"}myName未定义的报错信息,显然执行 foo() 的时候胜利的将其 this 指向指到了 bar,这时候 bar 就是 foo() 的 this

2. 通过对象调用办法设置

尝试执行上面的代码

var myObj = {
  name : "dellyoung", 
  showThis: function(){console.log(this)
  }
}
myObj.showThis()

打印出了{name: 'dellyoung', showThis: [Function: showThis] } , 显然当初这个 this 指向了调用它的 myObj。

能够失去论断:应用对象来调用其外部的一个办法,该办法的 this 是指向对象自身的。

能够认为 JavaScript 引擎在执行 myObject.showThis()时,将其转化为了:

myObj.showThis.call(myObj)

3. 依据下面两点得出小论断

  • 在全局环境中调用一个函数,函数外部的 this 指向的是全局变量 window。
  • 通过一个对象来调用其外部的一个办法,该办法的执行上下文中的 this 指向对象自身。
  • 谁调用函数,函数的 this 指向谁。对象调用函数天然不用说,全局环境调用其实也能够了解为 window 来调用,所以当然指向了 window

4. 通过构造函数中设置

咱们当初再来看一下通过 new 调用构造函数到底做了什么:

function polyNew(source, ...arg) {// 创立一个空的简略 JavaScript 对象(即{})let newObj = {};
    // 链接该对象(即设置该对象的构造函数)到另一个对象
    Object.setPrototypeOf(newObj, source.prototype); 
    // 将步骤 1 新创建的对象作为 this 的上下文;const resp = source.apply(newObj, arg);
    // 判断该函数返回值是否是对象
    if (Object.prototype.toString.call(resp) === "[object Object]") {
        // 如果该函数没有返回对象,则返回 this。return resp
    } else {
        // 如果该函数返回对象,那用返回的这个对象作为返回值。return newObj
    }
}

显然咱们看到source.apply(newObj, arg),所以构造函数其实也扭转了 this 指向,将 this 指向从原函数换到了新结构进去的函数。

解疑填坑

  • this 寄存在哪里?

    • this 寄存在每个执行上下文中
  • this 是如何呈现,又是如何隐没的?

    • this 随着执行上下文呈现,当执行上下文被回收后,也随之隐没
  • this 有什么作用?

    • 全局执行上下文中:this 指向了 window 对象,不便咱们来调用全局 window 对象。
    • 函数执行上下文中:this 指向了调用该函数的对象,缩小的参数的传递,原来如果须要在函数外部操作被调用对象,当然还须要将对象作为参数传递进去,而有了 this,就不须要了,间接拿this 就能操作被调用对象的属性。

call、apply、bind 为何物

上文用了屡次 call,想必大家曾经明确 call 做了什么了:

MDN:call() 办法应用一个指定的 this 值和独自给出的一个或多个参数来调用一个函数。

举个例子:

let bar = {myName : "dellyoung",}
function foo(){console.log(this.myName)
}
foo.call(bar) // 打印出 dellyoung

也就是说:调用 foo 函数的时候,通过应用 call(),并且传入 bar,使得 foo 函数外部的 this 指向了 bar

实现 call

咱们就依据这个论断来实现一下 call:

Function.prototype.dellCall = function (context = window,...param) {
    // 判断是函数能力调用 call 办法
    if (typeof this !== 'function') {return new TypeError("类型谬误");
    }
    // 将 this 也就是被调用的函数,通过赋值给传入的对象,来达到将被调用的函数增加到传入的对象上的目标
    context.fun = this;
    // 用传入的对象来调用须要被调用的函数,并保留返回后果
    const resp = context.fun(...param);
    // 删除传入对象上被增加的函数,避免内存透露
    Reflect.deleteProperty(context, 'fun');
    // 返回后果
    return resp;
};

其实外围很简略,咱们剖析一下:

  • 将被调用的函数作为一个属性增加到传入的对象上
  • 从而能够实现在传入的对象上,调用须要被调用的函数
  • 咱们剖析完发现外围原理还是:谁调用函数,函数的 this 指向谁

万变不离其宗:谁调用函数,函数的 this 指向谁。这句话其实能够帮忙咱们了解绝大部分 this 的问题了

实现 apply

apply 其实和 call 差不多,只不过传递参数的形式不同:

foo.call(obj,[param1,param2,...,paramN]) // 参数是数组,传入一个数组作为参数

foo.apply(obj,param1,param2,...,paramN) // 参数非数组,能够传一串参数

咱们对下面的 call 略微改一下就是 apply 了:

Function.prototype.dellApply = function (context = window, param = []) {
    // 判断是函数能力调用 call 办法
    if (typeof this !== 'function') {return new TypeError("类型谬误");
    }
    // 将被调用的函数作为一个属性增加到传入的对象上
    context.fun = this;
    // 在传入的对象上,调用须要被调用的函数
    const resp = context.fun(...param);
    // 删除传入对象上被增加的函数,避免内存透露
    Reflect.deleteProperty(context, 'fun');
    // 返回后果
    return resp;
}

实现 bind

bind 目标也一样,扭转 this,然而它并不是间接调用函数,而是返回 扭转了外部 this 值的函数,当须要的时候再调用:

咱们来实现一下:

Function.prototype.dellBind = function (context) {
    // 判断是函数能力调用 call 办法
    if (typeof this !== 'function') {return new TypeError("类型谬误")
    }
    // 用 that 变量保留被调用的函数
    const that = this;
    // 保留传入的参数
    const argArr = [...arguments];
    // 返回一个函数,这样调用这个被返回的函数,外部的 that.call()函数才会被执行
    return function F() {
        // 用 call 来实现扭转被调用函数外部 this 指向
        return that.call(context, [...argArr, ...arguments]);
    }
}

咱们来剖析一下 bind:

  • 其实 bind 和 apply 就一点区别,bind 返回一个被扭转了外部 this 指向的函数
  • 当调用返回的函数,扭转了外部 this 指向的函数能力运行,运行后返回后果
  • 实现来也很容易,bind 返回不再像 call 那样返回后果,而是返回了一个函数,调用返回的函数当然能力运行外部被扭转了 this 指向的函数

优化:严格的来说这并不是一个合格的 bind,因为还须要思考到把函数当作结构函数调用的状况,当应用 new 来把函数作为结构函数调用的时候,就不要扭转 this 指向了,间接对被调用函数 new 一下返回就行了

代码如下:

Function.prototype.dellBind = function (context) {
    // 判断是函数能力调用 call 办法
    if (typeof this !== 'function') {return new TypeError("类型谬误")
    }
    // 用 that 变量保留被调用的函数
    const that = this;
    // 保留传入的参数
    const argArr = [...arguments];
    // 返回一个函数
    return function F() {
        // 如果用的 new,即用的是构造函数
        if (this instanceof F) {return new that(...argArr, ...arguments);
        }
        // 用 call 来实现扭转被调用函数外部 this 指向
        return that.apply(context, [...argArr, ...arguments]);
    }
}

剖析一下:

  • 通过 instanceof 剖析就能够失去,this是不是由 F 通过 new 失去的,new曾经讲过啦,new运算外部会更换原型链:A.__proto__ === B.prototype
  • 所以如果 F 的原型在 this 的原型链上,那么当然是用的 new 当作结构函数调用了,咱们也应该 new 该函数返回即可

看完两件事

  • 欢送加我微信(iamyyymmm),拉你进技术群,长期交流学习
  • 关注公众号「呆鹅实验室」,和呆鹅一起学前端,进步技术认知

???? 点个赞反对我吧 ????

退出移动版