Skip to content

一. this 的绑定规则

this 到底指向什么呢?

引言

this 表示函数运行时的上下文对象,是 JavaScript 中的一个关键字,在函数运行时自动出现。但它的值并不是固定的,而是根据函数的调用方式决定。

❓先来看一个让人困惑的问题:

下面我们用同一个函数 foo,分别用三种方式调用,观察 this 的指向。

js
function foo() { 
  console.log(this)
}

// 方式一:直接调用(默认绑定)
foo()

// 方式二:通过对象调用(隐式绑定)
var obj = {
  name: 'later',
  foo: foo
}
obj.foo()

// 方式三:使用 call 显式绑定
foo.call('abc')

🔍 分析运行结果(📍分别说明浏览器和 Node.js)

调用方式代码浏览器中的 thisNode.js 中的 this
直接调用foo()window(非严)undefined(严)global(非严)undefined(严)
隐式绑定obj.foo()objobj
显式绑定foo.call('abc')String {'abc'}(非严)'abc'(严)[String: 'abc'](非严)'abc'(严)

🤯 这说明了什么?

  1. 函数调用时,JavaScript 为 this 自动绑定一个值
  2. this 的指向与函数定义的位置无关
  3. 它完全依赖于函数的调用方式和所处的运行环境
  4. this 是在运行时绑定的,而非编译期决定。

那么 this 到底是怎么样的绑定规则呢?

我们可以总结为 4条基础绑定规则+1个特殊情况,按照匹配顺序如下:

优先级绑定方式示例this 指向
1️⃣new 绑定new Foo()新创建的对象(构造器的实例)
2️⃣显式绑定foo.call(obj)call/apply/bind 显式传入的对象
3️⃣隐式绑定obj.foo()调用该函数的对象 obj
4️⃣默认绑定foo()非严:全局对象(window 和 global),严:undefined
+特殊绑定内置函数 / 箭头函数 / setTimeout(fn)各有例外(详见后文)

📌 小贴士:如何判断 this 到底绑定了什么?

我们可以通过“调用栈分析法”+“优先级匹配”来判断:

  1. 函数是否通过 new 调用? → 优先级最高
  2. 是否使用了 call / apply / bind? → 显式绑定
  3. 是否作为对象属性调用? → 隐式绑定
  4. 否则 → 默认绑定
  5. 特殊语法(箭头函数、定时器等)另行分析

🔔 关于 this 的误解很多,常见陷阱包括:

  • 将函数赋值给变量或参数后调用,导致绑定丢失;
  • setTimeout(fn, 0)this 被重置;
  • this 和作用域、闭包是完全不同的概念;
  • 箭头函数没有自己的 this,它继承外层作用域。

规则一:默认绑定

什么情况下使用默认绑定?

默认绑定的规则适用于独立函数调用,即:函数没有被绑定在某个对象上调用时,this 默认指向全局对象(或为 undefined,取决于是否开启严格模式)。

换句话说,如果一个函数是以最普通的方式直接调用的,例如 foo(),而不是通过对象引用调用(如 obj.foo()),那么默认绑定就生效。

🔹 案例一:最基础的独立函数调用

js
function foo() {
  console.log(this)
}

foo()
运行环境非严格模式输出严格模式输出
浏览器windowundefined
Node.jsglobalundefined

🔹 案例二:函数嵌套调用中的默认绑定

js
function fn1() {
  console.log(this)
}

function fn2() {
  fn1() // 直接调用
}

fn2()

fn1() 作为普通函数直接调用,因此 this 应用默认绑定规则。与是否由另一个函数调用它无关。

运行环境非严格模式输出严格模式输出
浏览器windowundefined
Node.jsglobalundefined

🔹 案例三:函数定义在对象中,但通过变量或参数调用

js
var obj = {
  bar: function () { // obj 中的 bar 保存的实际是该函数的引用(内存地址)
    console.log(this)
  }
}

var baz = obj.bar // baz 在内存中指向的是 obj 的 bar 属性所指向的函数的内存地址
baz() // 直接调用(这里调用实际就是通过内存地址找到堆内存中的函数,独立调用该函数)

尽管函数最初是定义在 obj 中的,但通过变量 baz 调用时,它已经“脱离”了对象上下文。
相当于 baz() 是直接函数调用 → 使用默认绑定规则。

运行环境非严格模式输出严格模式输出
浏览器windowundefined
Node.jsglobalundefined

🔹 案例四:通过参数传递函数引用,触发默认绑定

js
var obj = {
  bar: function () {
    console.log(this)
  }
}

function fn1(fn) {
  fn() // 直接调用
}
fn1(obj.bar) // 这里传入的是 obj 的 bar 属性所指向的函数的内存地址(引用)

obj.bar 是一个函数引用(内存地址),传给 fn1 后没有绑定到任何对象上。
fn() 是直接调用,仍然触发默认绑定规则。

运行环境非严格模式输出严格模式输出
浏览器windowundefined
Node.jsglobalundefined

📍 默认绑定规则

判断要点this 的绑定值
函数直接调用(非严格模式)浏览器:window;Node.js:global
函数直接调用(严格模式)undefined
函数通过变量引用或参数传递后直接调用默认绑定规则生效

💡记忆小贴士:函数“脱离了对象”,就会落入“默认绑定”的掌控之中。

规则二:隐式绑定

引言

在 JavaScript 中,this 的指向并不是由函数在哪里定义决定的,而是由它被调用的方式决定的。 当一个函数通过对象属性调用时,this 会被“隐式”绑定到该对象上。

🔍 什么是隐式绑定?

js
// 调用形式
object.method()

只要函数是通过对象属性来访问并调用的,那么在调用过程中,JavaScript 引擎就会将该对象绑定为函数内部的 this

✅ 示例 1:基础隐式绑定

js
function sayName() {
  console.log(this.name)
}

