JS this取值深度解读

JS this取值深度解读

前言:被 this 折磨的前端日常

"为什么函数里的 this 一会儿是 window,一会儿是 undefined?" "对象方法里的 this,赋值给变量后调用怎么就指向全局了?" "箭头函数的 this 为什么跟外层函数一模一样,改都改不了?" "用 new 创建实例时,this 明明指向实例,怎么返回个对象就变了?"

this 是 JavaScript 中最容易让人困惑的概念之一 ------ 它既不是 "定义时绑定",也不是 "谁调用就指向谁" 这么简单。很多开发者靠 "经验猜 this",遇到复杂场景就陷入调试困境。本文将从 "执行上下文本质" 出发,通过 "场景复现→原理拆解→代码验证→避坑指南" 的逻辑,帮你彻底搞懂 this 的取值规则,从此不再靠 "猜" 写代码。

一、先破后立:this 的本质不是 "谁调用指向谁"

在拆解具体场景前,必须先纠正一个流传甚广的误区:this 不是 "谁调用指向谁",而是 "函数调用时,执行上下文绑定的一个变量"

1.1 this 的核心特性:执行时绑定

this 的指向在函数定义时完全不确定 ,只有在函数被调用的那一刻,才会根据 "调用方式" 绑定到具体对象,也就是无论这个函数声明在哪里、被赋值过多少次。这是理解 this 的第一个关键:

javascript 复制代码
// 函数定义时,this毫无意义
function sayHi() {
  console.log(this.name);
}

// 调用方式1:普通函数调用 → this指向window(浏览器)/global(Node)
const name = "全局";
sayHi(); // 输出"全局"

// 调用方式2:对象方法调用 → this指向对象
const obj = { name: "张三", sayHi };
obj.sayHi(); // 输出"张三"

// 调用方式3:new调用 → this指向新实例
function Person(name) {
  this.name = name;
}
const p = new Person("李四");
console.log(p.name); // 输出"李四"

同样的函数sayHi,只因调用方式不同,this 指向完全不同 ------ 这说明 "调用方式" 才是 this 绑定的核心依据。

1.2 this 的底层逻辑:执行上下文

JavaScript 执行函数时,会创建一个 "执行上下文(Execution Context)",其中包含三个核心变量:

  • this:当前函数的调用者关联对象

  • AO(Activation Object):函数的活动对象(存储局部变量、参数等)

  • 作用域链:决定变量的查找范围

this 是执行上下文的固有属性,其值由 "函数调用时的调用点(Call Site)" 决定,而非函数定义的位置。

1.3 this 绑定规则的优先级

当多个规则同时生效时,优先级决定最终 this 指向,优先级从高到低:new 绑定 > 显式绑定(bind) > 隐式绑定 > 默认绑定 验证优先级:

  • 显式绑定 > 隐式绑定:
javascript 复制代码
const obj1 = { a: 1, foo: function() { console.log(this.a); } };
const obj2 = { a: 2 };
obj1.foo.call(obj2); // 输出 2 → 显式绑定覆盖隐式
  • new 绑定 > bind 显式绑定:
javascript 复制代码
function foo(a) { this.a = a; }
const boundFoo = foo.bind({ a: 10 }); // 显式绑定到 {a:10}
const instance = new boundFoo(20);    // new 绑定
console.log(instance.a); // 输出 20 → new 覆盖 bind

附上bind的实现代码(可知:new 绑定 > bind 显式绑定):

javascript 复制代码
Function.prototype.bind = function (context) {
  var self = this;
  var args = Array.prototype.slice.call(arguments, 1);

  var fBound = function () {
    var bindArgs = Array.prototype.slice.call(arguments);
    // new创建的时候,this指向new出来的对象实例,这个实例通过new创建的,那么实例的constructor指向fBound
    return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
  }
  // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
  fBound.prototype = this.prototype;
  return fBound;
}

二、场景拆解:6 种核心调用方式的 this 绑定规则

实际开发中,this 的绑定场景可归纳为 6 类,覆盖 99% 的业务需求。每类场景都有明确的绑定规则,掌握这些规则就能精准判断 this 指向。

