0基础进大厂,第11天:this到底指向谁?一文带你从此拿捏this的弯弯绕绕,“脚踢”面试官

95 阅读7分钟

引言

this提供了一种更优雅的方式来隐式地传递一个对象引用,这样可以让代码更加简洁,易于复用。可以说,this的存在大大提高了JS这门语言的灵活性
在前面的很多处代码中,我们都用到了this,但是并没有细聊this是如何工作的。
总有人说this的指向很难理解
我只想说,如果你认真看完这篇文章,请别再说不懂this的指向

对比:使用this VS 不使用this

不使用this
可以看到,我们需要手动地给函数speak传入对象

function speak(p) {
    var greeting = "Hello, I am " + " " + identify(p) + ".";
    console.log(greeting);
}
var Person = { name: "John" };

function identify(p) {
    return p.name.toUpperCase();
}

speak(Person);

输出结果:

image.png
使用this
可以看到,speak(Person)变成了speak.call(Person),函数speak参数列表里不再需要传入对象,而是直接使用this

function speak() {
    var greeting = "Hello, I am " + " " + identify(this) + ".";
    console.log(greeting);
}
var Person = { name: "John" };

function identify(p) {
    return p.name.toUpperCase();
}
speak.call(Person)

输出结果,跟上面一样。
我知道你有很多疑问。比如:.call()方法做了什么?
来来来,因为讲解代码要先用到,我先简单告诉你,speak.call(Person)就是将函数speak的this绑定到对象Person上,让函数speak的this指向对象Person

this的隐式绑定

深刻理解“this的指向由执行环境决定”

全局作用域下,this指向不同

在node.js中

先别管直接打印this合不合理,我们看看在node.js环境下的输出结果如何。

function fn() {
    console.log(this);
}
fn();

可以看到,this指向的是global

image.png

在浏览器(V8)中

指向的是window image.png 为什么在node.js和在浏览器(V8)跑同一份代码,输出的结果不同?
这是因为在node.js中,全局是global;而在浏览器中,全局是window对象

函数的调用方式决定this的指向

JavaScript的设计哲学

JavaScript在设计时有一个核心原则:每个函数调用都必须有一个this值。当没有明确指定this时,JavaScript需要提供一个"默认值"。

function test() {
    console.log(this); // 必须有一个this,不能是undefined(非严格模式)
}
test(); // JavaScript问:this应该是什么?答:全局对象

this绑定的内部机制

函数调用的内部过程

function myFunction() {
    console.log(this);
}

// 当你写 myFunction() 时,JavaScript内部实际做的是:
// 1. 确定函数对象:myFunction
// 2. 确定this值:没有明确指定,使用默认规则
// 3. 默认规则:非严格模式指向全局对象,严格模式为undefined
// 4. 调用函数:myFunction.call(全局对象)

myFunction();
// 等价于:
myFunction.call(global); // Node.js
myFunction.call(window); // 浏览器

所以,这就是清楚说明了,函数里的this出厂默认绑定全局对象,只有被其他对象调用时,才会改变this指向。也就是说,如果一直被调用,this指向就一直改变,直到指向最后的调用对象

总结一下:重要的this绑定规则

  • 默认绑定:当函数被独立调用时,函数里的this指向全局
  • 隐式绑定:当函数引用有上下文对象 且 被该对象调用 时,函数里的this指向这个上下文对象
  • 隐式丢失:当一个函数被多层对象调用的时,函数的 this 指向最近的那一层对象

三二一,上案例

案例一:

function fn() {
    console.log(this);//global
}
fn();

案例二:

function fn() {
    console.log(this);//this指向global
}
var obj = {
    a: 2,
    fn: fn(),
}

案例三:

function fn() {
    console.log(this);//this指向obj
}
var obj = {
    a: 2,
    fn: fn,
}
obj.fn();

案例二、三如何理解?

针对案例二:

我问你,一个函数在声明的时候没有return值,是不是就是相当于return undefined
我再问你,obj调用函数fn了吗?fn()的执行环境是什么?是obj吗?我先来告诉你,不是!
是哪?答:全局对象

针对案例三:

看到这行代码:obj.fn();
我再问你,obj调用函数fn了吗?
答:调用了。

核心解释:

上面两个解释太抽象了,我觉得是“废话”。
我就问你,在案例二:对象obj获取到函数fn的地址了吗?答:没有,只获取到undefined;
在案例三获取到了吗?答:获取到了,fn的值就是函数fn的地址,所以obj通过这个地址访问到函数fn的内部,这个过程就是调用
我知道,我知道,你看到案例一的函数fn调用长这个样子。

function fn() {
    console.log(this);//global
}
fn();

你肯定想问:案例一打印的是global,它就获取到函数fn的地址了吗?答:出厂自带

接下来引入新的概念——模块包裹机制