const person = {
  name: 'Alice',
  sayName: sayName
}

person.sayName()

函数 sayName 被作为 person 对象的属性来调用,所以 this 被绑定为 person 对象。

运行环境非严格模式输出严格模式输出
浏览器'Alice''Alice'
Node.js'Alice''Alice'

✅ 示例 2:多层对象访问

js
function greet() {
  console.log(this.name)
}

const user1 = { name: 'Tom', greet: greet }
const user2 = { name: 'Jerry', user1: user1 }

user2.user1.greet()

调用语句是 user2.user1.greet(),但真正调用函数的是 user1; 所以 this 绑定为 user1。

运行环境非严格模式输出严格模式输出
浏览器'Tom''Tom'
Node.js'Tom''Tom'

⚠️示例 3:函数赋值导致隐式绑定丢失️

js
function show() {
  console.log(this)
}

const obj = {
  show: show
}

const fn = obj.show
fn() // 输出:?

🧠 你可能以为 this 还是 obj,但其实不是!

fn() 是直接调用,不再通过 obj,所以默认绑定规则会生效。

运行环境非严格模式输出严格模式输出
浏览器windowundefined
Node.jsglobalundefined

⚠️示例 4:参数传递时也会发生隐式丢失

js
function display() {
  console.log(this)
}

const context = {
  display: display
}

function callFn(fn) {
  fn() // 独立调用
}

callFn(context.display) // 输出:?

尽管传入的是 context.display,但传入的仅仅是函数引用,传入函数后是“直接调用”,不再有对象上下文了,所以发生了隐式绑定丢失。

运行环境非严格模式输出严格模式输出
浏览器windowundefined
Node.jsglobalundefined

🧠 小结:隐式绑定的要点

调用方式this 绑定对象
obj.fn()obj
obj1.obj2.fn()obj2
var fn = obj.fn默认绑定(隐式绑定丢失)
callback(obj.fn)默认绑定(隐式绑定丢失)

规则三:显式绑定

引言

在隐式绑定中,this 的绑定依赖于调用对象。而显式绑定允许我们通过 call、apply 和 bind “手动指定” this 的指向,不再依赖于调用位置。

📌 什么是显式绑定?

JavaScript 中每一个函数都拥有 .call().apply() 方法,它们的第一个参数就是用于绑定 this 的值

js
func.call(thisArg, arg1, arg2, ...)    // 参数列表
func.apply(thisArg, [arg1, arg2, ...]) // 参数数组

thisArg:函数运行时要绑定为 this 的对象,后续参数分别作为函数调用时传入的参数。

对于这类,明确了 this 要绑定的对象的这种形式,我们称之为显式绑定

✅ 示例 1:使用 call 显式绑定

js
function sayHello() {
  console.log(this.name)
}

const person = { name: 'Later' }

sayHello.call(person)

sayHello 原本是独立函数,通过 .call(person) 显式绑定 this,使其指向 person。

运行环境非严格模式输出严格模式输出
浏览器'Later''Later'
Node.js'Later''Later'

✅ 示例 2:使用 apply 显式绑定 + 传参

js
function introduce(language, hobby) {
  console.log(`${this.name} speaks ${language} and likes ${hobby}`)
}

const user = { name: 'Alice' }

introduce.apply(user, ['JavaScript', 'reading'])

.apply() 与 .call() 区别仅在参数形式,this 显式绑定为 user。

运行环境非严格模式输出严格模式输出
浏览器'Alice speaks JavaScript and likes reading''Alice speaks JavaScript and likes reading'
Node.js'Alice speaks JavaScript and likes reading''Alice speaks JavaScript and likes reading'

🧩 示例 3:函数定义不在对象中,但可对任意对象调用

js
function showAge() {
  console.log(this.age)
}

const user = { age: 20 }

showAge.call(user)

不需要函数写在对象中,也能让它“像是”属于对象一样被调用。

运行环境非严格模式输出严格模式输出
浏览器2020
Node.js2020

✅ 示例 4:bind 返回新函数并绑定 this

js
function greet() {
  console.log(this.name)
}

const user = { name: 'Later' }

const boundGreet = greet.bind(user)

boundGreet() // 输出:'Later'

.bind() 不会立即调用函数,它返回一个永久绑定了 this 的新函数。

运行环境非严格模式输出严格模式输出
浏览器'Later''Later'
Node.js'Later''Later'

⚠️ 注意事项

  1. call 和 apply 是立即调用(单次绑定)
  2. bind 是返回函数(永久绑定)
  3. 显式绑定优先级高于隐式绑定!
js
function say() {
  console.log(this.name)
}

const obj = { name: 'Tom' }
const obj2 = { name: 'Jerry', say: say }

obj2.say.call(obj)

虽然原本 obj2.say() 是隐式绑定,但 call(obj) 显式绑定后,以 obj 为准。

运行环境非严格模式输出严格模式输出
浏览器'Tom''Tom'
Node.js'Tom''Tom'

🧠 小结

方法是否立即调用是否改变 this参数形式
call✅ 是✅ 是逗号分隔参数列表
apply✅ 是✅ 是参数数组
bind❌ 否(返回函数)✅ 是(永久绑定)参数列表(可选)

规则四:new 绑定

引言

在 JavaScript 中,函数不仅仅是可调用的单位,也可以被当作构造函数使用。通过 new 关键字调用函数时,会触发一套独特的绑定规则 —— this 会绑定到一个新创建的对象上

📌 什么是 new 绑定?

当我们使用 new 调用一个函数时,该函数会执行如下 4 个步骤:

  1. 在堆内存中,创建一个全新的空对象;
  2. 这个新对象的隐式原型 __proto__ 被连接到函数的显式原型 prototype 属性上;
  3. 函数内部的 this 被绑定到这个新对象上;
  4. 执行函数体代码,如果该函数没有显式返回一个对象类型其他类型无效,会返回隐式创建的新对象),则隐式返回这个新创建的对象。