2.1 场景 1:普通函数调用(无任何绑定)

调用形式 :直接通过函数名()调用,不挂载在任何对象上。 绑定规则

  • 非严格模式:this 绑定到全局对象(浏览器是window,Node 是global);

  • 严格模式(use strict):this 绑定到undefined(禁止自动绑定全局对象)。

代码验证:
javascript 复制代码
// 非严格模式
function normalCall() {
  console.log("非严格模式:", this); // window(浏览器)
}
normalCall();

// 严格模式
function strictCall() {
  "use strict";
  console.log("严格模式:", this); // undefined
}
strictCall();

// 坑点:函数内嵌套函数,仍按普通调用处理
function outer() {
  console.log("outer this:", this); // window(非严格模式)
  function inner() {
    console.log("inner this:", this); // window(非严格模式)
  }
  inner(); // 普通调用,与outer的this无关
}
outer();
避坑点:
  • ❌ 不要在普通函数中依赖this获取全局对象(严格模式下会报错);

  • ✅ 如需访问全局对象,浏览器用window,Node 用globalThis(通用)。

2.2 场景 2:对象方法调用(挂载在对象上调用)

调用形式 :通过对象.函数名()调用,函数作为对象的属性存在。 绑定规则 :this 绑定到 "调用该方法的对象"(即.前面的对象)。

代码验证:
javascript 复制代码
const user = {
  name: "张三",
  sayHi() {
    console.log("this指向:", this); // 指向user对象
    console.log("用户名:", this.name); // 输出"张三"
  },
  address: {
    city: "北京",
    getCity() {
      console.log("城市:", this.city); // 指向address对象,输出"北京"
    }
  }
};

// 直接调用对象方法 → this指向对象
user.sayHi(); 
user.address.getCity(); 

// 坑点1:方法赋值给变量后,变成普通调用
const sayHi = user.sayHi;
sayHi(); // 普通调用 → this指向window,name为undefined

// 坑点2:嵌套对象中,this指向直接调用的对象(非外层)
user.address.getCity.call(user); // 强制改变this为user,输出undefined(user无city属性)
关键原理:

this 绑定的是 "直接调用者",而非 "函数定义时所在的对象"。即使函数定义在其他地方,只要通过obj.fn()调用,this 就指向obj

javascript 复制代码
// 函数定义在外部
function getUserName() {
  console.log(this.name);
}

// 挂载到不同对象调用
const obj1 = { name: "李四", getUserName };
const obj2 = { name: "王五", getUserName };

obj1.getUserName(); // 输出"李四"(this指向obj1)
obj2.getUserName(); // 输出"王五"(this指向obj2)

2.3 场景 3:构造函数调用(new 关键字)

调用形式 :通过new 函数名()创建实例。 绑定规则:this 绑定到 "新创建的实例对象"。

代码验证:
javascript 复制代码
function Person(name, age) {
  // new调用时,this指向新创建的Person实例
  this.name = name;
  this.age = age;
  console.log("构造函数this:", this); // Person { name: "...", age: ... }
}

// new调用 → this绑定到实例
const p1 = new Person("赵六", 25);
console.log(p1.name); // 输出"赵六"

// 坑点:构造函数返回对象会覆盖this
function PersonWithReturn(name) {
  this.name = name;
  // 返回对象时,new创建的实例会被这个对象替代
  return { name: "钱七" };
}
const p2 = new PersonWithReturn("孙八");
console.log(p2.name); // 输出"钱七"(this被覆盖)

// 注意:返回基本类型(如number、string)不会覆盖this
function PersonWithPrimitive(name) {
  this.name = name;
  return 123; // 基本类型,不影响this
}
const p3 = new PersonWithPrimitive("周九");
console.log(p3.name); // 输出"周九"
new 调用的底层流程:
  1. 创建一个新的空对象(const obj = {});

  2. 将新对象的__proto__指向构造函数的prototype(实现继承);

  3. 调用构造函数,将 this 绑定到新对象;

  4. 若构造函数返回对象,则返回该对象;否则返回新对象。

2.4 场景 4:apply/call/bind 绑定(强制改变 this)

