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继承自外层作用域,且无法被修改
相关推荐
舒一笑2 小时前
Mac环境安装Nginx指南实录
前端·nginx·程序员
林希_Rachel_傻希希2 小时前
别再写 c=3 了!window 对象的隐藏规则
前端·javascript
_AaronWong2 小时前
Vue页面返回滚动位置恢复:keep-alive滚动记忆
前端·vue-router
子兮曰2 小时前
深度解析Proxy与目标对象(definiteObject):原理、特性与10个实战案例
前端·javascript·node.js
克里斯蒂亚L2 小时前
禁止打开多个浏览器标签页访问相同地址的页面:Cookie + SessionStorage
前端
倔强青铜三2 小时前
苦练Python第48天:类的私有变量“防身术”,把秘密藏进类里!
人工智能·python·面试
倔强青铜三2 小时前
苦练Python第47天:一文吃透继承与多继承,MRO教你不再踩坑
人工智能·python·面试
倔强青铜三2 小时前
为什么Python程序员必须学习Pydantic?从数据验证到API开发的革命性工具
人工智能·python·面试
!win !2 小时前
不定高元素动画实现方案(上)
前端·动画