使用 new 关键字来调用函数是会执行如下的操作:

js
function Person(name) {
  console.log(this) // this 指向堆内存中新创建的空对象
  console.log(this == p1) // false 因为这时p1还只是个undefined,在堆内存中新创建出来的对象这时还并没有赋值给p1
  this.name = name
}

var p1  = new Person('later')
console.log(p1) // Person {name: 'later'} 这个时候p1已经拿到创建出来的新对象的引用了

✅ 示例 1:基础构造函数使用

js
function Person(name) {
  console.log(this) // 输出:新创建的对象
  this.name = name
}

const p1 = new Person('Later')
console.log(p1) // 输出:Person { name: 'Later' }
  • new Person() 自动将 this 指向一个新的对象;
  • this.name = name 给新对象赋值;
  • 返回新对象赋值给 p1。

❓ 为何 this == p1 是 false?

js
function Person(name) {
  console.log(this == p1) // false,因为此时还未赋值给 p1,等函数体中代码执行完成才会赋值给 p1。
  this.name = name
}
const p1 = new Person('Later')
  • this 指向新创建对象;
  • 但 p1 变量直到构造函数返回后,才会拿到这个对象的引用。

✅ 示例 2:构造函数显式返回对象时

js
function Foo() {
  this.name = 'Later'
  return { name: 'Override' }
}

const obj = new Foo()
console.log(obj) // 输出:{ name: 'Override' }
  • 如果构造函数显式返回一个对象,这个对象会覆盖默认返回的 this;
  • 但如果返回的是非对象类型(如字符串、数字、布尔值等),则忽略返回值
js
function Foo() {
  this.name = 'Later'
  return 'hello'
}

console.log(new Foo()) // 输出:Foo { name: 'Later' }

📊 new vs 普通调用对比

场景普通调用new 调用
this 指向受默认/隐式/显式绑定控制始终指向新创建的对象
是否创建对象❌ 否✅ 是,自动创建新对象并绑定原型
返回值处理正常返回显式返回对象类型会替换默认返回;其他类型无效

特殊规则:内置回调中的 this 是怎么绑定的?

引言

在实际开发中,除了我们能直接控制调用方式的函数外,还有很多“你不会直接调用,但会传给系统或框架调用”的函数,比如:

setTimeout 的回调函数、DOM 元素上的事件处理函数、forEach 等数组方法的回调函数。

这些函数的 this 是由 JavaScript 引擎或宿主环境决定的,不遵循我们之前讲过的四大规则,属于特殊绑定场景。

1️⃣ 问题一:setTimeout 回调函数中的 this 到底是谁?

❓实际开发中我们常会写出这样的代码:

js
setTimeout(function () {
  console.log(this)
}, 1000)
  • 这个函数不是我们调用的,而是 JS 引擎在 1000ms 后自动调用的;
  • 我们没有任何“调用方”,也无法指定;
  • 那么 this 到底指向谁?

📋 输出结果对比:

运行环境严格模式 this非严格模式 this
浏览器windowwindow
Node.jsTimeout 对象Timeout 对象

✅ 原因解析:

  • 在浏览器中,无论是否严格模式,setTimeout 回调函数中的 this 会被隐式绑定到 window;
  • 在 Node.js 中,无论是否严格模式,setTimeout 回调函数由 lib/timers.js 中调度,this 是一个 Timeout 包装对象;

✅ 实战建议:使用箭头函数解决 this 丢失

js
const obj = {
  name: 'Later',
  run() {
    setTimeout(() => {
      console.log(this.name) // 'Later'
    }, 1000)
  }
}

obj.run()
  • 箭头函数没有自己的 this,会从定义位置向上寻找;
  • 所以此处的 this 正确绑定到 obj。

📋 输出结果对比:

运行环境严格模式非严格模式
浏览器LaterLater
Node.jsLaterLater

2️⃣ 问题二:DOM 事件回调中的 this 是谁?

❓在浏览器网页开发中,你是否写过这样的代码?

js
btn.onclick = function () {
  console.log(this === btn) // 👉 true
}

✅ 原因:

  • 在浏览器中,事件监听器里的 this 会被隐式绑定为触发事件的元素;
  • 宿主环境帮你做了 this 的绑定。

❌ 如果你使用箭头函数:

js
btn.onclick = () => {
  console.log(this) // 👉 window(或 undefined),不是 btn!
}

📋 事件回调中的 this 对比:

写法this 指向可控制性
function事件触发的 DOM 元素❌ 自动绑定
箭头函数外层作用域的 this❌ 无法绑定

✅ 实战建议:

  • 需要访问当前元素时,一定使用普通函数;
  • 如果你不关心 this 或想用箭头函数保持上下文,确保不会用到事件源。

3️⃣ 问题三:数组 forEach/map 等方法中的回调 this 是谁?

❓你写过类似代码吗?

js
const list = ['a', 'b', 'c']
list.forEach(function (item) {
  console.log(this) // 是什么?
})

📋 输出结果对比:

运行环境非严格模式严格模式
浏览器windowundefined
Node.jsglobalThisundefined

但你可以这样明确指定:

js
const context = { name: 'Later' }

list.forEach(function (item) {
  console.log(this.name) // 👉 'Later'
}, context)

⚠️ 注意箭头函数不会绑定 this:

js
list.forEach(item => {
  console.log(this) // 外层作用域的 this,不会是 context
}, { name: 'no effect' })

📋 forEach 回调中的 this 对比:

形式严格模式 this非严格模式 this
function,无 thisArgundefinedwindow(浏览器)或 global(Node)
function,有 thisArgthisArgthisArg
箭头函数(不管是否传 thisArg)外层作用域 this外层作用域 this

🧠 总结建议:

