当前位置:网站首页>4000 多字学懂弄通 js 中 this 指向问题,顺便手写实现 call、apply 和 bind
4000 多字学懂弄通 js 中 this 指向问题,顺便手写实现 call、apply 和 bind
2022-07-29 02:54:00 【东方睡衣】
全局作用域中
一般情况下,只有函数里面才有 this。但是在全局作用域下,也有 this,在浏览器环境里指向的是 window(GlobalObject),在 Node 环境下指向的是一个空对象 {}。
函数中
函数的调用执行需要在执行上下文栈(ECStack)中创建函数执行上下文(FEC),FEC 中有与之关联的 VO(AO)信息和 this 等信息,this 会在调用时动态的指向某个对象,图示如下:
四条绑定规则
因为 this 指向的对象并非是在函数定义时确定而是在执行时动态绑定的,所以在 js 中函数里 this 的指向一直是一个令人迷惑,需要当心注意的存在。然而,this 的指向毕竟不是无源之水,无本之木,而是有规可循的,下面就是 this 绑定的 4 条规则:
1. 默认绑定
像下面的例 1.1 这样,直接调用一个函数,就是默认绑定,this 在非严格模式下指向的为全局对象 window(浏览器环境,下文若无特殊说明,皆同):
// 例 1.1
var day = 'Saturday'
function fn() {var day = 'Monday'console.log(this.day) // Saturday
}
fn()
例 1.1 中的函数 fn 既不是通过某个对象(window 除外)比如 obj.fn() 调用,也不是通过 call 或 apply 调用,而是赤裸裸的独立调用,这种情况下 this 绑定的就是全局对象,所以打印 this.day 的时候找到的就是全局作用域中的 Saturday(注意,全局作用域中的 day 是用 var 定义的,如果是用 const 或 let 定义的,则结果会为 undefined)。有时候,情况比例 1.1 会略显复杂,但只要发现函数最后是独立调用的,那么 this 还是指向着 window,如下面的例 1.2:
// 例 1.2
var day = 'Saturday'
const obj = {day: 'Monday',fn() {var day = 'Tuesday'return function () {const day = 'Wednesday'console.log(this.day)}}
}
obj.fn()()
obj.fn() 得到的是 fn 执行返回的函数对象,再跟一个 () 也就是执行这个返回的函数,那么该函数依旧是独立执行的,所以函数中的 this 指向全局,打印结果依旧是大家喜爱的 Saturday。 下面再看一种比较少见的写法:
// 例 2.3
var day = 'Saturday'
const obj1 = {day: 'Monday',fn() {console.log(this.day)}
}
const obj2 = {day: 'Tuesday',fn: null
}
;(obj2.fn = obj1.fn)()
执行第 13 行的结果打印的是 Saturday,因为先执行的 obj2.fn = obj1.fn 返回的是 obj1.fn 指向的函数对象,之后跟上 () 调用,是对函数的独立调用,属于默认绑定,所以 this 指向为 window。这里还有个注意点是在 13 行的开头,这个 ; 是必须加的,否则在解析的时候会认为分号后面括号这部分内容和前面 {} 是一个整体,就会报错。
严格模式下
在严格模式下,默认绑定时 this 指向的是 undefined:
'use strict'
function fn() {console.log(this) // undefined
}
fn()
2. 隐式绑定
如果一个函数,是通过某个对象调用的,那么 this 就会指向该对象,属于隐式绑定。比如下面的例 2.1:
// 例 2.1
var day = 'Saturday'
const obj = {day: 'Monday',fn() {var day = 'Tuesday'console.log(this.day) // Monday}
}
obj.fn()
函数 fn 是通过对象 obj 调用的,所以 fn 中的 this,就指向了 obj,打印 this.day 相当于打印 obj.day,所以结果为 Monday。 其实,所谓的默认绑定和隐式绑定可以说是一个道理,比如我们可以把例 2.1 中做一些修改,生成例 2.2:
// 例 2.2
var day = 'Saturday'
const obj = {day: 'Monday',fn() {var day = 'Tuesday'console.log(this.day)}
}
const foo = obj.fn
foo()
这回不是直接执行 obj.fn(),而是将 obj.fn 先赋值给 foo,也就是让变量 foo 指向了 obj.fn 指向的函数对象,再直接调用 foo() 执行。此时符合默认绑定的情况,foo 中的 this 指向 window。换个角度,亦可看成执行的是 window.foo(),那么根据隐式绑定规则,函数中的 this 指向的应该是调用它的对象,还是 window,所以打印结果就又是令人喜爱的 Saturday 了。
3. 显式绑定
隐式绑定通过对象调用一个函数,这个对象内部必然有个对该函数的引用(比如属性),通过这个引用间接地、隐式地将 this 绑定到了对象上。但有时候,如果对象中不包含对某个函数的引用,我们又想让该对象可以调用这个函数,也就是让函数中的 this 强行指向某个不包含对该函数引用的对象,就得用 js 中所有的函数都拥有的 call、apply 或 bind 方法显示地对函数中的 this 进行绑定(关于它们 3 个方法的区别等细节,本文不做讨论)。下面举个简单的例子:
// 例 3.1
var day = 'Saturday'
function fn() {var day = 'Monday'console.log(this.day)
}
const obj = {day: 'Tuesday'
}
fn.call(obj) // Tuesday
fn.apply(obj) // Tuesday
const foo = fn.bind(obj)
foo() // Tuesday
将 obj 作为参数传给 call 或 apply,那么就是直接调用执行了函数 fn,并且 fn 中的 this 指向的就是 obj。而 bind 则是仅将参数绑定到函数的 this 上,然后返回一个新的函数,并不会直接执行函数。再来看个稍微复杂的例子:
// 例 3.2
var day = 'Saturday'
const obj = {day: 'Monday',fn() {var day = 'Tuesday'return function () {console.log(this.day)}}
}
const obj2 = {day: 'Wednesday'
}
obj.fn.call(obj2)()
obj.fn().call(obj2)
第 15 行,先是 obj.fn.call(obj2) 让 obj.fn 这个函数执行,并且将函数中的 this 指向 obj2。但请注意,这是让 obj.fn 的 this 指向 obj2,如果在第 6 行后面打印 this,那么得到结果会是 Wednesday。而 obj.fn.call(obj2) 的执行结果是返回了函数 function () { console.log(this.day) },然后通过 () 直接调用,应该按照默认绑定的规则来判断,this 指向的是 window,所以第 15 行执行的结果为 Saturday。第 16 行,obj.fn() 执行完后得到函数 function () { console.log(this.day) },通过 call(obj2) 调用执行,并且将 obj2 绑定到了函数内的 this 上,故而执行结果为 Wednesday。
特殊情况
注意,当传给 call、apply 或 bind 的第一个参数为 null 或 undefined 时,在非严格模式下,this 会指向全局对象 window;而在严格模式下,this 会指向 undefined。
// 非严格模式
var day = 'Saturday'
function fn(day) {var day = 'Monday'console.log(this)
}
fn.call(undefined)
fn.apply(undefined)
const foo = fn.bind(undefined)
foo()
第 6、7 和 9 行的执行结果如下图,均指向 window: 
手写实现 call、apply 和 bind 方法
- 实现 call
第 1 行直接在 Function.prototype 上添加 myCall 方法,让其成为所有函数的属性。第一个参数为函数的 this 需要绑定的对象,第二个用剩余参数 (Functions Rest Parameters) 接收需要传入函数的参数; 第 2 行的目的是确保 this 要绑定的目标为对象,这样才能在后面通过隐式绑定的方法来绑定 this。如果不传或是传入的为 undefined / null,则让 this 指向 window 对象。这里 new Object() 也可以直接用 Object() 替换; 第 3 行是为防止与 thisArg 原本的属性名重复,故使用 Symbol() 生成独一无二的属性名; 第 7 行的 ...theArgs 为展开语法 (Spread syntax),虽然和第 1 行的剩余参数写法一样,但可以看成是相反的作用 —— 剩余参数是将一个个参数放入到数组 argArray 中,展开语法是将数组 argArray 中的参数一个个拿出来。
Function.prototype.myCall = function (thisArg, ...argArray) {thisArg = thisArg !== undefined && thisArg !== null ? new Object(thisArg) : windowconst symbol = Symbol()// 给 thisArg 添加属性,值为调用 myCall 方法的函数本身thisArg[symbol] = this// 通过隐式绑定让调用 myCall 方法的函数执行,并且 this 指向传入函数的第一个参数 thisArg。const result = thisArg[symbol](...argArray)// 给 thisArg 添加了原本没有的属性,就需要删除掉delete thisArg[symbol]return result
}
- 实现 apply
apply 的实现和 call 非常相似,唯一不同的地方在于在定义 myApply 方法的时候,第二参数接收的直接是数组,需要给个默认值为空数组,防止当普通函数调用 myApply 但是没传任何参数导致在第 6 行使用展开语法的时候报错:
Function.prototype.myApply = function (thisArg, argArray = []) {thisArg =thisArg !== undefined && thisArg !== null ? new Object(thisArg) : windowconst symbol = Symbol()thisArg[symbol] = thisconst result = thisArg[symbol](...argArray) //delete thisArg[symbol]return result
}
- 实现 bind
bind 是返回一个绑定了指定 this 的新函数,而不会直接执行;且 bind 的传参可以在 bind 里传,也可以在调用返回的函数时传,也可以同时传。比如现有函数 function sum(num1, num2) { return num1 + num2 } ,想传递的参数为 10 和 20,可以 const newSum = sum.bind('Jay',10,20); newSum(),也可以 const newSum = sum.bind('Jay'); newSum(10, 20),还可以 const newSum = sum.bind('Jay', 10); newSum(20)。所以实现起来会与 call 和 apply 稍有不同:
Function.prototype.myBind = function (thisArg, ...argArray) {thisArg =thisArg !== undefined && thisArg !== null ? new Object(thisArg) : windowconst symbol = Symbol()thisArg[symbol] = this// 返回的是一个函数return function (...args) {// 在这个函数内部通过隐式绑定指定 thisconst result = thisArg[symbol](...argArray, ...args)delete thisArg[symbol]return result}
}
4. new 绑定
当函数通过 new 关键字调用时,函数中 this 指向的就是执行 new 后生成的新的实例对象。比如下面的例 4.1:
// 例 4.1
var day = 'Saturday'
function Fn(day) {this.day = day
}
const fn1 = new Fn('Monday')
const fn2 = new Fn('Tuesday')
console.log(fn1.day, fn2.day) // Monday Tuesday
当我们 new 构造函数 Fn 的时候,会创建一个新对象,参数 Monday 就被赋值给了新对象的 day 属性。因为 Fn 中没有返回其它对象,所以会默认把创建的新对象返回出去,相当于把 this 返回出去,由 fn1、fn2 接收。所以 fn1 的 day 为 Monday,fn2 的 day 为 Tuesday。 下面再看一个综合了上述四种绑定规则的案例:
var day = 'Saturday'
function Fn(day) {this.day = dayconsole.log('fn 的 this.day:' + this.day)this.obj = {day: 'Tuesday',foo() {var day = 'Wednesday'console.log('fn.obj.foo 的 this.day:' + this.day)return function () {console.log('fn.obj.foo 执行后返回的函数的 this.day:' + this.day)}}}
}
const obj = { day: 'Thursday' }
const fn = new Fn('Monday')
fn.obj.foo.call(obj)()
fn 是 new Fn 得到的,并且传的参数为 Monday,应用 new 绑定规则,所以执行第 18 行代码时会首先输出第 4 行的打印结果为“fn 的 this.day:Monday”。fn.obj.foo.call(obj) 的执行结果,一个是让 fn.obj.foo 这个函数的 this 指向了 obj,应用的是显示绑定规则,所以第 9 行的打印结果为“fn.obj.foo 的 this.day:Thursday”;另一个是返回了函数 function () { console.log(this.day) },之后再加个() 直接执行这个返回的函数,可以看成是独立的函数调用,为默认绑定,所以第 11 行打印的结果为“fn.obj.foo 执行后返回的函数的 this.day:Saturday”。
规则优先级
如果一个函数调用时,情况同时满足上面多条规则时,就需要根据优先级来确定 this 的指向了。上面 4 条规则的优先级如下: new 绑定 > 显示绑定 > 隐式绑定 > 默认规则 默认绑定的优先级最低这是显而易见的,下面举个例子验证一下隐式绑定和显示绑定的优先级:
var day = 'Saturday'
const obj = {day: 'Monday',fn() {console.log(this.day)}
}
const obj2 = { day: 'Tuesday' }
obj.fn.call(obj2) // Tuesday
打印结果为 Tuesday,可见 call 的显示绑定起了作用,说明显示绑定优先级高于隐式绑定。 因为 new 和 call 或 apply 都是执行函数,所以他们不能同时使用。下面通过 bind 作为显示绑定的代表来和 new 绑定做对比:
var day = 'Saturday'
function Fn(day) {this.day = dayconsole.log(this)
}
const obj = { day: 'Wednesday' }
const foo = Fn.bind(obj)
const fn = new foo('Monday')
先在第 7 行将 Fn 的 this 通过 bind 显示绑定为 obj 对象并将返回的函数赋值给 foo,再在第 8 行通过 new 调用 foo 得到 fn 对象。最后浏览器控制台输出结果为“Fn {day: ‘Monday’}”,说明 this 指向的是 fn 对象,new 的优先级高于 bind。
箭头函数
箭头函数是不绑定 this 的,所以前面这 4 条绑定规则对箭头函数里的 this 的指向就都不适用了。箭头函数的 this 的指向,与其外层作用域 this 的指向相同。下面举个例子:
var day = 'Saturday'
const obj = {day: 'Tuesday',fn: () => console.log(this.day)
}
obj.fn() // Saturday
obj.fn 指向的是一个箭头函数,执行箭头函数时其内部的 this 与其上层作用域的 this 指向相同。需要注意的是,这里箭头函数的上层作用域为全局作用域, obj 这个对象的 {},是不构成作用域的,与直接写个 {} 生成的块作用域不同。 再来个综合点的例子:
var day = 'Saturday'
function Fn(day) {this.day = daythis.obj = {day: 'Tuesday',foo() {var day = 'Wednesday'return () => {console.log(this.day)}}}
}
const obj = { day: 'Friday' }
const fn = new Fn('Monday')
fn.obj.foo.call(obj)()
第 17 行,fn.obj.foo.call(obj) 的执行结果,就是将 fn.obj.foo 这个函数的 this 绑定为了第 14 行定义的 obj 对象,并且返回了一个箭头函数。后面再加个括号执行返回的箭头函数,里面的 this 指向的是外层作用域(fn.obj.foo 这个函数的作用域)的 this,所以结果为 Friday。
内置函数(built-in function)
上面四种规则,适用于对我们自己定义的函数的 this 指向进行分析。但有时候一些函数是如何调用的我们并不清楚,比如函数作为参数传给一个 js 或第三方库的内置函数,像是 setTimeout()、arr.forEach()等,我们并不清楚这些内置函数内部是如何调用作为参数传入的函数的。所以下面专门分析这些特别的情况中,this 的指向问题。
setTimeout
执行下面例 5.1 代码,得到打印的结果为 Saturday,可见 setTimeout() 的回调函数里面的 this,无论是否是在严格模式下,默认指向的都是 window 对象,这是因为由 setTimeout() 调用的代码运行在与所在函数完全分离的执行环境上,可能其在内部实现是通过 apply 绑定了 window 执行的我们传入的函数。
// 例 5.1
var day = 'Saturday'
function fn() {var day = 'Monday'setTimeout(function () {console.log(this.day) // Saturday}, 0)
}
fn()
注意:例 5.1 中 setTimeout 的回调函数是 function 声明的函数,而非箭头函数,如果是箭头函数那么 this 依然是指向上层作用域中的 this。比如将例 5.1 稍作修改如下:
// 例 5.2
var day = 'Saturday'
function fn() {var day = 'Monday'setTimeout(() => {console.log(this.day) // Sunday}, 1000)
}
const obj = { day: 'Sunday' }
fn.call(obj)
例 5.2 中传入 setTimeout 参数为箭头函数,this 指向的就是其上层作用域 fn 的 this,fn 被 call 调用,this 绑定为了 obj,所以打印结果为 Sunday。
事件监听
比如 target.addEventListener('click', function () { ... }),或是 target.onclick = function () { ... } 等,在这些事件监听方法触发后执行的函数中, this 指向的都是触发事件的 target 对象。比如在页面中有个 box:
<div id="box"></div>
我们监听这个 box 元素的点击事件:
box.onclick = function () {console.log('onclick', this)
}
box.addEventListener('click', function () {console.log('addEventListener', this)
})
两种方式的监听,打印的 this 指向的都是 box 元素对象。
数组方法
我们以 Array.prototype.forEach() 举例说明:
const arr = [1, 2, 3]
arr.forEach(function () {console.log(this)
})
得到的打印结果 this 指向的是 window。如果想指定回调函数的 this 指向,可以传入第 2 个参数,比如:
const arr = [1, 2, 3]
arr.forEach(function () {console.log(this)
}, 'Jay')
此时 this 指向的就是字符串 Jay。数组的方法能不能通过传入参数指定回调函数的 this,可以查阅文档或是如果使用的代码编辑器,比如 VS Code 就会有如下图提示,可以看到有可选参数 thisArg,则代表可以指定 this:

当然,这些都是指回调函数不是箭头函数的情况。
边栏推荐
猜你喜欢

会议OA之反馈功能

MySQL compound query (important)

Zone --- line segment tree lazy marking board sub problem
![[opencv] use OpenCV to call mobile camera](/img/66/6207bafbc9696e43da7a60a386a238.jpg)
[opencv] use OpenCV to call mobile camera

Analysis of concepts and terms in data warehouse

解析机器人与人类情感共鸣的主观意识

创客教育的起源和内涵的基本理念

Analyzing the subjective consciousness of emotional resonance between robots and human beings

R语言ERROR: compilation failed for package ‘****‘

第2章 VRP命令行
随机推荐
cuda-gdb提示:/tmp/tmpxft_***.cudafe1.stub.c: No such file or directory.
【打开新世界大门】看测试老鸟如何把API 测试玩弄在鼓掌之间
数仓中概念术语解析
盘点国内外项目协同管理软件:SaaS和定制化成趋势
百度副总裁李硕:数字技术加持下中国劳动力成本上升是好事
Analyzing the subjective consciousness of emotional resonance between robots and human beings
vim常用命令
MySQL large table joint query optimization, large transaction optimization, avoiding transaction timeout, lock wait timeout and lock table
Linux下安装MySQL8.0的详细步骤
JVM基础入门篇一(内存结构)
.net serialize enumeration as string
01-SDRAM:初始化模块的代码
Shell编程规范与变量
MySQL operation database data error: fatal error encoded during command execution
C陷阱与缺陷 第3章 语义“陷阱” 3.7 求值顺序
C#从网址异步获得json格式的数据
Zone --- line segment tree lazy marking board sub problem
2022-07-28 顾宇佳 学习笔记
场景分类任务可用数据集(部分)
Day 5 experiment