JS 中 this 的疑难杂症

前言

本篇文章主要是讲解我学习 JavaScript 中的 this 路上的疑惑部分和部分重要但不是很容易理解的知识点,所以需要带一点 this 的简单理解,主体 this 全面解析讲解可以转战 this

一、this 的本质解析

this 是函数执行时的动态上下文指针,其值完全由调用方式决定。它如同汽车的方向盘,控制权在驾驶员(调用者)手中,而非汽车(函数)本身。

javascript 复制代码
const car = {
  brand: 'Tesla',
  start() {
    console.log(`${this.brand} 启动`);
  }
};

const bike = { brand: 'Giant' };

// 不同调用方式改变this指向
car.start();       // "Tesla 启动"
car.start.call(bike); // "Giant 启动"

ps:本文不细讲bind、call和apply,可以根据链接转架

二、调用位置与调用方式

1. 位置调用

我们先看看书上这一块

以下是其给的示例:

javascript 复制代码
function baz() { 
    // 当前调用栈是:baz 
    // // 因此,当前调用位置是全局作用域 
    console.log( "baz" ); 
    bar(); // <-- bar 的调用位置 
    }
function bar() {
    // 当前调用栈是 baz -> bar 
    // 因此,当前调用位置在 baz 中 
    console.log( "bar" ); 
    foo(); // <-- foo 的调用位置 
    }
function foo() { 
    // 当前调用栈是 baz -> bar -> foo 
    // 因此,当前调用位置在 bar 中 
    console.log( "foo" ); 
}
baz(); // <-- baz 的调用位置

当时给我一顿骗啊,这里说 this 就和函数调用位置有关了,那么这里的 foo 中的 this 就该指向 bar,但是后面我又知道了 this 指向和函数调用方式有关。我们先理解完下面的方法调用再谈。

2. 方法调用

一、直接调用

先只谈一谈这个

javascript 复制代码
var a = 1;
var obj  =  {
    a: 2,
    b: function () {
        function fun() {
          return this.a
        }
       console.log(fun());
    }
} 
obj.b();//1

其实这里本身不注意也会有点问题,我一开始是在Node.js 环境中运行的。Node.js 环境:全局对象是 globalvar a = 1 不会 创建 global.a。所以会输出undefined,而不是之前我们所指的直接调用函数 this 指向全局的 1,解决方案:直接在浏览器运行即可。

好了,简单介绍完 this的直接调用就可回到我们刚刚要提出的问题:诶!根据书上的位置调用和直接调用方式,如果我们将一个函数 func 在另一个函数 testFunc 中直接调用呢?它的 this 是指向 testFunc 还是全局?不用想,文章开篇就说,完全由调用方式决定,结果就出来了。

javascript 复制代码
function testFunc(){
    let test = "test";
    func();
}

function func(){
    console.log(this.test);
}
testFunc(); //undefined

那书上为什么只写调用位置呢?其实只是为了方便理解,回到书上代码:

javascript 复制代码
function baz() { 
    // 当前调用栈是:baz 
    // // 因此,当前调用位置是全局作用域 
    console.log( "baz" ); 
    bar(); // <-- bar 的调用位置 
    }
function bar() {
    // 当前调用栈是 baz -> bar 
    // 因此,当前调用位置在 baz 中 
    console.log( "bar" ); 
    foo(); // <-- foo 的调用位置 
    }
function foo() { 
    // 当前调用栈是 baz -> bar -> foo 
    // 因此,当前调用位置在 bar 中 
    console.log( "foo" ); 
}
baz(); // <-- baz 的调用位置

按照 foo 的调用方式,这里是直接调用,所以肯定是指向全局,但是它的调用栈确实是是 baz -> bar -> foo ,而我们需要用调用栈找到调用位置,再根据绑定规则(调用方式)来确定 this 的指向,这样解释我觉得你应该能更明白书上的意思。

二、作为对象的方法调用

依旧开车jym

javascript 复制代码
var tools = ["油漆","锤子"]
const garage = {
  tools: ["扳手", "千斤顶"],
  listTools:function() {
    return this.tools.join(', ');
  }
};

console.log(garage.listTools()); //扳手, 千斤顶

这个很好理解,listTools函数作为obj的一个方法调用,这时候this指向调用它的对象,这里也就是obj。那么如果listTools函数不作为对象方法调用呢?看看下面这段代码:

javascript 复制代码
var tools = ["油漆","锤子"]
const garage = {
  tools: ["扳手", "千斤顶"],
  listTools:function () {
    return this.tools.join(',');
  }

};

var test = garage.listTools
console.log(test()); //油漆,锤子  来咯

啊?!test 函数执行结果竟然是全局变量["油漆","锤子"]。这就涉及Javascript的内存空间了,garage 对象的 listTools 属性存储的是对该匿名函数的一个引用。当赋值给 test 的时候,并没有单独开辟内存空间存储新的函数(诶,new 会啊,下面讲讲),而是类似让 test 存储一个指针,而这个指针指向 listTools 的匿名函数

最后两行代码可以这么理解:

javascript 复制代码
var test = listTools // test指向listTools的匿名函数(相当于直接让test为这个函数)
console.log(test()); // 那这一步就好理解了,执行test就是直接执行该匿名函数

那我们再想想,直接在全局执行函数是什么?没错------直接调用,自然该函数执行结果指向全局!

3. 构造函数调用

javascript 复制代码
function Engine(power) {
  this.power = power;
  this.start = function() {
    console.log(`${this.power}马力引擎启动`);
  };
}