场景this 绑定行为推荐使用方式
setTimeout(fn)浏览器:window
Node:Timeout对象
使用箭头函数继承外部 this
DOM 事件(function)this === 事件源元素保留普通函数语法
DOM 事件(箭头)this === 外层作用域不建议用于操作当前元素
forEach(fn, ctx)ctx 或默认规则用 function + thisArg 最稳妥
forEach(arrow)this 为外层作用域推荐用于无 this 操作的简洁场景

二. apply / call / bind

🚩开发中遇到的问题

在很多时候,我们并不希望某个函数“属于对象”,但又想临时把它的 this 指向某个对象。
答案是:使用 JavaScript 为函数提供的 3 个工具方法: call()、apply()、bind()。

例如:

js
function printInfo() {
  console.log(this.name)
}

const user = { name: 'Later' }

printInfo()         // ❌ this 是 window 或 undefined
user.printInfo = printInfo
user.printInfo()    // ✅ this 是 user

但我其实并不想写 user.printInfo = printInfo 这样多余的赋值,我只想临时指定 this,就像下面这样直接调用:

js
printInfo.call(user)

于是我们就需要 JavaScript 提供的三个工具函数:call / apply / bind —— 它们都是为了解决一个问题: 🧭 “如何在不改变函数定义位置的前提下,控制 this 的绑定?”

1️⃣ 显式绑定:使用 call() / apply()

这两个方法都能在调用函数时,显式指定 this。

语法区别:

js
func.call(thisArg, arg1, arg2, ...)
func.apply(thisArg, [arg1, arg2, ...])
  • 第一个参数 thisArg:要绑定的 this 对象;
  • 其他参数:函数的实参(call 用参数列表传入、apply 用数组传入);
  • 都是立即执行。

🧪 案例一:call/apply 绑定 this

js
function bar() {
  console.log(this)
}

bar.call({ name: 'later' }) // output: { name: 'later' }
bar.call(123)               // output: Number对象,包装123
  • bar() 是一个普通函数,默认绑定规则下会指向 window(非严格);
  • 但通过 call(),我们强制将 this 指向传入的对象;
  • 即使传入的是一个原始值,也会自动被包装成对象(如 123 → new Number(123))。

🧪 案例二:传递参数

js
function foo(name, age, height) {
  console.log("foo函数被调用:", this)
  console.log("打印参数:", name, age, height)
}

// apply: 参数作为数组传入
foo.apply("apply", ["kobe", 30, 1.98])

// call: 参数以逗号分隔
foo.call("call", "james", 25, 2.05)

🧪 示例三:绑定原始类型或 null/undefined

js
function show() {
  console.log(this)
}

show.call(123)        // Number 包装对象
show.call('abc')      // String 包装对象
show.call(null)       // 默认绑定规则生效
show.call(undefined)  // 同上

📋 运行环境差异表:

thisArg浏览器 非严格浏览器 严格Node 非严格Node 严格
123Number {123}123[String: 'abc']123
'abc'String {'abc'}'abc'[Number: 123]'abc'
nullwindownullglobalnull
undefinedwindowundefinedglobalundefined

如果我们希望一个函数总是显式的绑定到一个对象上,可以怎么做呢?

使用 bind

2️⃣ bind:不执行函数,只返回一个“永久绑定了 this 的新函数”

📘 基本语法:

js
const newFn = fn.bind(thisArg, preset1, preset2, ...)
  • 不会调用该函数,而是返回一个新的绑定了 this 的函数(Bound Function,BF);
  • 绑定函数是一个 exotic function object怪异函数对象es6 中的术语)
  • 可“预设”部分参数(类似柯里化),调用时自动填充。bind 的另一个最简单的用法是使一个函数拥有预设的初始参数:bind 方法中传入的除 thisArg 之外的其余参数,会作为返回的新函数的初始参数,而新函数调用时传入的参数会排在这些初始(预设)参数之后

🧪 示例:延迟调用 + 预设参数

js
function add(a, b, c) {
  console.log(this.name, a, b, c)
}

const obj = { name: 'Later' }

const boundFn = add.bind(obj, 1) // 预设参数
boundFn(2, 3)  // Later 1 2 3

bind(obj, 1):返回一个新函数,调用它时 this 永远是 obj,且第一个参数始终是 1。

⚠️ bind 函数的 this 是不可再更改的

js
const boundFn = foo.bind({ name: 'A' })
boundFn.call({ name: 'B' }) // 仍然输出 A,不是 B

一旦 bind 绑定了 this,就无法通过 call/apply 再次修改。

3️⃣ ⚠️ 箭头函数不支持 call / apply / bind 绑定 this

由于箭头函数没有自己的 this 指针,通过 call() 或 apply() 方法调用一个箭头函数时,只能传递参数(不能绑定 this),他们的第一个参数会被忽略。(这种现象对于 bind 方法同样成立)

例如:

js
var adder = {
  base: 1,
  add: function(a) {
    var f = v => v + this.base
    return f(a)
  },
  addThruCall: function(a) {
    var f = v => v + this.base
    var b = { base: 2 }
    return f.call(b, a) // call 无效
  }
}

console.log(adder.add(1))         // 输出 2
console.log(adder.addThruCall(1)) // 仍然输出 2

f 是一个箭头函数,this 只能从定义时外层作用域捕获,call(b) 无效!

✅ 总结 & 对比表

方法是否立即调用返回新函数?参数形式是否能绑定 this箭头函数生效?
call参数列表❌(无效)
apply参数数组❌(无效)
bind参数预设+调用追加✅(且不可被改)❌(无效)

🎯 实战使用建议

需求场景推荐方法
立即执行 + 指定 thiscall 或 apply
延迟执行 + this 永久绑定bind
想修改箭头函数的 this(别想了)不可能
要兼容数组参数(如 from apply)apply

三. this 的绑定优先级

引言