调用形式 :通过fn.apply(obj)fn.call(obj)fn.bind(obj)调用。 绑定规则 :this 强制绑定到传入的objobjnull/undefined时,非严格模式绑定全局,严格模式绑定obj)。

三者区别:
方法 调用形式 是否立即执行 参数传递方式
apply fn.apply(obj, [arg1, arg2]) 数组传递参数
call fn.call(obj, arg1, arg2) 逗号分隔传递参数
bind const newFn = fn.bind(obj) 返回新函数,延迟执行
代码验证:
javascript 复制代码
function sayInfo(age, city) {
  console.log(`姓名:${this.name},年龄:${age},城市:${city}`);
}

const user = { name: "吴十" };

// apply调用 → 数组传参,立即执行
sayInfo.apply(user, [28, "上海"]); // 输出"姓名:吴十,年龄:28,城市:上海"

// call调用 → 逗号传参,立即执行
sayInfo.call(user, 28, "上海"); // 输出同上

// bind调用 → 返回新函数,延迟执行
const boundSayInfo = sayInfo.bind(user, 28);
boundSayInfo("上海"); // 输出同上

// 坑点:bind是硬绑定,后续无法被apply/call修改
const newObj = { name: "郑十一" };
boundSayInfo.call(newObj, "北京"); // 仍输出"吴十"(this无法改变)
特殊情况:obj 为 null/undefined
javascript 复制代码
// 非严格模式:obj为null/undefined时,this绑定全局
sayInfo.call(null, 28, "广州"); // 姓名:undefined(window.name为空)

// 严格模式:obj为null/undefined时,this绑定obj本身
function strictSayInfo() {
  "use strict";
  console.log(this);
}
strictSayInfo.call(null); // 输出null
strictSayInfo.call(undefined); // 输出undefined

2.5 场景 5:箭头函数(无独立 this)

调用形式const fn = () => { ... }(无function关键字)。 绑定规则:箭头函数没有独立的 this,其 this 继承自 "外层执行上下文的 this"(定义时的外层,非调用时)。

核心特性:
  1. 无法通过apply/call/bind改变 this(绑定后仍为外层 this);

  2. 不能作为构造函数(用 new 调用会报错);

  3. 没有arguments对象(需用剩余参数...args替代)。

代码验证:
javascript 复制代码
// 场景1:箭头函数作为对象方法 → this继承外层(window)
const obj = {
  name: "王十二",
  sayHi: () => {
    console.log(this.name); // undefined(this指向window)
  }
};
obj.sayHi();

// 场景2:箭头函数嵌套在对象方法中 → 继承方法的this
const obj2 = {
  name: "李十三",
  outer() {
    const inner = () => {
      console.log(this.name); // 继承outer的this,指向obj2,输出"李十三"
    };
    inner();
  }
};
obj2.outer();

// 场景3:箭头函数无法被apply/call改变this
const arrowFn = () => {
  console.log(this);
};
arrowFn.call({ name: "张十四" }); // 输出window(非严格模式),无法改变

// 场景4:箭头函数不能作为构造函数
const ArrowPerson = () => {};
new ArrowPerson(); // 报错:ArrowPerson is not a constructor
适用场景:
  • 嵌套函数中需要继承外层 this(如定时器、回调函数);

  • 避免普通函数中 this 绑定全局的问题(如 React 类组件的事件回调)。

2.6 场景 6:DOM 事件回调与 class 中的 this

(1)DOM 事件回调

调用形式dom.addEventListener('click', fn)绑定规则:this 绑定到 "触发事件的 DOM 元素"(即事件源)。

javascript 复制代码
const btn = document.createElement("button");
btn.textContent = "点击我";
document.body.appendChild(btn);

// 普通函数 → this指向btn
btn.addEventListener("click", function() {
  console.log(this); // <button>点击我</button>
});

// 坑点:箭头函数 → this继承外层(window)
btn.addEventListener("click", () => {
  console.log(this); // window(无法获取btn)
});
(2)class 中的 this

绑定规则 :class 内部默认启用严格模式,方法中的 this 默认绑定到实例,但脱离实例调用时为undefined

