JavaScript this 关键词
当一个函数调用时,会创建一个执行上下文,这个上下文包括函数调用的一些信息(调用栈,传入参数,调用方式),this就指向这个执行上下文。
this不是静态的,也并不是在编写的时候绑定的,而是在运行时绑定的。它的绑定和函数声明的位置没有关系,只取决于函数调用的方式。
this的绑定规则一共有五种:
1、默认绑定
2、隐式绑定
3、显示绑定
4、new绑定
5、ES6新增箭头函数绑定
默认绑定
默认绑定通常是指函数独立调用,不涉及其他绑定规则。在严格模式和非严格模式下,this的指向是不同的
1、非严格模式
非严格模式,print()独立调用即
为默认绑定,this
指向window
,所以打印this就是打印window。
其实定义的foo和print方法都是在window下的,this指向的是window,所以为this.foo赋值后,之前定义的foo就会变为234
2、严格模式
严格模式下,函数内部的this指向undefined
3、let/const
let/const
定义的变量存在暂时性死区,而且不会挂载到window
对象上,因此print
中是无法获取到a和b
的。
4、对象内执行
foo
虽然在obj
的bar
函数中,但foo
函数仍然是独立运行的,foo
中的this
依旧指向window
对象。
5、方法内执行
var a = 1 function outer () { var a = 2 function inner () { console.log(this.a) // 1 console.log(this) } inner() } outer()
同4
6、自执行函数
默认情况下,自执行函数的this
指向window
隐式绑定
1、隐式绑定
foo()
: 默认绑定,打印1和window
obj.foo()
: 隐式绑定,打印2和obj
obj
是通过var
定义的,obj
会挂载到window
之上的,obj.foo()
就相当于window.obj.foo()
,这也印证了this永远指向最后调用它的那个对象规则。
2、对象链式调用
可以看出this指向的是最后调用它的obj2
3、隐式绑定的丢失
隐式绑定可是个调皮的东西,一不小心它就会发生绑定的丢失。一般会有两种常见的丢失:
- 使用另一个变量作为函数别名,之后使用别名执行函数
- 将函数作为参数传递时会被隐式赋值
隐式绑定丢失之后,this
的指向会启用默认绑定。
取函数别名
JavaScript
对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。
上面将obj.foo
赋值给foo
,就是将foo
也指向了obj.foo
所指向的堆内存,此后再执行foo
,相当于直接执行的堆内存的函数,与obj
无关,foo
为默认绑定。笼统的记,只要fn前面什么都没有,肯定不是隐式绑定。
函数作为参数传递
用函数预编译的知识来解答这个问题:函数预编译四部曲前两步分别是:
- 找形参和变量声明,值赋予
undefined
- 将形参与实参相统一,也就是将实参的值赋予形参。
obj.foo
作为实参,在预编译时将其值赋值给形参fn
,是将obj.foo
指向的地址赋给了fn
,此后fn
执行不会与obj
产生任何关系。fn
为默认绑定。
回调函数
var name='zcxiaobao'; function introduce(){ console.log('Hello,My name is ', this.name); } const Tom = { name: 'TOM', introduce: function(){ setTimeout(function(){ console.log(this) console.log('Hello, My name is ',this.name); }) } } const Mary = { name: 'Mary', introduce } const Lisa = { name: 'Lisa', introduce } Tom.introduce(); setTimeout(Mary.introduce, 100); setTimeout(function(){ Lisa.introduce(); },200);
Tom.introduce()执行: console位于setTimeout的回调函数中,是独立运行的,所以此时this指向window
Mary.introduce直接作为setTimeout的函数参数,实际不受穿入参数影响,会发生上面说的隐式绑定丢失,变为默认绑定
Lisa.introduce执行虽然位于setTimeout的回调函数中,但保持xxx.fn()模式,为隐式绑定,this此时指向xxx,即Lisa
所以如果我们想在setTimeout
或setInterval
中使用外界的this
,需要提前存储一下,避免this
的丢失。
显式绑定
显式绑定比较好理解,就是通过call()、apply()、bind()
等方法,强行改变this
指向。
上面的方法虽然都可以改变this
指向,但使用起来略有差别:
call()和apply()
函数会立即执行bind()
函数会返回新函数,不会立即执行函数call()和apply()
的区别在于call
接受若干个参数,apply
接受数组。
三种调用方式
foo()
: 默认绑定。foo.call(obj)
: 显示绑定,foo
的this
指向obj
foo.apply(obj)
: 同callfoo.bind(obj)
: 显式绑定,但不会立即执行函数,没有返回值
通过显式绑定修复隐式绑定丢失
1、首先修正讲doFoo的this指向obj
2、修正fn的this
回调函数与call
注意call的位置1
foo()
: 默认绑定foo.call(obj)
: 显式绑定foo().call(obj)
: 对foo()
执行的返回值执行call
,foo
返回值为undefined
,执行call()
会报错
注意call的位置2
foo()
: 默认绑定foo.call(obj)
: 显式绑定foo().call(obj)
:foo()
执行,打印2
,返回匿名函数通过call
将this
指向obj
,打印1
。
这里千万注意:最后一个foo().call(obj)
有两个函数执行,会打印2个值。
bind
call会立即执行函数,而bind会返回一个新函数,但不会执行
首先我们要先确定,最后会输出几个值?bind
不会执行函数,因此只有两个foo()
会打印a
。
foo()
: 默认绑定,打印2
foo.bind(obj)
: 返回新函数,不会执行函数,无输出foo().bind(obj)
: 第一层foo()
,默认绑定,打印2
,后bind
将foo()
返回的匿名函数this
指向obj
,不执行
外层this与内层this
如果使用call、bind
等修改了外层函数的this
,那内层函数的this
会受影响吗?
foo.call(obj)
: 第一层函数foo
通过call
将this
指向obj
,打印1
;第二层函数为匿名函数,默认绑定,打印2
。
对象中的call
obj.foo()()
: 第一层obj.foo()
执行为隐式绑定,打印出foo:obj
;第二层匿名函数为默认绑定,打印inner:window
obj.foo.call(obj2)()
: 类似题目4.7
,第一层obj.foo.call(obj2)
使用call
将obj.foo
的this
指向obj2
,打印foo: obj2
;第二层匿名函数默认绑定,打印inner:window
obj.foo().call(obj2)
: 类似题目4.5
,第一层隐式绑定,打印:foo: obj
,第二层匿名函数使用call
将this
指向obj2
,打印inner: obj2
带参数的call
要注意call
执行的位置:
obj.foo(a).call(obj2, 1)
:obj.foo(a)
: foo的AO中b值为传入的a(形参与实参相统一),值为2,返回匿名函数fn- 匿名函数
fn.call(obj2, 1)
: fn的this指向为obj2,c值为1 this.a + b + c = obj2.a + FooAO.b + c = 3 + 2 + 1 = 6
obj.foo.call(obj2)(1)
:obj.foo.call(obj2)
: obj.foo的this指向obj2,未传入参数,b = this.a = obj2.a = 3;返回匿名函数fn- 匿名函数
fn(1)
: c = 1,默认绑定,this指向window this.a + b + c = window.a + obj2.a + c = 2 + 3 + 1 = 6
显式绑定扩展
上面提了很多call/apply
可以改变this
指向,但都没有太多实用性。下面来一起学几个常用的call与apply
使用。
apply求数组最值
JavaScript中没有给数组提供类似max和min函数,只提供了Math.max/min
,用于求多个数的最值,所以可以借助apply方法,直接传递数组给Math.max/min
类数组转为数组
ES6
未发布之前,没有Array.from
方法可以将类数组转为数组,采用Array.prototype.slice.call(arguments)
或[].slice.call(arguments)
将类数组转化为数组。
es6 的Array.from
数组高阶函数
日常编码中,我们会经常用到forEach、map
等,但这些数组高阶方法,它们还有第二个参数thisArg
,每一个回调函数都是显式绑定在thisArg
上的。
例如下面这个例子
这个例子里的function显示绑定在第二个参数,即obj上
new绑定
使用new
来构建函数,会执行如下四部操作:
- 创建一个空的简单
JavaScript
对象(即{}
); - 为新创建的空对象添加属性
__proto__
,将该属性链接至构造函数的原型对象 ; - 为新创建的空对象作为
this
的上下文 ; - 如果该函数没有返回对象,则返回
this
。
通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this。
属性加方法
zc.introduce()
: zc是new创建的实例,this指向zc,打印zc
zc.howOld()()
: zc.howOld()返回一个匿名函数,匿名函数为默认绑定,因此打印18
new界的天王山
分析后面三个打印结果之前,先补充一些运算符优先级方面的知识
从上图可以看到,部分优先级如下:new(带参数列表) = 成员访问 = 函数调用 > new(不带参数列表)
new Foo.getName()
首先从左往右看:new Foo
属于不带参数列表的new(优先级19),Foo.getName
属于成员访问(优先级20),getName()
属于函数调用(优先级20),同样优先级遵循从左往右执行。
Foo.getName
执行,获取到Foo上的getName
属性- 此时原表达式变为
new (Foo.getName)()
,new (Foo.getName)()
为带参数列表(优先级20),(Foo.getName)()
属于函数调用(优先级20),从左往右执行 new (Foo.getName)()
执行,打印2
,并返回一个以Foo.getName()
为构造函数的实例
这里有一个误区:很多人认为这里的
new
是没做任何操作的的,执行的是函数调用。那么如果执行的是Foo.getName()
,调用返回值为undefined
,new undefined
会发生报错,并且我们可以验证一下该表达式的返回结果。
可见在成员访问之后,执行的是带参数列表格式的new操作。
new Foo().getName()
- 同上一样分析,先执行
new Foo()
,返回一个以Foo
为构造函数的实例Foo
的实例对象上没有getName
方法,沿原型链查找到Foo.prototype.getName
方法,打印3
new new Foo().getName()
从左往右分析: 第一个new不带参数列表(优先级19),
new Foo()
带参数列表(优先级20),剩下的成员访问和函数调用优先级都是20
new Foo()
执行,返回一个以Foo
为构造函数的实例- 在执行成员访问,
Foo
实例对象在Foo.prototype
查找到getName
属性- 执行
new (new Foo().getName)()
,返回一个以Foo.prototype.getName()
为构造函数的实例,打印3
箭头函数
箭头函数没有自己的
this
,它的this
指向外层作用域的this
,且指向函数定义时的this
而非执行时。
this指向外层作用域的this
: 箭头函数没有this
绑定,但它可以通过作用域链查到外层作用域的this
指向函数定义时的this而非执行时
:JavaScript
是静态作用域,就是函数定义之后,作用域就定死了,跟它执行时的地方无关。更详细的介绍见JavaScript之静态作用域与动态作用域。对象方法使用箭头函数
上文说到,箭头函数的
this
通过作用域链查到,intro
函数的上层作用域为window
。
箭头函数与普通函数比较
obj.intro2()()
: 不做赘述,打印My name is tom
obj.intro()()
:obj.intro()
返回箭头函数,箭头函数的this
取决于它的外层作用域,因此箭头函数的this
指向obj
,打印My name is zc
箭头函数与普通函数的嵌套
obj1.intro()()
: 类似题目7.2
,打印obj1,obj1
obj2.intro()()
:obj2.intro()
为箭头函数,this
为外层作用域this
,指向window
。返回匿名函数为默认绑定。打印window,window
obj3.intro()()
:obj3.intro()
与obj2.intro()
相同,返回值为箭头函数,外层作用域intro
的this
指向window
,打印window,window
new碰上箭头函数
zc
是new User
实例,因此构造函数User
的this
指向zc
zc.intro()
: 打印My name is zc
zc.howOld()
:howOld
为箭头函数,箭头函数this由外层作用域决定,且指向函数定义时的this,外层作用域为User
,this
指向zc
,打印My age is 24
call碰上箭头函数
箭头函数由于没有
this
,不能通过call\apply\bind
来修改this
指向,但可以通过修改外层作用域的this
来达成间接修改
obj1.intro.call(obj2)()
: 第一层函数为普通函数,通过call
修改this
为obj2
,打印obj2
。第二层函数为箭头函数,它的this
与外层this
相同,同样打印obj2
。obj1.intro().call(obj2)
: 第一层函数打印obj1
,第二次函数为箭头函数,call
无效,它的this
与外层this
相同,打印obj1
obj1.intro2.call(obj2)()
: 第一层为箭头函数,call
无效,外层作用域为window
,打印window
;第二次为普通匿名函数,默认绑定,打印window
obj1.intro2().call(obj2)
: 与上同,打印window
;第二层为匿名函数,call
修改this
为obj2
,打印obj2
箭头函数扩展
总结
- 箭头函数没有
this
,它的this
是通过作用域链查到外层作用域的this
,且指向函数定义时的this
而非执行时。- 不可以用作构造函数,不能使用
new
命令,否则会报错- 箭头函数没有
arguments
对象,如果要用,使用rest
参数代替- 不可以使用
yield
命令,因此箭头函数不能用作Generator
函数。- 不能用
call/apply/bind
修改this
指向,但可以通过修改外层作用域的this
来间接修改。- 箭头函数没有
prototype
属性。避免使用场景
箭头函数定义对象方法
箭头函数不能作为构造函数
综合题
对象综合体
隐式绑定丢失
foo.bar()
: 隐式绑定,打印20
(foo.bar)()
: 上面提到过运算符优先级的知识,成员访问与函数调用优先级相同,默认从左到右,因此括号可有可无,隐式绑定,打印20
(foo.bar = foo.bar)()
:隐式绑定丢失,给foo.bar
起别名,虽然名字没变,但是foo.bar
上已经跟foo
无关了,默认绑定,打印10
(foo.bar, foo.bar)()
: 隐式绑定丢失,起函数别名,将逗号表达式的值(第二个foo.bar)赋值给新变量,之后执行新变量所指向的函数,默认绑定,打印10
。上面那说法有可能有几分难理解,隐式绑定有个定性条件,就是要满足
XXX.fn()
格式,如果破坏了这种格式,一般隐式绑定都会丢失。arguments
这个题要注意一下,有坑。
fn()
: 默认绑定,打印10
arguments[0]()
: 这种执行方式看起来就怪怪的,咱们把它展开来看看:
arguments
是一个类数组,arguments
展开,应该是下面这样:
arguments: { 0: fn, 1: 1, length: 2 }
arguments[0]
: 这是访问对象的属性0?0不好理解,咱们把它稍微一换,方便一下理解:
arguments: { fn: fn, 1: 1, length: 2 }
- 到这里大家应该就懂了,隐式绑定,
fn
函数this
指向arguments
,打印2压轴题
fn.call(null)
或者fn.call(undefined)
都相当于fn()
obj.fn
为立即执行函数: 默认绑定,this
指向window
我们来一句一句的分析:
var number
: 立即执行函数的AO
中添加number
属性,值为undefined
this.number *= 2
:window.number = 10
number = number * 2
: 立即执行函数AO
中number
值为undefined
,赋值后为NaN
number = 3
:AO
中number
值由NaN
修改为3
- 返回匿名函数,形成闭包
此时的obj可以类似的看成以下代码(注意存在闭包):
obj = { number: 3, fn: function () { var num = this.number; this.number *= 2; console.log(num); number *= 3; console.log(number); } } 复制代码
myFun.call(null)
: 相当于myFun()
,隐式绑定丢失,myFun
的this
指向window
。依旧一句一句的分析:
var num = this.number
:this
指向window
,num = window.num = 10
this.number *= 2
:window.number = 20
console.log(num)
: 打印10number *= 3
: 当前AO
中没有number
属性,沿作用域链可在立即执行函数的AO
中查到number
属性,修改其值为9
console.log(number)
: 打印立即执行函数AO
中的number
,打印9
obj.fn()
: 隐式绑定,fn
的this
指向obj
继续一步一步的分析:
var num = this.number
:this->obj
,num = obj.num = 3
this.number *= 2
:obj.number *= 2 = 6
console.log(num)
: 打印num
值,打印3number *= 3
: 当前AO
中不存在number
,继续修改立即执行函数AO
中的number
,number *= 3 = 27
console.log(number)
: 打印27
console.log(window.number)
: 打印20这里解释一下,为什么
myFun.call(null)
执行时,找不到number
变量,是去找立即执行函数AO
中的number
,而不是找window.number
: JavaScript采用的静态作用域,当定义函数后,作用域链就已经定死。(更详细的解释文章最开始的推荐中有)总结
- 默认绑定: 非严格模式下
this
指向全局对象,严格模式下this
会绑定到undefined
- 隐式绑定: 满足
XXX.fn()
格式,fn
的this
指向XXX
。如果存在链式调用,this永远指向最后调用它的那个对象- 隐式绑定丢失:起函数别名,通过别名运行;函数作为参数会造成隐式绑定丢失。
- 显示绑定: 通过
call/apply/bind
修改this
指向new
绑定: 通过new
来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this
。- 箭头函数绑定: 箭头函数没有
this
,它的this
是通过作用域链查到外层作用域的this
,且指向函数定义时的this
而非执行时
转载于https://juejin.cn/post/7019470820057546766