学习了四条规则,接下来开发中我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了多条规则,优先级谁更高呢?

1️⃣ 默认绑定优先级最低

毫无疑问,默认规则的优先级是最低的,一旦存在其他规则,默认绑定会被完全覆盖。

js
function foo() {
  console.log(this)
}

foo() // 默认绑定:浏览器中 this -> window
foo.call({ name: 'later' }) // 显式绑定,this -> { name: 'later' }

2️⃣ 显式绑定 > 隐式绑定

当隐式绑定与 call/apply 显式绑定冲突时,显式绑定优先级更高。

js
var obj = {
  foo: function() {
    console.log(this)
  }
}

obj.foo.call('haha') // output:String {'haha'}
分析
  • 本来调用的是 obj.foo,按隐式绑定应为 obj;
  • 但 call('haha') 明确指定了 this;
  • 所以 this 指向 'haha' 被包装的对象。

3️⃣ bind > call/apply

bind 是“永久绑定”,优先级比 call/apply 更高。

js
function func() { console.log(this) }

var func2 = func.bind('haha')

func2.call('hehe')   // output:String {'haha'}
func2.apply('hehe')  // output:String {'haha'}
详细信息
  • bind('haha') 返回了一个新函数;
  • 后续调用 call('hehe') 是作用在这个绑定过的函数上,无法修改 this;
  • 所以最终输出仍然是 'haha'。

4️⃣ new 绑定优先级最高

  • new 是唯一可以“完全忽略 bind 的绑定”的规则;
  • 一旦用了 new,即使这个函数是 bind 过的,也会以构造方式优先执行。
js
function func() {
  console.log(this)
}

var func2 = func.bind('haha')

new func2() // this -> 新创建的对象
详细信息
  • func2 是绑定了 'haha' 的函数;
  • 但 new 的绑定优先级更高,它会创建一个新对象,并将 this 绑定到新对象;
  • 所以 this 是一个新的实例对象。

⚠️ 注意

new 和 call / apply 是不能一起使用的,你不能这样写:new func.call(obj),这是无意义的表达式, 但可以 new (func.bind(obj))(),这时 new 优先级仍然最高。

✅ 总结:绑定优先级从低到高如下

优先级(依次递增)绑定方式说明
1默认绑定无调用对象、无 call/apply/new 时生效
2隐式绑定通过对象调用函数:obj.fn()
3显式绑定call/apply 强制指定 this
4bind 绑定bind 返回永久绑定 this 的新函数
5new 构造绑定创建新对象并绑定 this,优先级最高

四. 规则之外的特殊情况

引言

到目前为止,我们已经掌握了 this 的四大绑定规则,以及它们之间的优先级。

但在开发过程中,还有一些“边缘场景”,它们的行为看似违反规则,但其实有合理解释 —— 本节我们就来揭示这些“规则之外”的 this 绑定陷阱。

情况一:忽略显式绑定(传入 null / undefined)

js
function foo() { console.log(this) }

foo.call({}) 
foo.call('abc')               
foo.call(123)               
foo.call(null)           
foo.call(undefined)

📋 运行环境差异表:

绑定值非严格模式 this(window/node.js)严格模式 this(window/node.js)
{}{}{}
'abc'String {'abc'} / [String: 'abc']'abc'
123Number {123} / [Number: 123]123
nullwindow / globalnull
undefinedwindow / globalundefined

❓发生了什么? 你可能以为:

“call 是显式绑定啊,我传了 null,this 不就该是 null 吗?”

其实:

非严格模式下:

  • 如果传入的是值类型(null 和 undefined除外),JS 会自动将其装箱成对应的对象;
  • 如果传入的是 null 或 undefined,因为没有对应的包装类型,则视为没有绑定 this,会回退为默认绑定规则
  • 如果是对象类型,this绑定的就是该对象。

严格模式下:

  • 传什么,绑定的this就是什么。

🔄 bind(null) 的情况也一样:

js
function foo() { console.log(this) }

foo.bind(null)() // 非严格模式下,fallback到默认绑定
foo.bind(undefined)()

📋 运行环境差异表:

环境非严格模式 this严格模式this
Windowwindow 对象null / undefined
Node.jsglobal 对象null / undefined

虽然 bind 看似显式绑定了 null,但内部仍然触发 fallback;
bar 执行时实际等价于一个独立函数调用。

情况二:间接函数引用(赋值触发默认绑定)

js
function foo() { console.log(this) }

var obj1 = {
  name: 'obj1',
  foo: foo
}

var obj2 = {
  name: 'obj2'
}

obj1.foo()                   // 输出:obj1(隐式绑定)
;(obj2.foo = obj1.foo)()     // 输出:window 或 undefined

❓发生了什么?

我们先来看右侧这个调用表达式:

js
(obj2.foo = obj1.foo)()
// 圆括号会当成一个表达式去解析,
// 而该表达式内部在赋值的同时,也会返回表达式右侧的值(即obj1中的foo属性所指向的函数),
// 所以这里是会当成一个独立的函数调用

拆解过程如下:

  1. obj2.foo = obj1.foo 是一个赋值表达式;
  2. 表达式返回的是值(即函数),是右边的函数 foo 本身,并非绑定行为;
  3. 整个语句其实是 (函数对象)(),而不是通过 obj2.foo() 形式调用,实际调用的是“结果”,而不是路径;
  4. 所以调用位置没有绑定对象 —— 属于独立函数调用;
  5. 最终触发的是默认绑定。

情况三:箭头函数不绑定 this(只捕获外层作用域)

🧪 案例:模拟网络请求

这里使用 setTimeout 来模拟网络请求,请求到数据后怎么存放到 data 中呢?
我们需要拿到 obj 对象,设置 data;
但是直接拿到的 this 是 window,我们需要在外层定义:var _this = this;
在 setTimeout 的回调函数中使用 _this 就代表了 obj 对象;

