博客 / 詳情

返回

JS this取值深度解讀

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 調用的底層流程:
  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) 返回新函數,延遲執行
代碼驗證:

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”(定義時的外層,非調用時)。

核心特性:
  1. 無法通過apply/call/bind改變 this(綁定後仍為外層 this);
  2. 不能作為構造函數(用 new 調用會報錯);
  3. 沒有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)

解決方案

  1. 直接通過對象調用:obj.sayHi()
  2. 用 bind 綁定 this:const hi = obj.sayHi.bind(obj); hi()
  3. 用箭頭函數包裹: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();

解決方案

  1. 用箭頭函數(繼承外層 this):

    
    setTimeout(() => {
      console.log(this.name); // 輸出“孫十八”
    }, 1000);
  2. 保存 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"]

解決方案

  1. 用箭頭函數:

    
    arr.map(item => this.prefix + item);
  2. 傳 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();

解決方案

  1. constructor 中 bind 綁定:

    
    constructor() {
      // ...
      this.handleClick = this.handleClick.bind(this);
    }
  2. 用箭頭函數作為回調:

    
    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); // 輸出“默認商品”

解決方案

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

    
    function Product(name) {
      this.name = name;
      return this; // 或不寫return
    }
  2. 若需返回額外數據,掛載到 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%:

  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 可以在不同用户對象上覆用,輸出不同用户名:


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 代碼。總而言之,一鍵點贊、評論、喜歡收藏吧!這對我很重要!

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.