JS this取值深度解讀
前言:被 this 折磨的前端日常
“為什麼函數裏的 this 一會兒是 window,一會兒是 undefined?”
“對象方法裏的 this,賦值給變量後調用怎麼就指向全局了?”
“箭頭函數的 this 為什麼跟外層函數一模一樣,改都改不了?”
“用 new 創建實例時,this 明明指向實例,怎麼返回個對象就變了?”
this 是 JavaScript 中最容易讓人困惑的概念之一 —— 它既不是 “定義時綁定”,也不是 “誰調用就指向誰” 這麼簡單。很多開發者靠 “經驗猜 this”,遇到複雜場景就陷入調試困境。本文將從 “執行上下文本質” 出發,通過 “場景復現→原理拆解→代碼驗證→避坑指南” 的邏輯,幫你徹底搞懂 this 的取值規則,從此不再靠 “猜” 寫代碼。
一、先破後立:this 的本質不是 “誰調用指向誰”
在拆解具體場景前,必須先糾正一個流傳甚廣的誤區:this 不是 “誰調用指向誰”,而是 “函數調用時,執行上下文綁定的一個變量”。
1.1 this 的核心特性:執行時綁定
this 的指向在函數定義時完全不確定,只有在函數被調用的那一刻,才會根據 “調用方式” 綁定到具體對象,也就是無論這個函數聲明在哪裏、被賦值過多少次。這是理解 this 的第一個關鍵:
// 函數定義時,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) > 隱式綁定 > 默認綁定
驗證優先級:
- 顯式綁定 > 隱式綁定:
const obj1 = { a: 1, foo: function() { console.log(this.a); } };
const obj2 = { a: 2 };
obj1.foo.call(obj2); // 輸出 2 → 顯式綁定覆蓋隱式
- new 綁定 > bind 顯式綁定:
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 顯式綁定):
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(禁止自動綁定全局對象)。
代碼驗證:
// 非嚴格模式
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 綁定到 “調用該方法的對象”(即.前面的對象)。
代碼驗證:
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:
// 函數定義在外部
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 綁定到 “新創建的實例對象”。
代碼驗證:
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 調用的底層流程:
- 創建一個新的空對象(
const obj = {}); - 將新對象的
__proto__指向構造函數的prototype(實現繼承); - 調用構造函數,將 this 綁定到新對象;
- 若構造函數返回對象,則返回該對象;否則返回新對象。
2.4 場景 4:apply/call/bind 綁定(強制改變 this)
調用形式:通過fn.apply(obj)、fn.call(obj)、fn.bind(obj)調用。
綁定規則:this 強制綁定到傳入的obj(obj為null/undefined時,非嚴格模式綁定全局,嚴格模式綁定obj)。
三者區別:
| 方法 | 調用形式 | 是否立即執行 | 參數傳遞方式 |
|---|---|---|---|
| apply | fn.apply(obj, [arg1, arg2]) |
是 | 數組傳遞參數 |
| call | fn.call(obj, arg1, arg2) |
是 | 逗號分隔傳遞參數 |
| bind | const newFn = fn.bind(obj) |
否 | 返回新函數,延遲執行 |
代碼驗證:
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
// 非嚴格模式: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”(定義時的外層,非調用時)。
核心特性:
- 無法通過
apply/call/bind改變 this(綁定後仍為外層 this); - 不能作為構造函數(用 new 調用會報錯);
- 沒有
arguments對象(需用剩餘參數...args替代)。
代碼驗證:
// 場景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 元素”(即事件源)。
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。
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 指向錯誤
錯誤代碼:
const obj = {
name: "趙十七",
sayHi() {
console.log(this.name);
}
};
const hi = obj.sayHi;
hi(); // undefined(this指向window)
解決方案:
- 直接通過對象調用:
obj.sayHi(); - 用 bind 綁定 this:
const hi = obj.sayHi.bind(obj); hi(); - 用箭頭函數包裹:
const hi = () => obj.sayHi(); hi()。
3.2 坑 2:定時器回調中 this 指向全局
錯誤代碼:
const obj = {
name: "孫十八",
delaySayHi() {
setTimeout(function() {
console.log(this.name); // undefined(this指向window)
}, 1000);
}
};
obj.delaySayHi();
解決方案:
-
用箭頭函數(繼承外層 this):
setTimeout(() => { console.log(this.name); // 輸出“孫十八” }, 1000); -
保存 this 到變量:
const self = this; setTimeout(function() { console.log(self.name); // 輸出“孫十八” }, 1000);
3.3 坑 3:數組 forEach/map 中的 this 指向錯誤
錯誤代碼:
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"]
解決方案:
-
用箭頭函數:
arr.map(item => this.prefix + item); -
傳 this 作為 forEach/map 的第二個參數:
arr.map(function(item) { return this.prefix + item; }, this); // 第二個參數綁定this
3.4 坑 4:class 方法作為事件回調,this 為 undefined
錯誤代碼:
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();
解決方案:
-
constructor 中 bind 綁定:
constructor() { // ... this.handleClick = this.handleClick.bind(this); } -
用箭頭函數作為回調:
this.btn.addEventListener("click", (e) => this.handleClick(e));
3.5 坑 5:箭頭函數作為對象方法,this 指向錯誤
錯誤代碼:
const obj = {
name: "周十九",
sayHi: () => {
console.log(this.name); // undefined(this指向window)
}
};
obj.sayHi();
解決方案:
-
放棄箭頭函數,用普通函數作為對象方法:
sayHi() { console.log(this.name); // 輸出“周十九” }
3.6 坑 6:構造函數返回對象,this 被覆蓋
錯誤代碼:
function Product(name) {
this.name = name;
// 錯誤:返回對象覆蓋this
return { name: "默認商品" };
}
const phone = new Product("手機");
console.log(phone.name); // 輸出“默認商品”
解決方案:
-
不返回對象,或返回 this:
function Product(name) { this.name = name; return this; // 或不寫return } -
若需返回額外數據,掛載到 this 上:
function Product(name) { this.name = name; this.extra = { price: 999 }; // 額外數據掛載到this }
3.7 坑 7:bind 多次綁定,只有第一次生效
錯誤代碼:
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 指向混亂
錯誤代碼:
// 外層非嚴格模式
function outer() {
"use strict"; // 內層嚴格模式
function inner() {
console.log(this); // undefined(嚴格模式)
}
inner();
}
outer();
解決方案:
- 項目中統一嚴格模式(推薦),在入口文件或模塊頂部添加
"use strict"; - 避免函數內部局部啓用嚴格模式,導致 this 行為不一致。
四、總結:this 指向的判斷流程(萬能公式)
遇到任何 this 指向問題,都可以按以下步驟判斷,準確率 100%:
- 函數是否為箭頭函數?
→ 是:this 繼承外層執行上下文的 this(直接找外層 this);
→ 否:進入下一步。 - 函數是否用 new 調用?
→ 是:this 指向新創建的實例;
→ 否:進入下一步。 - 函數是否用 apply/call/bind 綁定?
→ 是:this 指向綁定的對象(obj 為 null/undefined 時,非嚴格模式綁全局,嚴格模式綁 obj);
→ 否:進入下一步。 - 函數是否作為對象方法調用(obj.fn ())?
→ 是:this 指向調用方法的對象(obj);
→ 否:進入下一步。 - 是否為嚴格模式(普通調用)?
→ 是:this 綁定到 undefined;
→ 否:this 綁定到全局對象(window/global)。
五、最後:this 的設計意義
為什麼 JavaScript 要設計 this?本質是為了 “代碼複用”—— 讓函數可以在不同對象上調用,無需為每個對象重複定義相同邏輯。
比如一個sayHi函數,通過 this 可以在不同用户對象上覆用,輸出不同用户名:
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 代碼。總而言之,一鍵點贊、評論、喜歡加收藏吧!這對我很重要!