const v6 = new Engine(300);
v6.start(); // "300马力引擎启动"

new 之后"自动挡"触发了:

  1. 创建(或者说构造了)一个全新的新对象 {}
  2. 这个新对象会被执行[[Prototype]]连接
  3. 这个新对象会被绑定到函数调用的 this
  4. 函数调用时自动返回这个新对象

new 好啊,最不容易混淆的就是 new 了。稍微提一嘴就是原型链,prototype对象的方法的this指向实例对象,因为实例对象的__proto__已经指向了原型函数的prototype。这就涉及原型链的知识了,即方法会沿着对象的原型链进行查找。这里我虽然也写了文章原型链,但不是很细腻,可以去 MDN 查查或者看看大佬文章了解了解。

new 操作具有最高优先级,覆盖其他绑定方式:

javascript 复制代码
function Car(model) {
  this.model = model;
}

const boundCar = Car.bind({ model: "Toyota" });
const tesla = new boundCar("Model S");

console.log(tesla.model); // "Model S"(new覆盖bind)

三、箭头函数的特殊规则

箭头函数如同焊接固定的方向盘,this 在定义时永久绑定

javascript 复制代码
var tools = ["油漆","锤子"];
const garage = {
  tools: ["扳手", "千斤顶"]
};
var fun = () => console.log(this.tools.join(','));
fun() //油漆,锤子
fun.call(garage) //油漆,锤子

箭头函数本身没有 this ,以其上下文的 this 作为自己的 this 值,也就是说箭头函数的 this 在词法层面就完成了绑定。apply,call方法只是传入参数,无法改变已绑定的 this 。

四、bind 的不可变性

bind 创建的函数副本也和箭头一样,如同焊死的方向盘:

javascript 复制代码
const original = function() {
  console.log(this.id);
};

const bound = original.bind({ id: 100 });

bound(); // 100
bound.call({ id: 200 }); // 仍输出100(绑定不可覆盖)
bound.apply(null); // 仍输出100

即使是 bind({ id: 100 }).bind({ id: 200 }) 也改变不了第一次的 bind ,倔的像生产队的驴。

五、小考点

箭头函数和 bind

前面说到,箭头函数"硬",bind绑定"稳",那来一场真男人之间的较量吧!

javascript 复制代码
func = () => {
  // 这里 this 指向取决于外层 this
  console.log(this)
}

func.bind(1)() // Window

bind赢了指向就是1,箭头赢了指向就是Window,显然这场较量以 bind 失败结束了,其实还是比较容易理解,根据前文描述------箭头更"快"嘛。

bind 与 new

bind 不服啊,干不过箭头函数,我还干不过 new 吗?真让你干过了我前面夸 new 不白夸了,看看以下代码:

javascript 复制代码
function func() {
  console.log(this, this.__proto__ === func.prototype)
}

boundFunc = func.bind(1)
new boundFunc() // Object true

Function.prototype.bind 方法会创建一个新的函数 boundFunc,并将 func 函数的 this 值绑定为传入的参数 1。不过,当 bind 返回的函数使用 new 关键字调用时,bind 绑定的 this 值会被忽略,new 操作符会创建一个新的对象,并且这个新对象会作为 this 传递给构造函数。

六、非严格模式的安全转换

this 也有自己的脾气啊,当没人管的时候(非严格模式),如果得出 this 指向是 undefined 或 null,那么 this 会自动指向全局对象

javascript 复制代码
function checkThis() {
  console.log(this === window);
}

checkThis.call(null);    // true(转为全局对象)
checkThis.call(undefined); // true
checkThis.bind().bind(1)(); // true
checkThis.apply(); // true
  • 浏览器环境转为 window
  • Node.js 环境转为 global
  • 严格模式下保持 null/undefined

终极决策树

遇到 this 疑惑时,按顺序检查:

  1. 函数是否被 new 调用? → 指向新实例
  2. 是否通过 call/apply/bind 调用? → 指向指定对象
  3. 是否作为对象方法调用? → 指向该对象
  4. 是否箭头函数? → 指向定义时外层 this
  5. 严格模式? → undefined : 全局对象

记住这个核心口诀:

"点前 new 绑 call apply,箭头定义定终身"

掌握这些规则,就能精准操控 JavaScript 的 this 机制,避免在代码中迷失方向。

相关推荐
Blossom.11818 分钟前
人工智能在智能供应链中的创新应用与未来趋势
前端·人工智能·深度学习·安全·机器学习
无限大632 分钟前
《计算机“十万个为什么”》之前端与后端
前端·后端·程序员
JuneXcy35 分钟前
Vue 核心技术与实战day07
前端·javascript·vue.js
shibin38 分钟前
基于axios 二次封装:构建强大的 HTTP 请求层
前端·typescript
xianshenglu39 分钟前
我的 Angular 总结:创建一个通用测试模块,简化单元测试
前端·javascript·angular.js
前端工作日常44 分钟前
资源加载错误捕获的深层解析:为什么只能用 addEventListener('error')?
javascript
粥里有勺糖44 分钟前
视野修炼-技术周刊第121期 | Rolldown-Vite
前端·javascript·github
用户05956611920944 分钟前
Java 面试资料中相关代码使用方法及组件封装方法解析
面试
帅夫帅夫1 小时前
四道有意思的考题
前端·javascript·面试
tonytony1 小时前
useRequest如何避免Race condition
前端·react.js