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继承自外层作用域,且无法被修改
相关推荐
于慨19 小时前
Lambda 表达式、方法引用(Method Reference)语法
java·前端·servlet
石小石Orz19 小时前
油猴脚本实现生产环境加载本地qiankun子应用
前端·架构
从前慢丶19 小时前
前端交互规范(Web 端)
前端
@yanyu66619 小时前
07-引入element布局及spring boot完善后端
javascript·vue.js·spring boot
CHU72903519 小时前
便捷约玩,沉浸推理:线上剧本杀APP功能版块设计详解
前端·小程序
GISer_Jing19 小时前
Page-agent MCP结构
前端·人工智能
王霸天19 小时前
💥别再抄网上的Scale缩放代码了!50行源码教你写一个永不翻车的大屏适配
前端·vue.js·数据可视化
小领航20 小时前
用 Three.js + Vue 3 打造炫酷的 3D 行政地图可视化组件
前端·github
@大迁世界20 小时前
2026年React大洗牌:React Hooks 将迎来重大升级
前端·javascript·react.js·前端框架·ecmascript
PieroPc20 小时前
一个功能强大的 Web 端标签设计和打印工具,支持服务器端直接打印到局域网打印机。Fastapi + html
前端·html·fastapi