在node.js环境的非严格模式下:

拥有模块包裹机制
当代码在Node.js模块文件(如 test.js)中执行时:

// test.js
let a = this;
console.log(a); // 输出 {}

Node.js会将模块代码包裹在一个函数中执行

(function (exports, require, module, __filename, __dirname) {
  let a = this; // 此处的 `this` 指向 `module.exports`
  console.log(a); // 输出 {}
}).call(module.exports, ...);

关键点:

  • 包裹函数的 this 被显式绑定为 module.exports(初始值为空对象 {}
  • 模块作用域中,顶层 this ​不等于全局对象 global,而是指向当前模块的导出对象

同样是这份代码,我们将它放在浏览器上运行

let a = this;
console.log(a); 

输出的是window image.png 关键点: 浏览器的V8引擎没有模块包裹机制,顶层作用域直接绑定到全局对象 window

进阶案例

案例一(重点):

先来看份代码,执行环境为node.js的非严格模式node.js 问:为什么输出的是undefined

var a = 1
function fn() {
    console.log(this.a);//undefined
}
function fn1() {
    var a = 2;
    fn();
}
fn1();

答:在node.js的非严格模式下,node.js采用模块包裹机制,var声明的变量会放在模块函数里,但由于函数fn是独立调用,所以this指向global,在global里没有属性a
所以,我问你,访问对象里没有的属性,返回的是什么?答:undefined
如果在浏览器中执行这份代码呢?

image.png 答:在浏览器的执行机制眼里,用var声明对象相当于在window对象里添加属性,函数的this默认指向window,所以this.a === window.a

image.png

案例二:

如果你已经理解了案例一,那么案例二简直ez

image.png

var a = 3
function fn() {
    var a = 2
    function fn1() {
        var a = 1;
        console.log(this.a);//undefined
    }
    fn1();
}
fn();

案例三(重点):

请问输出的是什么?

let b = 2;
function fn() {
    console.log(this.b);
}
fn();

答:undefined
为什么?我问你:let、const和var的区别在哪里?影响到this的指向了吗?回答完毕!

判断是否被对象调用

案例四:

var a = 1
function fn() {
    console.log(this.a);//2
}
var obj = {
    a: 2,
    fn: fn,
}
obj.fn();

案例五:

var a = 1
function fn() {
    console.log(this.a);//undefined
}
var obj = {
    a: 2,
    fn: fn(),
}

this的显式绑定

这些是官方写好的方法,简单好理解,就先直接介绍概念再讲讲案例
显示绑定:

  • fn.call(obj,x,y,z...):
    • 显示地将fn里的this绑定到obj上,call负责帮fn接收参数
  • fn.apply(obj,[x,y,z...])
    • 显示地将fn里的this绑定到obj上,apply负责帮fn接收参数,参数是数组
  • fn.bind(obj,x,y,z...)
    • 显示地将fn里的this绑定到obj上,bind会返回一个新的函数,新函数和bind都可以帮fn接收参数,参数零散的传入
    • 返回一个新的函数,这个函数的this绑定到obj上,并且会帮fn接收参数
function fn(x, y) {
    console.log(this.a, x + y);
}
var obj = {
    a: 2,
}
fn.call(1, 2, 3);//undefined 5
fn.call(obj, 2, 3);//2 5
fn.apply(obj, [2, 3]);//2 5
const bar = fn.bind(obj)//优先使用bind里的参数
bar(2, 3);//2 5s

image.png

解释:
  • fn.call(1, 2, 3) //undefined 5
    意思就是将函数fn绑定到1上,但是1是原始类型,无法绑定属性,所以返回undefined
    参数 2 3分别对应x y
  • fn.apply(obj, [2, 3])与上面的唯一的区别就是接收的参数是数组的形式。
  • const bar = fn.bind(obj)更为灵活一些,以下的输出结果都是一样的
const bar = fn.bind(obj,3)//优先使用bind里的参数
bar(2);//2 5s
const bar = fn.bind(obj,23)

new的绑定

猜到能猜得到,new的绑定指向实例对象,不然实例对象怎么获取到传入的参数

箭头函数

  • 箭头函数没有自己的 this,它的 this 是继承自外层作用域的 this
  • 先看this函数是谁的,再看this函数是怎么被调用的
function a() {
    let b = function () {
        let c = () => {
            let d = () => {
                console.log(this)//global
            }
            d()
        }
        c()
    }
    b()
}
a()

总结

  • 函数出厂自带this指向全局对象
  • node.js的全局对象是global
  • 浏览器的全局执行对象是window
  • 隐式绑定核心的一句话:谁调用函数,this就指向谁
  • 显式绑定熟悉方法使用
  • new的绑定指向实例对象
  • 箭头函数没有this
    文章写作实属不易,不要吝啬点赞🌹