关于this,我提出了下面几个问题:
要完全明白这些,咱们要先理解JavaScript执行上下文和调用栈是什么
为清晰讲述
this
结合了<a href="https://time.geekbang.org/column/article/120257" type="_blank">《浏览器工作原理与实践》</a>部分内容
咱们先看下这段代码的函数调用过程:
var a = 2
function add(){
var b = 10
return a+b
}
add()
这段代码很简单,先是创建了一个 add 函数,接着在代码的最下面又调用了该函数。
在执行到函数 add() 之前,也就是第6行之前,JavaScript 引擎会为上面这段代码创建全局执行上下文,包含了声明的函数和变量,你可以参考下图:
从图中可以看出,代码中全局变量和函数都保存在全局上下文的变量环境中。
执行上下文准备好之后,便开始执行全局代码,当执行到 add 这儿时,JavaScript 判断这是一个函数调用,那么将执行以下操作:
就这样,当执行到 add 函数的时候,我们就有了两个执行上下文了——全局执行上下文和 add 函数的执行上下文。
也就是说在执行 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 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。
相信根据上文内容大家应该已经明白什么是JavaScript执行上下文和调用栈了
我们再来看this,其实它也存放在执行上下文中。
执行上下文包括了:变量环境、词法环境、outer、this。如下图所示:
从图中可以看出,this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this
执行上下文主要分为三种
所以对应的 this 也只有这三种
在控制台中输入
console.log(this) // window
console.log(this === window) // true
我们可以看出来:全局执行上下文中的 this 也是指向 window 对象。
执行下面代码:
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.根据上面两点得出小结论
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指向从原函数换到了新构造出来的函数。
window
对象,方便我们来调用全局window
对象。this
,就不需要了,直接拿this
就能操作被调用对象的属性。上文用了多次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:
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的问题了
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目的也一样,改变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,因为还需要考虑到把函数当作构造函数调用的情况,当使用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
new
当作构造函数调用了,咱们也应该new
该函数返回即可🌈 点个赞支持我吧 🌈
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。