js
var obj = {
  data: [],
  fetchData() {
    var _this = this
    setTimeout(function() {
      var res = ['abc', 'cba'] // 模拟获取到的数据
      _this.data.push(...res)
    }, 1000)
  }
}

obj.fetchData()

分析:

当通过 obj.fetchData 调用函数时,fetchData 函数内部的 this 绑定到了 obj 对象,
当执行 setTimeout 中的回调函数时,其中的 this 是指向 window 的,
因为 setTimeout 中的函数是显式绑定到 window 身上的,所以其内部的回调函数中 this 指向的就是 window 了,
我们可以定义一个 fetchData 函数内部的变量 _this 指向函数内部的 this,
当我们使用 _this 变量,会往上层作用域一层一层往查找该变量,
最后在上层函数作用域中找到 _this 变量

✅ 使用箭头函数继承 this:

js
var obj = {
  data: [],
  fetchData() {
    setTimeout(() => {
      var res = ['abc', 'cba']
      this.data.push(...res) // this 正确指向 obj
    }, 1000)
  }
}

obj.fetchData()

箭头函数在定义时就会捕获外层 this;
无法被 call/apply/bind 修改;
非常适合写在回调、事件处理等 this 容易丢失的地方。

✅ 小结:规则之外的 this 行为

情况原因说明this 结果(Window / Node.js)
call/apply 绑定 nullJS 内部 fallback 为默认绑定window / Global
赋值返回函数执行表达式结果是函数 → 独立调用 → 默认绑定window / undefined
箭头函数定义时捕获外层作用域,无法再修改外层 this(不可被改)

五. 箭头函数的使用

为什么引入箭头函数?

在 ES5 时代,我们通常使用函数声明或函数表达式来定义函数:

js
// 函数声明
function add(x, y) {
  return x + y
}

// 函数表达式
var square = function(n) {
  return n * n
}

这些写法虽然语义清晰,但在回调函数、链式处理等场景中写起来较冗长。

于是,ES6 引入了箭头函数(Arrow Function)作为一种更简洁的函数表达形式。

基本语法

箭头函数如何编写呢?

():函数的参数、{}:函数的执行体

js
// 普通函数表达式
var greet = function(name) {
  return 'Hello, ' + name
}

// 箭头函数写法
var greet = (name) => {
  return 'Hello, ' + name
}

语法优化

优化1️⃣:如果只有一个形参可以省略 ()

js
// 优化前
const greet = (name) => {
  return `hello ${name}`
}

// 优化后
const greet = name => {
  return `hello ${name}`
}

优化2️⃣:如果函数执行体中,只有一行代码,可以省略函数体的大括号,会自动返回该行代码的返回值(return 语句除外)。

如果函数体中只有一行代码,但该代码是 return 语句时,想省略大括号的话,需要去掉 return 关键词,这样该行代码的返回值会作为整个函数的返回值。

js
// 优化前
nums.map((n) => {
  return n * 2
})

// 优化后
nums.map((n) => n * 2)

优化3️⃣:如果函数执行体中只有一行代码,且该行代码返回值是一个对象字面量,想省略大括号的话,那么就需用小括号包裹这个对象字面量

如果不用小括号包裹的话, JavaScript 引擎会把对象字面量的大括号 {} 解析成函数的执行体的代码块 {}

js
// 优化前
const foo = () => {
  return { name: 'foo' }
}

// 错误 🙅
var foo = () => { name: 'foo' }

// 正确 ✅
var foo = () => ({ name: 'foo' })

示例对比:数组处理中的简化

js
var nums = [1, 2, 3, 4]

// 普通函数
var double1 = nums.map(function(n) {
  return n * 2
})

// 箭头函数
var double2 = nums.map(n => n * 2)

箭头函数在这些场景中极大提升了代码的可读性与简洁度。

为什么箭头函数的 this 要单独讲?

在传统函数中,this 的指向是动态的 —— 它取决于函数的调用方式、上下文、显式绑定等:

js
function foo() {
  console.log(this)
}
const obj = { foo }

foo()         // 默认绑定
obj.foo()     // 隐式绑定
foo.call(123) // 显式绑定
new foo()     // new 绑定

📋 不同环境输出差异:

调用方式浏览器 非严格浏览器 严格Node 非严格Node 严格
foo()windowundefinedGlobal 对象undefined
obj.foo()obj 对象obj 对象obj 对象obj 对象
foo.call(123)Number {123}123[Number: 123]123
new foo()新实例新实例新实例新实例

而箭头函数彻底改变了这一行为 —— 它的 this 与这些规则统统无关。

箭头函数不绑定 this

箭头函数不绑定 this,只会从「定义时」的作用域链中一层一层往上查找 this

🔔 提示

函数的作用域链:是在函数声明(编写/定义)时,就确定下来了。

作为对象方法时也不会绑定 obj

js
var name = 'global'

var obj = {
  name: 'obj',
  foo: () => {
    console.log(this.name)
  }
}

obj.foo() // 'global'

虽然语法上写在对象中,但箭头函数不接受任何方式的 this 绑定,自然这里通过 obj.foo() 这种隐式绑定 this 的方式也就无效。

call/apply/bind 对箭头函数无效

js
var name = 'global'
var foo = () => {
  console.log(this.name)
}

foo.call({ name: 'aaa' })     // 'global'
foo.apply({ name: 'aaa' })    // 'global'
foo.bind({ name: 'aaa' })()   // 'global'

不能作为构造函数使用

js
const ArrowFn = () => {}
console.log("prototype" in ArrowFn) // false
console.log("__proto__" in ArrowFn) // true
new ArrowFn() // ❌ TypeError: ArrowFn is not a constructor

箭头函数不能作为构造函数使用,因为它们是没有显式原型 prototype 属性的,但是作为对象是有隐式原型的(不能和 new 一起使用,会抛出错误)。

