JavaScript 的 this 到底是个啥?从调用逻辑到手写实现,彻底搞懂绑定机制

"this 到底是谁?"------每个 JS 开发者都曾深夜发问的灵魂拷问。

在 JavaScript 中,this 是一个既强大又令人困惑的关键字。它不像 Java 或 C++ 那样在定义时就确定,而是在运行时 、根据调用方式 动态绑定。今天,我们就来揭开 this 的神秘面纱,从底层机制讲起,再手写实现 callapplybind,让你彻底掌握这个"变色龙"!

🔍 一、this 到底是谁?------决定 this 的四大法则

this 的值不是在函数定义时决定的,而是在函数被调用时决定的 。绝大多数情况下,函数的调用方式决定了 this 的指向。

✅ 法则 1:全局环境中的 this

无论是否开启严格模式,全局环境下的 this 都指向全局对象

  • 浏览器中:window
  • Node.js 中:global
js 复制代码
console.log(this === window); // true (浏览器)

✅ 法则 2:直接调用函数(独立函数调用)

这是最容易出错的地方!

js 复制代码
function foo() {
  console.log(this);
}
foo(); // 非严格模式:window;严格模式:undefined
  • 非严格模式this 指向全局对象(window
  • 严格模式thisundefined

⚠️ 注意:this 不能在执行期间被赋值,它是自动绑定的。


✅ 法则 3:对象方法调用 ------ "谁调用,this 就是谁"

js 复制代码
const obj = {
  name: 'Alice',
  greet() {
    console.log(this.name); // this 指向 obj
  }
};

obj.greet(); // 'Alice'

即使函数被赋值给变量,调用时 this 依然取决于调用者

js 复制代码
const fn = obj.greet;
fn(); // undefined (严格模式) 或 window.name (非严格)
// 因为此时是直接调用,不是 obj 调用!

✅ 法则 4:构造函数调用(new

使用 new 调用函数时,this 指向新创建的实例对象

js 复制代码
function Person(name) {
  this.name = name; // this 指向新创建的 person 实例
}
const p = new Person('Bob');

🧰 二、掌控 "this" 的三大法宝

既然this的指向如此善变,有没有办法让它听话呢?JavaScript 提供了三种方法来明确指定this的值:call、apply 和 bind。

call:逐个传递参数

call方法允许你指定函数执行时的this值,并逐个传递参数:

javascript 复制代码
function introduce(age, hobby) {
  console.log(`我是${this.name},今年${age}岁,喜欢${hobby}`);
}

const person = { name: "李四" };
introduce.call(person, 25, "打篮球"); // 我是李四,今年25岁,喜欢打篮球

apply:数组传递参数

applycall的作用相同,唯一的区别是apply接受一个数组作为参数:

javascript 复制代码
introduce.apply(person, [25, "打篮球"]); // 我是李四,今年25岁,喜欢打篮球

在 ES6 的扩展运算符出现之前,apply在处理数组参数时特别有用,比如求数组中的最大值:

javascript 复制代码
const numbers = [1, 3, 2, 5, 4];
console.log(Math.max.apply(null, numbers)); // 5

bind:永久绑定

bind方法会创建一个新函数,这个新函数的this被永久绑定到指定的值,无论之后如何调用:

javascript 复制代码
const boundIntroduce = introduce.bind(person);
boundIntroduce(25, "打篮球"); // 我是李四,今年25岁,喜欢打篮球

// 即使作为其他对象的方法调用,this依然不变
const anotherPerson = { name: "王五" };
anotherPerson.introduce = boundIntroduce;
anotherPerson.introduce(30, "游泳"); // 我是李四,今年30岁,喜欢游泳

bind还可以实现参数柯里化(Currying),即预先设置部分参数:

javascript 复制代码
const introduceWithAge = introduce.bind(person, 25);
introduceWithAge("打篮球"); // 我是李四,今年25岁,喜欢打篮球

🔧 三、底层实现:亲手打造 call、apply 和 bind

理解这些方法的底层实现,能帮助我们更深入地掌握它们的工作原理。

手写 call 方法

javascript 复制代码
Function.prototype.myCall = function(context) {
  // 检查调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("调用者必须是函数");
  }
  
  // 获取参数(第一个参数是context,剩下的是函数参数)
  const args = [...arguments].slice(1);
  let result;
  
  // 如果未提供context,默认指向全局对象
  context = context || window;
  
  // 使用Symbol避免属性名冲突
  const fnSymbol = Symbol('fn');
  context[fnSymbol] = this;
  
  // 调用函数
  result = context[fnSymbol](...args);
  
  // 清理痕迹
  delete context[fnSymbol];
  
  return result;
};

这里使用Symbol作为属性名,是为了避免覆盖context中可能已存在的fn属性,这是一种更安全的实现方式。

手写 apply 方法

javascript 复制代码
Function.prototype.myApply = function(context) {
  if (typeof this !== "function") {
    throw new TypeError("调用者必须是函数");
  }
  
  let result;
  context = context || window;
  
  const fnSymbol = Symbol('fn');
  context[fnSymbol] = this;
  
  // apply接受数组作为参数
  if (arguments[1]) {
    result = context[fnSymbol](...arguments[1]);
  } else {
    result = context[fnSymbol]();
  }
  
  delete context[fnSymbol];
  return result;
};

applycall的主要区别在于参数的处理方式,apply接收一个数组作为函数参数。

手写 bind 方法

javascript 复制代码
Function.prototype.myBind = function(context) {
  if (typeof this !== "function") {
    throw new TypeError("调用者必须是函数");
  }
  
  // 保存原函数和初始参数
  const fn = this;
  const args = [...arguments].slice(1);
  
  // 返回一个新函数
  function boundFn() {
    // 当新函数被当作构造函数调用时,this应该指向实例
    // 否则指向bind时指定的context
    return fn.apply(
      this instanceof boundFn ? this : context,
      args.concat(...arguments)
    );
  }
  
  // 维护原型链
  boundFn.prototype = Object.create(fn.prototype);
  
  return boundFn;
};

bind的实现稍微复杂一些,主要是要处理新函数被当作构造函数调用的情况。这时this应该指向新创建的实例,而不是bind时指定的context

⚡ 四、箭头函数:打破常规的 "this"

ES6 引入的箭头函数带来了一种全新的this绑定方式:它不会创建自己的this,而是继承自外层作用域的this

javascript 复制代码
const obj = {
  name: "箭头函数",
  normalFn: function() {
    console.log(this.name); // 箭头函数
    
    const arrowFn = () => {
      console.log(this.name); // 箭头函数(继承自normalFn的this)
    };
    
    arrowFn();
  }
};

obj.normalFn();

箭头函数的this一旦确定就不会改变,即使使用callapplybind也无法修改:

javascript 复制代码
const arrowFn = () => {
  console.log(this);
};

arrowFn.call({ name: "测试" }); // 仍然指向全局对象

这使得箭头函数非常适合作为回调函数,尤其是在处理异步操作时,可以避免this指向混乱的问题。

🎯 总结:掌握 "this" 的核心原则

  1. this的指向由函数被调用的方式决定,而非定义时的环境
  2. 全局环境中,this指向全局对象
  3. 函数直接调用时,this在非严格模式下指向全局对象,严格模式下为undefined
  4. 作为对象方法调用时,this指向调用该方法的对象
  5. callapply可以立即调用函数并指定this
  6. bind会创建一个新函数,永久绑定this
  7. 箭头函数的this继承自外层作用域,且无法被修改
相关推荐
●VON几秒前
React Native for OpenHarmony:解构 TouchableOpacity 的触摸反馈与事件流控制
javascript·学习·react native·react.js·性能优化·openharmony
有诺千金3 分钟前
VUE3入门很简单(5)---组件通信(自定义事件)
javascript·vue.js·ecmascript
想逃离铁厂的老铁3 分钟前
Day60 >> 94、城市间货物运输1️⃣ + 95、城市间货物运输 2️⃣ + 96、城市间货物运输 3️⃣
java·服务器·前端
努力学算法的蒟蒻5 分钟前
day74(2.2)——leetcode面试经典150
面试·职场和发展
GISer_Jing1 小时前
WebGL跨端兼容实战:移动端适配全攻略
前端·aigc·webgl
迦南giser1 小时前
前端性能——传输优化
前端
小白_ysf1 小时前
Vue 中常见的加密方法(对称、非对称、杂凑算法)
前端·vue.js·算法
2501_944448002 小时前
Flutter for OpenHarmony衣橱管家App实战:支持我们功能实现
android·javascript·flutter
人工智能训练8 小时前
【极速部署】Ubuntu24.04+CUDA13.0 玩转 VLLM 0.15.0:预编译 Wheel 包 GPU 版安装全攻略
运维·前端·人工智能·python·ai编程·cuda·vllm
会跑的葫芦怪8 小时前
若依Vue 项目多子路径配置
前端·javascript·vue.js