javascript 复制代码
class User {
  constructor(name) {
    this.name = name;
  }

  sayHi() {
    console.log(this.name);
  }
}

const user = new User("刘十五");
user.sayHi(); // 输出"刘十五"(this指向实例)

// 坑点:方法赋值后调用 → 严格模式下this为undefined
const sayHi = user.sayHi;
sayHi(); // 报错:Cannot read properties of undefined (reading 'name')

// 解决方案:在constructor中绑定this
class UserWithBind {
  constructor(name) {
    this.name = name;
    // 绑定this到实例
    this.sayHi = this.sayHi.bind(this);
  }

  sayHi() {
    console.log(this.name);
  }
}
const user2 = new UserWithBind("陈十六");
const sayHi2 = user2.sayHi;
sayHi2(); // 输出"陈十六"(this已绑定)

三、避坑指南:8 个高频 this 指向错误及解决方案

掌握规则后,还要能识别实际开发中的 "隐形陷阱",以下是 8 个最容易踩的坑及解决方法。

3.1 坑 1:对象方法赋值给变量后调用,this 指向错误

错误代码

javascript 复制代码
const obj = {
  name: "赵十七",
  sayHi() {
    console.log(this.name);
  }
};
const hi = obj.sayHi;
hi(); // undefined(this指向window)

解决方案

  1. 直接通过对象调用:obj.sayHi()

  2. 用 bind 绑定 this:const hi = obj.sayHi.bind(obj); hi()

  3. 用箭头函数包裹:const hi = () => obj.sayHi(); hi()

3.2 坑 2:定时器回调中 this 指向全局

错误代码

javascript 复制代码
const obj = {
  name: "孙十八",
  delaySayHi() {
    setTimeout(function() {
      console.log(this.name); // undefined(this指向window)
    }, 1000);
  }
};
obj.delaySayHi();

解决方案

  1. 用箭头函数(继承外层 this):

    javascript 复制代码
    setTimeout(() => {
      console.log(this.name); // 输出"孙十八"
    }, 1000);
  2. 保存 this 到变量:

    javascript 复制代码
    const self = this;
    setTimeout(function() {
      console.log(self.name); // 输出"孙十八"
    }, 1000);

3.3 坑 3:数组 forEach/map 中的 this 指向错误

错误代码

javascript 复制代码
const obj = {
  prefix: "编号:",
  processArr(arr) {
    return arr.map(function(item) {
      return this.prefix + item; // undefined(this指向window)
    });
  }
};
obj.processArr([1, 2, 3]); // ["undefined1", "undefined2", "undefined3"]

解决方案

  1. 用箭头函数:

    javascript 复制代码
    arr.map(item => this.prefix + item);
  2. 传 this 作为 forEach/map 的第二个参数:

    javascript 复制代码
    arr.map(function(item) {
      return this.prefix + item;
    }, this); // 第二个参数绑定this

3.4 坑 4:class 方法作为事件回调,this 为 undefined

错误代码

javascript 复制代码
class Button {
  constructor() {
    this.text = "点击";
    this.btn = document.createElement("button");
    this.btn.textContent = this.text;
    this.btn.addEventListener("click", this.handleClick);
  }

  handleClick() {
    console.log(this.text); // undefined(this为undefined,严格模式)
  }
}
new Button();

解决方案

  1. constructor 中 bind 绑定:

    javascript 复制代码
    constructor() {
      // ...
      this.handleClick = this.handleClick.bind(this);
    }
  2. 用箭头函数作为回调:

    javascript 复制代码
    this.btn.addEventListener("click", (e) => this.handleClick(e));

3.5 坑 5:箭头函数作为对象方法,this 指向错误

错误代码

javascript 复制代码
const obj = {
  name: "周十九",
  sayHi: () => {
    console.log(this.name); // undefined(this指向window)
  }
};
obj.sayHi();

解决方案

  • 放弃箭头函数,用普通函数作为对象方法:

    javascript 复制代码
    sayHi() {
      console.log(this.name); // 输出"周十九"
    }

3.6 坑 6:构造函数返回对象,this 被覆盖