没有 arguments 对象

js
function normalFn() {
  console.log('normalFn arguments:', arguments)
}

const arrowFn = () => {
  console.log('arrowFn arguments:', arguments)
}

normalFn(1, 2, 3) 
arrowFn(1, 2, 3)

// output(浏览器环境):
// normalFn arguments: Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
// ❌ Uncaught ReferenceError: arguments is not defined

原因说明:

  • 普通函数拥有自己的 arguments 对象;
  • 箭头函数没有自己的 arguments,引用时会往上层作用域查找;
  • 如果上层作用域也没有 arguments(比如顶层作用域),则报错。

没有 new.target 元属性

🔔 提示

new.target 元属性允许你检测函数或构造函数是否是通过 new 运算符被调用的。在通过 new 运算符执行的函数或构造函数中,new.target 返回一个指向 new 调用的构造函数或函数的引用。在普通的函数调用中,new.target 的值是 undefined。

在普通函数中,如果函数是直接通过 new 构造的,则 new.target 指向函数本身。如果函数不是通过 new 调用的,则 new.target 是 undefined。函数可以被用作 extends 的基类,这种情况下 new.target 可能指向子类。

在箭头函数中,new.target 是从周围的作用域继承的。如果箭头函数不是在另一个具有 new.target 绑定的类或函数中定义的,则会抛出语法错误。

js
function NormalClass() {
  console.log(new.target)
  console.log(new.target === NormalClass)
}

NormalClass()
// undefined
// false
new NormalClass()
// ƒ NormalClass() {}
// true
js
const ArrowClass = () => {
  console.log(new.target) // ❌ Uncaught SyntaxError: new.target expression is not allowed here
}

原因说明:箭头函数无法用 new 构造,也没有构造上下文,因此访问 new.target 会保错。

作为类字段时的绑定机制说明

MDN原句

由于类体具有 this 上下文,因此作为类字段的箭头函数会关闭类的 this 上下文,箭头函数体中的 this 将正确指向实例(对于静态字段来说是类本身)。但是,由于它是一个闭包,而不是函数本身的绑定,因此 this 的值不会根据执行上下文而改变。

js
class C {
  a = 1;
  autoBoundMethod = () => {
    console.log(this.a);
  };
}

const c = new C();
c.autoBoundMethod(); // 1
const { autoBoundMethod } = c;
autoBoundMethod(); // 1
// 如果这是普通方法,此时应该是 undefined

箭头函数属性通常被称作“自动绑定方法”,因为它与普通方法的等价性相同:

js
class C {
  a = 1;
  constructor() {
    this.method = this.method.bind(this);
  }
  method() {
    console.log(this.a);
  }
}

备注: 类字段是在实例(instance)上定义的,而不是在原型(prototype)上定义的,因此每次创建实例都会创建一个新的函数引用并分配一个新的闭包,这可能会导致比普通非绑定方法更多的内存使用。

原文一

类体具有 this 上下文,因此作为类字段的箭头函数会关闭类的 this 上下文,箭头函数体中的 this 将正确指向实例(对于静态字段来说是类本身)。

理解:

当你在类中以“类字段”的方式定义箭头函数时,这个箭头函数会在定义时捕获类的实例作用域中的 this,并将它永久保存在函数内部(形成闭包)。因此:

  • 无论你以后怎么调用这个函数,this 始终指向它创建时的那个实例;
  • 如果这是一个静态字段,则箭头函数捕获的是类本身而不是实例。

这就是为什么我们说箭头函数“封闭(固定)了类的 this 上下文”。

📌 解释关键词:

术语说明
类字段指写在类体中、不是方法的属性,如 a = 1fn = () => {}
封闭/关闭作用域指函数创建时捕获其外层作用域中的变量,并形成闭包
实例上下文指 new 出来的对象,在类字段中 this 默认指向这个对象实例
静态字段上下文static foo = () => {} 中的 this 捕获的是类构造函数本身(类名)

原文二

由于它是一个闭包,而不是函数本身的绑定,因此 this 的值不会根据执行上下文而改变。

理解:

“它是一个闭包”
指的是箭头函数创建时就捕获了其所在作用域的变量,包括 this 在内。

“不是函数本身的绑定”
指箭头函数没有自己的 this 绑定机制,不像普通函数那样可以通过 call、apply、bind 或 new 动态设置 this。

“this 的值不会根据执行上下文而改变”
即使你改变调用方式、或把箭头函数当作回调、甚至用 call/apply 显式绑定,它的 this 都不会被修改,永远保持最初的作用域里的 this

原文三

类字段是在实例(instance)上定义的,而不是在原型(prototype)上定义的,因此每次创建实例都会创建一个新的函数引用并分配一个新的闭包,这可能会导致比普通非绑定方法更多的内存使用。

理解:

  • 类字段的箭头函数是定义在实例上的,会为每个实例创建一份新的函数副本;
  • 而普通方法是定义在 prototype 上的(所有实例共享);
  • 与原型上的方法(共享)不同,这会稍微增加内存使用。

七. this面试题分析

js
var name = '222'
var person = {
  name: 'person',
  sayName: function() {
    console.log(this.name)
  }
}

function sayName() {
  var s = person.sayName
  s()
  person.sayName()
  (person.sayName)()
  (b = person.sayName)()
}

