前言
关于this的指向问题在我们的无论项目中还是面试中都会或多或少出现过坑的地方,其实万事万物回归本质都是大道至简,this也一样,迷惑的原因是没有掌握其最本质的原理,本篇文章带你深入最本质的地方,大白话人人都能懂的道理
this指向结论
先说结论,this指向如以下这张表所示:
| 调用方式 | 示例 | 函数中的this指向 |
|---|---|---|
| 通过new调用 | new Method() | 新对象 |
| 直接调用 | method() | 全局对象 |
| 通过对象调用 | obj.method() | 前面的对象 |
call、apply、bind | method.call(ctx) | 第一个参数 |
在MDN文档中这样介绍this的:是当前执行上下文中的一个属性,在非严格模式下是一个对象;严格模式下可以是任意值
那么这里涉及到的第一个知识点就是执行上下文
执行上下文是什么,有什么用
执行上下文是JavaScript中用来管理代码的执行环境,包括了当前执行的代码所需的所有信息和状态,如变量对象、作用域链、this指向等,是实现JavaScript作用域和作用域链的重要机制。
其主要有以下几种:
- 全局执行上下文,负责初始化全局变量和函数还有this,只有一个
- 函数执行上下文,是在函数调用时创建的,负责执行函数体内的代码,包括了函数的变量、作用域、this等,会有多个,相互独立
- Eval执行上下文,通过eval函数创建的,动态的改变当前作用域链,了解一下即可
- 块级执行上下文,是ES6新增的,主要是由let、const等声明的变量所在的代码块产生的执行上下文
这里主要介绍全局执行上下文和函数执行上下文中this指向
在全局执行上下文中,this指向无论是否是严格环境都指向全局对象,全局对象可以用globalThis来指代,如浏览器环境中指的是window,则在Node.js环境中指的是global
而且函数执行上下文时,this的值则取决于函数被调用的方式。当然在严格模式下,会是undefined
函数中的this指向
在函数中的this指向往往是最迷惑人的,那么记住这么一句话:函数中的this指向谁,完全取决于如何去调用这个函数的
要完全理解这句话,为什么this会是动态的,会在函数调用的时候确定的呢?
那是因为this是在函数执行上下文中确定的,而函数执行上下文是在函数调用的时候创建的,所以函数中的this是动态的
函数调用的四种方式正如一开始的那张表格所示
new调用- 直接调用
- 通过对象调用
call、apply、bind调用
new调用
根据MDN文档介绍,new调用会返回一个新的对象,而this会指向这个新的对象,具体规则如下:
- 创建一个空的简单JavaScript对象(即
{}) - 为步骤1新创建的对象添加属性
__proto__,将该属性链接至构造函数的原型对象 - 将步骤1新创建的对象作为
this的上下文 - 如果该函数没有返回对象,则返回
this
直接调用
其中this指向全局对象
通过对象调用
this指向这个对象
call、apply、bind调用
this指向第一个参数,这里介绍下call和bind的方式
call方法接收第一个参数作为this的上下文,如果this值是null或undefined时会自动替换成全局对象,如果是原始值则会被转成对应的包装对象
Function.prototype.myCall = function (ctx, ...args) {
// 归一化,确保ctx一定是对象
ctx = ctx === undefined || ctx === null ? globalThis : Object(ctx);
// 待执行的函数
const fn = this;
// 用Symbol作为属性不重复,阻止fn被外部覆盖
const key = Symbol();
Object.defineProperty(ctx, key, {
enumerable: false, // 不可枚举
value: fn
});
// 执行函数
const result = ctx[key](...args);
delete ctx[key];
return result;
};
注意点:
- call方法是函数的原型对象里的属性,所以要写在函数的原型对象上
- 要对ctx做归一化处理,原始类型要用包装对象Object,null和undefined要改为全局对象用globalThis(浏览器中指的是window,Node环境中指的是global)不区分环境
- call方法中的this怎么获取,这里就用到了上述规则中的第三条了,通过对象调用,则this指向这个对象,由于fn.call()通常这么用的,所以this会指代这个fn函数
- 把call中的this作为ctx的一个属性加上去,属性用Symbol不重复且不可枚举,最后执行完删除属性
bind方法则接收一个第一个参数为this上下文,然后返回一个新的函数,这个新的函数如果用new操作符调用则this指向这个new创建的新的对象
看如下这个例子:
其中newFn是Fn.bind后创建的新函数,按照文章开头所示图中的规则来看,貌似是第二条规则-直接调用函数
没错,你是对的😊!
newFn中的this指向的确实是全局对象,但是由于其是bind生成的,在其函数内部又调用了Fn.call(1),于是又把this指向了1包装的Number对象,所以上述的规则并没有错误,只是咱们的知识理解的不到位罢了
bind的伪代码可以写成如下所示:
Function.prototype.myBind = function (ctx) {
return function () {
if (当前函数是new调用的) {
new 原始函数();
} else {
原始函数.call(ctx);
}
};
};
bind方法的具体代码如下:
Function.prototype.myBind = function (ctx) {
// 取出ctx后面的参数
const args = Array.prototype.slice.call(arguments, 1);
// 拿到this
const fn = this;
return function A() {
// 新函数里有参数
const restArgs = Array.prototype.slice.call(arguments);
const allArgs = args.concat(restArgs);
// 判断新函数调用方式
if (Object.getPrototypeOf(this) === A.prototype) {
return new fn(...allArgs); // this不改变,指向fn创建的新对象
}
return fn.apply(ctx, allArgs); // this指向ctx
};
};
这里注意的点是:bind返回新的函数是怎么调用的,一种是直接调用,使用规则第二条,只不过函数内部通过apply改变了this的指向为ctx;另外一种是new调用,使用规则第一条,this指向就是内部fn创建的新对象了
箭头函数中的this
有人说箭头函数中的this该怎么指向,一句话:箭头函数没有自己的this
箭头函数中this其实当做一个变量来理解,而这个变量是外部作用域的,这就涉及到了闭包和词法作用域的知识了
闭包
闭包是指在一个函数内部定义的函数可以访问到该函数的变量,即使该函数已经执行完毕并且返回了。简单来说,闭包就是一个函数和它所在的词法环境的组合
当一个函数内部定义了另一个函数,并将它作为返回值返回时,如果该内部函数引用了外部函数的变量,就会形成一个闭包。因为内部函数要访问外部函数的变量,需要将外部函数的词法环境保存下来,这样就可以在内部函数执行时继续访问这些变量
词法作用域
词法作用域的工作就是JavaScript预编译阶段,确定所有变量和函数的作用域以及其对应的标识符,形成词法环境
词法环境是JavaScript引擎内部用于管理变量和函数作用域的数据结构
闭包也是这里形成的,而箭头函数的this当作变量的话,其实就在函数定义的时候就确定了
最后总结,this的指向问题就正如文章开头的那张图所示,四种调用方法,其他的都可以根据其推导出来。