错误代码

javascript 复制代码
function Product(name) {
  this.name = name;
  // 错误:返回对象覆盖this
  return { name: "默认商品" };
}
const phone = new Product("手机");
console.log(phone.name); // 输出"默认商品"

解决方案

  1. 不返回对象,或返回 this:

    javascript 复制代码
    function Product(name) {
      this.name = name;
      return this; // 或不写return
    }
  2. 若需返回额外数据,挂载到 this 上:

    javascript 复制代码
    function Product(name) {
      this.name = name;
      this.extra = { price: 999 }; // 额外数据挂载到this
    }

3.7 坑 7:bind 多次绑定,只有第一次生效

错误代码

javascript 复制代码
function fn() {
  console.log(this.name);
}
const obj1 = { name: "吴二十" };
const obj2 = { name: "郑二十一" };

// 多次bind,只有第一次生效
const bound1 = fn.bind(obj1);
const bound2 = bound1.bind(obj2);
bound2(); // 输出"吴二十"(obj2绑定无效)

解决方案

  • 避免多次 bind,如需动态改变 this,用 apply/call(而非 bind)。

3.8 坑 8:严格模式与非严格模式混用,this 指向混乱

错误代码

javascript 复制代码
// 外层非严格模式
function outer() {
  "use strict"; // 内层严格模式
  function inner() {
    console.log(this); // undefined(严格模式)
  }
  inner();
}
outer();

解决方案

  • 项目中统一严格模式(推荐),在入口文件或模块顶部添加"use strict"

  • 避免函数内部局部启用严格模式,导致 this 行为不一致。

四、总结:this 指向的判断流程(万能公式)

遇到任何 this 指向问题,都可以按以下步骤判断,准确率 100%:

  1. 函数是否为箭头函数? → 是:this 继承外层执行上下文的 this(直接找外层 this); → 否:进入下一步。

  2. 函数是否用 new 调用? → 是:this 指向新创建的实例; → 否:进入下一步。

  3. 函数是否用 apply/call/bind 绑定? → 是:this 指向绑定的对象(obj 为 null/undefined 时,非严格模式绑全局,严格模式绑 obj); → 否:进入下一步。

  4. 函数是否作为对象方法调用(obj.fn ())? → 是:this 指向调用方法的对象(obj); → 否:进入下一步。

  5. 是否为严格模式(普通调用)? → 是:this 绑定到 undefined; → 否:this 绑定到全局对象(window/global)。

五、最后:this 的设计意义

为什么 JavaScript 要设计 this?本质是为了 "代码复用"------ 让函数可以在不同对象上调用,无需为每个对象重复定义相同逻辑。

比如一个sayHi函数,通过 this 可以在不同用户对象上复用,输出不同用户名:

javascript 复制代码
function sayHi() {
  console.log(`Hi, ${this.name}`);
}

const userA = { name: "用户A", sayHi };
const userB = { name: "用户B", sayHi };

userA.sayHi(); // Hi, 用户A
userB.sayHi(); // Hi, 用户B

理解 this 的设计初衷,才能更好地运用它。希望本文能帮你告别 "this 困惑",写出逻辑清晰、无隐藏 bug 的 JavaScript 代码。总而言之,一键点赞、评论、喜欢收藏吧!这对我很重要!

相关推荐
名字越长技术越强2 小时前
CSS之选择器|弹性盒子模型
前端·css
用户93816912553602 小时前
VUE3项目--路由切换时展示进度条
前端
小王码农记2 小时前
vue2中table插槽新语法 v-slot
前端·vue.js
前端婴幼儿2 小时前
前端直接下载到本地(实时显示下载进度)
前端
三小河2 小时前
前端 Class 语法从 0 开始学起
前端
hjt_未来可期2 小时前
js实现复制、粘贴文字
前端·javascript·html
米诺zuo2 小时前
Next.js 路由与中间件
前端
小明记账簿_微信小程序2 小时前
webpack实用配置dev--react(一)
前端
linhuai2 小时前
Flutter如何实现 登录页 和 注册页 不显示底部菜单,其他页面显示底部菜单
前端