sayName()
// output:
// '222'    默认绑定(独立函数调用)
// 'person' 隐式绑定
// 'person' 隐式绑定,等同于person.sayName(), 加小括号不加效果完全一样,加小括号控制代码优先级,不加小括号,本身点.语法优先级就很高
// '222'    间接函数引用,赋值语句返回结果即独立函数调用
js
var name = '222'
var person1 = {
  name: 'person1',
  foo1: function() {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function() {
    return function() {
      console.log(this.name)
    }
  },
  foo4: function() {
    return () => {
      console.log(this.name)
    }
  }
}

var person2 = {name: 'person2'}

person1.foo1()                // 'person1'  隐式绑定
person1.foo1.call(person2)    // 'person2'  显式绑定

person1.foo2()                // '222'      上层作用域查找this
person1.foo2.call(person2)    // '222'      箭头函数不绑定this,上层作用域查找this

person1.foo3()()              // '222'      默认绑定 拿到返回的普通函数直接进行默认调用
person1.foo3.call(person2)()  // '222'      默认绑定 拿到返回的普通函数直接进行默认调用
person1.foo3().call(person2)  // 'person2'  显式绑定

person1.foo4()()              // 'person1'  箭头函数调用会从上层作用域查找this
person1.foo4.call(person2)()  // 'person2'  箭头函数调用会从上层作用域查找this
person1.foo4().call(person2)  // 'person1'  箭头函数不绑定this,从上层作用域查找this
js
var name = '222'
function Person(name) {
  this.name = name
  this.foo1 = function() {
    console.log(this.name)
  }
  this.foo2 = () => console.log(this.name)
  this.foo3 = function() {
    return function() {
      console.log(this.name)
    }
  }
  this.foo4 = function() {
    return () => {
      console.log(this.name)
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.foo1()                // 'person1'  隐式绑定
person1.foo1.call(person2)    // 'person2'  显式绑定

person1.foo2()                // 'person1'  上层作用域查找:上层作用域中的this绑定到了person1
person1.foo2.call(person2)    // 'person1'  不绑定this,上层作用域查找

person1.foo3()()              // '222'      默认绑定 独立函数调用
person1.foo3.call(person2)()  // '222'      默认绑定 独立函数调用
person1.foo3().call(person2)  // 'person2'  显式绑定

person1.foo4()()              // 'person1'  上层作用域查找
person1.foo4.call(person2)()  // 'person2'  foo4函数中this绑定person2,箭头函数查找上层作用域foo4函数中的this
person1.foo4().call(person2)  // 'person1'  箭头函数不绑定this,上层作用域查找
js
var name = '222'
function Person(name) {
  this.name = name
  this.obj = {
    name: 'obj',
    foo1: function() {
      return function() {
        console.log(this.name)
      }
    },
    foo2: function() {
      return () => {
        console.log(this.name)
      }
    }
  }
}

var person1 = new Person('person1')
var person2 = new Person('person2')

person1.obj.foo1()()              // '222'      普通函数默认调用(独立函数调用)
person1.obj.foo1.call(person2)()  // '222'      普通函数默认调用(独立函数调用)
person1.obj.foo1().call(person2)  // 'person2'  普通函数显式绑定调用

person1.obj.foo2()()              // 'obj'      上层作用域中查找到foo2函数作用域中的this隐式绑定obj
person1.obj.foo2.call(person2)()  // 'person2'  上层作用域中查找到foo2函数作用域中的this显式绑定person2
person1.obj.foo2().call(person2)  // 'obj'      箭头函数不绑定this,上层作用域中查找到foo2函数作用域中的this隐式绑定obj

总结:

普通函数中的 this,是根据调用的时候绑定的。

箭头函数中是没有 this 的,所以是不被绑定的,会去上层作用域中查找 this ,是根据执行上下文决定的。

八. 手写apply、call、bind


js
Function.prototype.executionFn = function(thisArg, args) {
  thisArg = ( thisArg == null || thisArg == undefined ) ? window : Object(thisArg) // 这里是防止传入绑定的参数为空或者是原始类型的值,如string或number
  Object.defineProperty(thisArg, '_this', {
    configurable: true,
    enumerable: false
    value: this
  })
  args ? thisArg._this(...args) : thisArg._this()
  delete this._this
}

/**
* 私有的apply方法
* @param {any} thisArg 传入的绑定对象
* @param {Array} args 传入的参数数组
*/
Function.prototype._apply = function(thisArg, args) {
  // thisArg = ( thisArg == null || thisArg == undefined ) ? window : Object(thisArg)
  // thisArg._this = this
  // Object.defineProperty(thisArg, '_this', {
  //   configurable: true,
  //   enumerable: false
  // })
  // args ? thisArg._this(...args) : thisArg._this()
  this.executionFn(thisArg, args)
}

function foo(name, age) {
  console.log(this, name, age)
}

foo._apply({name: 'later'}, ['later', 18])
foo._apply('abc', ['later', 18])
foo._apply()

/**
* 私有的call方法
* @param {any} thisArg 传入的绑定对象
* @param {Array} args 传入的参数列表
*/
Function.prototype._call = function(thisArg, ...args) {
  // thisArg = ( thisArg == null || thisArg == undefined ) ? window : Object(thisArg)
  // thisArg._this = this
  // Object.defineProperty(thisArg, '_this', {
  //   configurable: true,
  //   enumerable: false
  // })
  // args ? thisArg._this(...args) : thisArg._this()
  this.executionFn(thisArg, args)
}

foo._call()
foo._call({name: 'later'}, 'later', 18)
foo._call('abc', 'later', 18)
js
function foo(name, age) {
  console.log(this, name, age)
}
/**
* 私有的bind方法
* @param {any} thisArg 传入的绑定对象
* @param {Array} args 传入的参数列表
*/
Function.prototype._bind = function(thisArg, ...args) {
  thisArg = ( thisArg == null || thisArg == undefined ) ? window : Object(thisArg)
  Object.defineProperty(thisArg, '_this', {
    configurable: true,
    enumerable: false,
    value: this
  })

  return (...rest) => {
    args ? thisArg._this(...args, ...rest) : thisArg._this(...rest)
  }
}

var foo2 = foo._bind({name: 'later'}, 'later')
foo2(23)
foo2(18)
foo2()
var foo3 = foo._bind()
foo3()

如有转载或CV请标注本站原文地址