一. this 的绑定规则
this 到底指向什么呢?
引言
this
表示函数运行时的上下文对象,是 JavaScript 中的一个关键字,在函数运行时自动出现。但它的值并不是固定的,而是根据函数的调用方式决定。
❓先来看一个让人困惑的问题:
下面我们用同一个函数 foo,分别用三种方式调用,观察 this 的指向。
function foo() {
console.log(this)
}
// 方式一:直接调用(默认绑定)
foo()
// 方式二:通过对象调用(隐式绑定)
var obj = {
name: 'later',
foo: foo
}
obj.foo()
// 方式三:使用 call 显式绑定
foo.call('abc')
🔍 分析运行结果(📍分别说明浏览器和 Node.js)
调用方式 | 代码 | 浏览器中的 this | Node.js 中的 this |
---|---|---|---|
直接调用 | foo() | window(非严)undefined(严) | global(非严)undefined(严) |
隐式绑定 | obj.foo() | obj | obj |
显式绑定 | foo.call('abc') | String {'abc'} (非严)'abc' (严) | [String: 'abc'] (非严)'abc' (严) |
🤯 这说明了什么?
- 函数调用时,JavaScript 为 this 自动绑定一个值;
this
的指向与函数定义的位置无关;- 它完全依赖于函数的调用方式和所处的运行环境;
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 到底绑定了什么?
我们可以通过“调用栈分析法”+“优先级匹配”来判断:
- 函数是否通过
new
调用? → 优先级最高- 是否使用了
call / apply / bind
? → 显式绑定- 是否作为对象属性调用? → 隐式绑定
- 否则 → 默认绑定
- 特殊语法(箭头函数、定时器等)另行分析
🔔 关于 this 的误解很多,常见陷阱包括:
- 将函数赋值给变量或参数后调用,导致绑定丢失;
setTimeout(fn, 0)
中this
被重置;this
和作用域、闭包是完全不同的概念;- 箭头函数没有自己的
this
,它继承外层作用域。
规则一:默认绑定
什么情况下使用默认绑定?
默认绑定的规则适用于独立函数调用,即:函数没有被绑定在某个对象上调用时,this 默认指向全局对象(或为 undefined,取决于是否开启严格模式)。
换句话说,如果一个函数是以最普通的方式直接调用的,例如 foo(),而不是通过对象引用调用(如 obj.foo()),那么默认绑定就生效。
🔹 案例一:最基础的独立函数调用
function foo() {
console.log(this)
}
foo()
运行环境 | 非严格模式输出 | 严格模式输出 |
---|---|---|
浏览器 | window | undefined |
Node.js | global | undefined |
🔹 案例二:函数嵌套调用中的默认绑定
function fn1() {
console.log(this)
}
function fn2() {
fn1() // 直接调用
}
fn2()
fn1() 作为普通函数直接调用,因此 this 应用默认绑定规则。与是否由另一个函数调用它无关。
运行环境 | 非严格模式输出 | 严格模式输出 |
---|---|---|
浏览器 | window | undefined |
Node.js | global | undefined |
🔹 案例三:函数定义在对象中,但通过变量或参数调用
var obj = {
bar: function () { // obj 中的 bar 保存的实际是该函数的引用(内存地址)
console.log(this)
}
}
var baz = obj.bar // baz 在内存中指向的是 obj 的 bar 属性所指向的函数的内存地址
baz() // 直接调用(这里调用实际就是通过内存地址找到堆内存中的函数,独立调用该函数)
尽管函数最初是定义在 obj 中的,但通过变量 baz 调用时,它已经“脱离”了对象上下文。
相当于 baz() 是直接函数调用 → 使用默认绑定规则。
运行环境 | 非严格模式输出 | 严格模式输出 |
---|---|---|
浏览器 | window | undefined |
Node.js | global | undefined |
🔹 案例四:通过参数传递函数引用,触发默认绑定
var obj = {
bar: function () {
console.log(this)
}
}
function fn1(fn) {
fn() // 直接调用
}
fn1(obj.bar) // 这里传入的是 obj 的 bar 属性所指向的函数的内存地址(引用)
obj.bar 是一个函数引用(内存地址),传给 fn1 后没有绑定到任何对象上。
fn() 是直接调用,仍然触发默认绑定规则。
运行环境 | 非严格模式输出 | 严格模式输出 |
---|---|---|
浏览器 | window | undefined |
Node.js | global | undefined |
📍 默认绑定规则
判断要点 | this 的绑定值 |
---|---|
函数直接调用(非严格模式) | 浏览器:window ;Node.js:global |
函数直接调用(严格模式) | undefined |
函数通过变量引用或参数传递后直接调用 | 默认绑定规则生效 |
💡记忆小贴士:函数“脱离了对象”,就会落入“默认绑定”的掌控之中。
规则二:隐式绑定
引言
在 JavaScript 中,this
的指向并不是由函数在哪里定义决定的,而是由它被调用的方式决定的。 当一个函数通过对象属性调用时,this 会被“隐式”绑定到该对象上。
🔍 什么是隐式绑定?
// 调用形式
object.method()
只要函数是通过对象属性来访问并调用的,那么在调用过程中,JavaScript 引擎就会将该对象绑定为函数内部的 this
。
✅ 示例 1:基础隐式绑定
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:多层对象访问
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:函数赋值导致隐式绑定丢失️
function show() {
console.log(this)
}
const obj = {
show: show
}
const fn = obj.show
fn() // 输出:?
🧠 你可能以为 this 还是 obj,但其实不是!
fn() 是直接调用,不再通过 obj,所以默认绑定规则会生效。
运行环境 | 非严格模式输出 | 严格模式输出 |
---|---|---|
浏览器 | window | undefined |
Node.js | global | undefined |
⚠️示例 4:参数传递时也会发生隐式丢失
function display() {
console.log(this)
}
const context = {
display: display
}
function callFn(fn) {
fn() // 独立调用
}
callFn(context.display) // 输出:?
尽管传入的是 context.display,但传入的仅仅是函数引用,传入函数后是“直接调用”,不再有对象上下文了,所以发生了隐式绑定丢失。
运行环境 | 非严格模式输出 | 严格模式输出 |
---|---|---|
浏览器 | window | undefined |
Node.js | global | undefined |
🧠 小结:隐式绑定的要点
调用方式 | this 绑定对象 |
---|---|
obj.fn() | obj |
obj1.obj2.fn() | obj2 |
var fn = obj.fn | 默认绑定(隐式绑定丢失) |
callback(obj.fn) | 默认绑定(隐式绑定丢失) |
规则三:显式绑定
引言
在隐式绑定中,this
的绑定依赖于调用对象。而显式绑定允许我们通过 call、apply 和 bind “手动指定” this 的指向,不再依赖于调用位置。
📌 什么是显式绑定?
JavaScript 中每一个函数都拥有
.call()
和.apply()
方法,它们的第一个参数就是用于绑定 this 的值。
func.call(thisArg, arg1, arg2, ...) // 参数列表
func.apply(thisArg, [arg1, arg2, ...]) // 参数数组
thisArg:函数运行时要绑定为 this 的对象,后续参数分别作为函数调用时传入的参数。
对于这类,明确了 this 要绑定的对象的这种形式,我们称之为显式绑定。
✅ 示例 1:使用 call 显式绑定
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 显式绑定 + 传参
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:函数定义不在对象中,但可对任意对象调用
function showAge() {
console.log(this.age)
}
const user = { age: 20 }
showAge.call(user)
不需要函数写在对象中,也能让它“像是”属于对象一样被调用。
运行环境 | 非严格模式输出 | 严格模式输出 |
---|---|---|
浏览器 | 20 | 20 |
Node.js | 20 | 20 |
✅ 示例 4:bind 返回新函数并绑定 this
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' |
⚠️ 注意事项
- call 和 apply 是立即调用(单次绑定);
- bind 是返回函数(永久绑定);
- 显式绑定优先级高于隐式绑定!
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 个步骤:
- 在堆内存中,创建一个全新的空对象;
- 这个新对象的隐式原型
__proto__
被连接到函数的显式原型prototype
属性上;- 函数内部的
this
被绑定到这个新对象上;- 执行函数体代码,如果该函数没有显式返回一个对象类型(其他类型无效,会返回隐式创建的新对象),则隐式返回这个新创建的对象。
使用 new 关键字来调用函数是会执行如下的操作:
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:基础构造函数使用
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?
function Person(name) {
console.log(this == p1) // false,因为此时还未赋值给 p1,等函数体中代码执行完成才会赋值给 p1。
this.name = name
}
const p1 = new Person('Later')
- this 指向新创建对象;
- 但 p1 变量直到构造函数返回后,才会拿到这个对象的引用。
✅ 示例 2:构造函数显式返回对象时
function Foo() {
this.name = 'Later'
return { name: 'Override' }
}
const obj = new Foo()
console.log(obj) // 输出:{ name: 'Override' }
- 如果构造函数显式返回一个对象,这个对象会覆盖默认返回的 this;
- 但如果返回的是非对象类型(如字符串、数字、布尔值等),则忽略返回值:
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 到底是谁?
❓实际开发中我们常会写出这样的代码:
setTimeout(function () {
console.log(this)
}, 1000)
- 这个函数不是我们调用的,而是 JS 引擎在 1000ms 后自动调用的;
- 我们没有任何“调用方”,也无法指定;
- 那么 this 到底指向谁?
📋 输出结果对比:
运行环境 | 严格模式 this | 非严格模式 this |
---|---|---|
浏览器 | window | window |
Node.js | Timeout 对象 | Timeout 对象 |
✅ 原因解析:
- 在浏览器中,无论是否严格模式,setTimeout 回调函数中的 this 会被隐式绑定到 window;
- 在 Node.js 中,无论是否严格模式,setTimeout 回调函数由 lib/timers.js 中调度,this 是一个 Timeout 包装对象;
✅ 实战建议:使用箭头函数解决 this 丢失
const obj = {
name: 'Later',
run() {
setTimeout(() => {
console.log(this.name) // 'Later'
}, 1000)
}
}
obj.run()
- 箭头函数没有自己的 this,会从定义位置向上寻找;
- 所以此处的 this 正确绑定到 obj。
📋 输出结果对比:
运行环境 | 严格模式 | 非严格模式 |
---|---|---|
浏览器 | Later | Later |
Node.js | Later | Later |
2️⃣ 问题二:DOM 事件回调中的 this 是谁?
❓在浏览器网页开发中,你是否写过这样的代码?
btn.onclick = function () {
console.log(this === btn) // 👉 true
}
✅ 原因:
- 在浏览器中,事件监听器里的 this 会被隐式绑定为触发事件的元素;
- 宿主环境帮你做了 this 的绑定。
❌ 如果你使用箭头函数:
btn.onclick = () => {
console.log(this) // 👉 window(或 undefined),不是 btn!
}
📋 事件回调中的 this 对比:
写法 | this 指向 | 可控制性 |
---|---|---|
function | 事件触发的 DOM 元素 | ❌ 自动绑定 |
箭头函数 | 外层作用域的 this | ❌ 无法绑定 |
✅ 实战建议:
- 需要访问当前元素时,一定使用普通函数;
- 如果你不关心 this 或想用箭头函数保持上下文,确保不会用到事件源。
3️⃣ 问题三:数组 forEach/map 等方法中的回调 this 是谁?
❓你写过类似代码吗?
const list = ['a', 'b', 'c']
list.forEach(function (item) {
console.log(this) // 是什么?
})
📋 输出结果对比:
运行环境 | 非严格模式 | 严格模式 |
---|---|---|
浏览器 | window | undefined |
Node.js | globalThis | undefined |
但你可以这样明确指定:
const context = { name: 'Later' }
list.forEach(function (item) {
console.log(this.name) // 👉 'Later'
}, context)
⚠️ 注意箭头函数不会绑定 this:
list.forEach(item => {
console.log(this) // 外层作用域的 this,不会是 context
}, { name: 'no effect' })
📋 forEach 回调中的 this 对比:
形式 | 严格模式 this | 非严格模式 this |
---|---|---|
function,无 thisArg | undefined | window (浏览器)或 global (Node) |
function,有 thisArg | thisArg | thisArg |
箭头函数(不管是否传 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()。
例如:
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,就像下面这样直接调用:
printInfo.call(user)
于是我们就需要 JavaScript 提供的三个工具函数:call / apply / bind —— 它们都是为了解决一个问题: 🧭 “如何在不改变函数定义位置的前提下,控制 this 的绑定?”
1️⃣ 显式绑定:使用 call() / apply()
这两个方法都能在调用函数时,显式指定 this。
语法区别:
func.call(thisArg, arg1, arg2, ...)
func.apply(thisArg, [arg1, arg2, ...])
- 第一个参数 thisArg:要绑定的 this 对象;
- 其他参数:函数的实参(call 用参数列表传入、apply 用数组传入);
- 都是立即执行。
🧪 案例一:call/apply 绑定 this
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))。
🧪 案例二:传递参数
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
function show() {
console.log(this)
}
show.call(123) // Number 包装对象
show.call('abc') // String 包装对象
show.call(null) // 默认绑定规则生效
show.call(undefined) // 同上
📋 运行环境差异表:
thisArg | 浏览器 非严格 | 浏览器 严格 | Node 非严格 | Node 严格 |
---|---|---|---|---|
123 | Number {123} | 123 | [String: 'abc'] | 123 |
'abc' | String {'abc'} | 'abc' | [Number: 123] | 'abc' |
null | window | null | global | null |
undefined | window | undefined | global | undefined |
如果我们希望一个函数总是显式的绑定到一个对象上,可以怎么做呢?
使用 bind
2️⃣ bind:不执行函数,只返回一个“永久绑定了 this 的新函数”
📘 基本语法:
const newFn = fn.bind(thisArg, preset1, preset2, ...)
- 不会调用该函数,而是返回一个新的绑定了 this 的函数(Bound Function,BF);
- 绑定函数是一个
exotic function object
(怪异函数对象,es6
中的术语)- 可“预设”部分参数(类似柯里化),调用时自动填充。bind 的另一个最简单的用法是使一个函数拥有预设的初始参数:bind 方法中传入的除 thisArg 之外的其余参数,会作为返回的新函数的初始参数,而新函数调用时传入的参数会排在这些初始(预设)参数之后。
🧪 示例:延迟调用 + 预设参数
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 是不可再更改的
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 方法同样成立)
例如:
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 | ❌ | ✅ | 参数预设+调用追加 | ✅(且不可被改) | ❌(无效) |
🎯 实战使用建议
需求场景 | 推荐方法 |
---|---|
立即执行 + 指定 this | call 或 apply |
延迟执行 + this 永久绑定 | bind |
想修改箭头函数的 this(别想了) | 不可能 |
要兼容数组参数(如 from apply) | apply |
三. this 的绑定优先级
引言
学习了四条规则,接下来开发中我们只需要去查找函数的调用应用了哪条规则即可,但是如果一个函数调用位置应用了多条规则,优先级谁更高呢?
1️⃣ 默认绑定优先级最低
毫无疑问,默认规则的优先级是最低的,一旦存在其他规则,默认绑定会被完全覆盖。
function foo() {
console.log(this)
}
foo() // 默认绑定:浏览器中 this -> window
foo.call({ name: 'later' }) // 显式绑定,this -> { name: 'later' }
2️⃣ 显式绑定 > 隐式绑定
当隐式绑定与 call/apply 显式绑定冲突时,显式绑定优先级更高。
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 更高。
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 过的,也会以构造方式优先执行。
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 |
4 | bind 绑定 | bind 返回永久绑定 this 的新函数 |
5 | new 构造绑定 | 创建新对象并绑定 this,优先级最高 |
四. 规则之外的特殊情况
引言
到目前为止,我们已经掌握了 this 的四大绑定规则,以及它们之间的优先级。
但在开发过程中,还有一些“边缘场景”,它们的行为看似违反规则,但其实有合理解释 —— 本节我们就来揭示这些“规则之外”的 this 绑定陷阱。
情况一:忽略显式绑定(传入 null / undefined)
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' |
123 | Number {123} / [Number: 123] | 123 |
null | window / global | null |
undefined | window / global | undefined |
❓发生了什么? 你可能以为:
“call 是显式绑定啊,我传了 null,this 不就该是 null 吗?”
其实:
非严格模式下:
- 如果传入的是值类型(null 和 undefined除外),JS 会自动将其装箱成对应的对象;
- 如果传入的是 null 或 undefined,因为没有对应的包装类型,则视为没有绑定 this,会回退为默认绑定规则;
- 如果是对象类型,this绑定的就是该对象。
严格模式下:
- 传什么,绑定的this就是什么。
🔄 bind(null) 的情况也一样:
function foo() { console.log(this) }
foo.bind(null)() // 非严格模式下,fallback到默认绑定
foo.bind(undefined)()
📋 运行环境差异表:
环境 | 非严格模式 this | 严格模式this |
---|---|---|
Window | window 对象 | null / undefined |
Node.js | global 对象 | null / undefined |
虽然 bind 看似显式绑定了 null,但内部仍然触发 fallback;
bar 执行时实际等价于一个独立函数调用。
情况二:间接函数引用(赋值触发默认绑定)
function foo() { console.log(this) }
var obj1 = {
name: 'obj1',
foo: foo
}
var obj2 = {
name: 'obj2'
}
obj1.foo() // 输出:obj1(隐式绑定)
;(obj2.foo = obj1.foo)() // 输出:window 或 undefined
❓发生了什么?
我们先来看右侧这个调用表达式:
(obj2.foo = obj1.foo)()
// 圆括号会当成一个表达式去解析,
// 而该表达式内部在赋值的同时,也会返回表达式右侧的值(即obj1中的foo属性所指向的函数),
// 所以这里是会当成一个独立的函数调用
拆解过程如下:
- obj2.foo = obj1.foo 是一个赋值表达式;
- 表达式返回的是值(即函数),是右边的函数 foo 本身,并非绑定行为;
- 整个语句其实是 (函数对象)(),而不是通过 obj2.foo() 形式调用,实际调用的是“结果”,而不是路径;
- 所以调用位置没有绑定对象 —— 属于独立函数调用;
- 最终触发的是默认绑定。
情况三:箭头函数不绑定 this(只捕获外层作用域)
🧪 案例:模拟网络请求
这里使用 setTimeout 来模拟网络请求,请求到数据后怎么存放到 data 中呢?
我们需要拿到 obj 对象,设置 data;
但是直接拿到的 this 是 window,我们需要在外层定义:var _this = this
;
在 setTimeout 的回调函数中使用 _this 就代表了 obj 对象;
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:
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 绑定 null | JS 内部 fallback 为默认绑定 | window / Global |
赋值返回函数执行 | 表达式结果是函数 → 独立调用 → 默认绑定 | window / undefined |
箭头函数 | 定义时捕获外层作用域,无法再修改 | 外层 this(不可被改) |
五. 箭头函数的使用
为什么引入箭头函数?
在 ES5 时代,我们通常使用函数声明或函数表达式来定义函数:
// 函数声明
function add(x, y) {
return x + y
}
// 函数表达式
var square = function(n) {
return n * n
}
这些写法虽然语义清晰,但在回调函数、链式处理等场景中写起来较冗长。
于是,ES6 引入了箭头函数(Arrow Function)作为一种更简洁的函数表达形式。
基本语法
箭头函数如何编写呢?
():函数的参数、{}:函数的执行体
// 普通函数表达式
var greet = function(name) {
return 'Hello, ' + name
}
// 箭头函数写法
var greet = (name) => {
return 'Hello, ' + name
}
语法优化
优化1️⃣:如果只有一个形参可以省略 ()。
// 优化前
const greet = (name) => {
return `hello ${name}`
}
// 优化后
const greet = name => {
return `hello ${name}`
}
优化2️⃣:如果函数执行体中,只有一行代码,可以省略函数体的大括号,会自动返回该行代码的返回值(return 语句除外)。
如果函数体中只有一行代码,但该代码是 return 语句时,想省略大括号的话,需要去掉 return 关键词,这样该行代码的返回值会作为整个函数的返回值。
// 优化前
nums.map((n) => {
return n * 2
})
// 优化后
nums.map((n) => n * 2)
优化3️⃣:如果函数执行体中只有一行代码,且该行代码返回值是一个对象字面量,想省略大括号的话,那么就需用小括号包裹这个对象字面量。
如果不用小括号包裹的话, JavaScript 引擎会把对象字面量的大括号
{}
解析成函数的执行体的代码块{}
。
// 优化前
const foo = () => {
return { name: 'foo' }
}
// 错误 🙅
var foo = () => { name: 'foo' }
// 正确 ✅
var foo = () => ({ name: 'foo' })
示例对比:数组处理中的简化
var nums = [1, 2, 3, 4]
// 普通函数
var double1 = nums.map(function(n) {
return n * 2
})
// 箭头函数
var double2 = nums.map(n => n * 2)
箭头函数在这些场景中极大提升了代码的可读性与简洁度。
为什么箭头函数的 this 要单独讲?
在传统函数中,this 的指向是动态的 —— 它取决于函数的调用方式、上下文、显式绑定等:
function foo() {
console.log(this)
}
const obj = { foo }
foo() // 默认绑定
obj.foo() // 隐式绑定
foo.call(123) // 显式绑定
new foo() // new 绑定
📋 不同环境输出差异:
调用方式 | 浏览器 非严格 | 浏览器 严格 | Node 非严格 | Node 严格 |
---|---|---|---|---|
foo() | window | undefined | Global 对象 | undefined |
obj.foo() | obj 对象 | obj 对象 | obj 对象 | obj 对象 |
foo.call(123) | Number {123} | 123 | [Number: 123] | 123 |
new foo() | 新实例 | 新实例 | 新实例 | 新实例 |
而箭头函数彻底改变了这一行为 —— 它的 this 与这些规则统统无关。
箭头函数不绑定 this
箭头函数不绑定 this,只会从「定义时」的作用域链中一层一层往上查找 this。
🔔 提示
函数的作用域链:是在函数声明(编写/定义)时,就确定下来了。
作为对象方法时也不会绑定 obj
var name = 'global'
var obj = {
name: 'obj',
foo: () => {
console.log(this.name)
}
}
obj.foo() // 'global'
虽然语法上写在对象中,但箭头函数不接受任何方式的 this 绑定,自然这里通过 obj.foo() 这种隐式绑定 this 的方式也就无效。
call/apply/bind 对箭头函数无效
var name = 'global'
var foo = () => {
console.log(this.name)
}
foo.call({ name: 'aaa' }) // 'global'
foo.apply({ name: 'aaa' }) // 'global'
foo.bind({ name: 'aaa' })() // 'global'
不能作为构造函数使用
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 对象
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 绑定的类或函数中定义的,则会抛出语法错误。
function NormalClass() {
console.log(new.target)
console.log(new.target === NormalClass)
}
NormalClass()
// undefined
// false
new NormalClass()
// ƒ NormalClass() {}
// true
const ArrowClass = () => {
console.log(new.target) // ❌ Uncaught SyntaxError: new.target expression is not allowed here
}
原因说明:箭头函数无法用 new 构造,也没有构造上下文,因此访问 new.target 会保错。
作为类字段时的绑定机制说明
由于类体具有 this 上下文,因此作为类字段的箭头函数会关闭类的 this 上下文,箭头函数体中的 this 将正确指向实例(对于静态字段来说是类本身)。但是,由于它是一个闭包,而不是函数本身的绑定,因此 this 的值不会根据执行上下文而改变。
class C {
a = 1;
autoBoundMethod = () => {
console.log(this.a);
};
}
const c = new C();
c.autoBoundMethod(); // 1
const { autoBoundMethod } = c;
autoBoundMethod(); // 1
// 如果这是普通方法,此时应该是 undefined
箭头函数属性通常被称作“自动绑定方法”,因为它与普通方法的等价性相同:
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 = 1 、fn = () => {} |
封闭/关闭作用域 | 指函数创建时捕获其外层作用域中的变量,并形成闭包 |
实例上下文 | 指 new 出来的对象,在类字段中 this 默认指向这个对象实例 |
静态字段上下文 | static foo = () => {} 中的 this 捕获的是类构造函数本身(类名) |
原文二
由于它是一个闭包,而不是函数本身的绑定,因此 this 的值不会根据执行上下文而改变。
理解:
“它是一个闭包”
指的是箭头函数创建时就捕获了其所在作用域的变量,包括 this 在内。
“不是函数本身的绑定”
指箭头函数没有自己的 this 绑定机制,不像普通函数那样可以通过 call、apply、bind 或 new 动态设置 this。
“this 的值不会根据执行上下文而改变”
即使你改变调用方式、或把箭头函数当作回调、甚至用 call/apply 显式绑定,它的 this 都不会被修改,永远保持最初的作用域里的 this。
原文三
类字段是在实例(instance)上定义的,而不是在原型(prototype)上定义的,因此每次创建实例都会创建一个新的函数引用并分配一个新的闭包,这可能会导致比普通非绑定方法更多的内存使用。
理解:
- 类字段的箭头函数是定义在实例上的,会为每个实例创建一份新的函数副本;
- 而普通方法是定义在 prototype 上的(所有实例共享);
- 与原型上的方法(共享)不同,这会稍微增加内存使用。
七. this面试题分析
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' 间接函数引用,赋值语句返回结果即独立函数调用
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
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,上层作用域查找
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
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)
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()