这个看起来似乎有点傻,其实可以做出很有趣的事。比如使用eval你可以根据用户的输入直接创建函数。这可以使程序根据时间或用户输入的不同而使程序本身发生变化,通过举一反三,你可以获得惊人的效果。
当一个函数调用时,会创建一个执行上下文,这个上下文包括函数调用的一些信息(调用栈,传入参数,调用方式),this
就指向这个执行上下文。
this不是静态的,也并不是在编写的时候绑定的,而是在运行时绑定的。它的绑定和函数声明的位置没有关系,只取决于函数调用的方式。
本篇文章有点长,涉及到很多道面试题,有难有简单,如果能耐心的通读一编,我相信以后this都不成问题。
学习this之前,建议先学习以下知识:
在文章的最开始,陈列一下本篇文章涉及的内容,保证让大家不虚此行。
在JavaScript
中,要想完全理解this
,首先要理解this
的绑定规则,this
的绑定规则一共有5种:
ES6
新增箭头函数绑定
下面来一一介绍以下this
的绑定规则。
默认绑定通常是指函数独立调用,不涉及其他绑定规则。非严格模式下,this
指向window
,严格模式下,this
指向undefined
。
这个foo
值可以说道两句:
如果学习过预编译的知识,在预编译过程中,foo
和print
函数会存放在全局GO
中(即window
对象上),所以上述代码就类似下面这样:
把题目1.1
稍作修改,看看严格模式下的执行结果。
注意事项:开启严格模式后,函数内部this
指向undefined
,但全局对象window
不会受影响
let/const
定义的变量存在暂时性死区,而且不会挂载到window
对象上,因此print
中是无法获取到a和b
的。
foo
虽然在obj
的bar
函数中,但foo
函数仍然是独立运行的,foo
中的this
依旧指向window
对象。
这个题与题目1.4
类似,但要注意,不要把它看成闭包问题
默认情况下,自执行函数的
this
指向window
自执行函数只要执行到就会运行,并且只会运行一次,this
指向window
。
函数的调用是在某个对象上触发的,即调用位置存在上下文对象,通俗点说就是**XXX.func()**这种调用模式。
此时func
的this
指向XXX
,但如果存在链式调用,例如XXX.YYY.ZZZ.func
,记住一个原则:this永远指向最后调用它的那个对象。
感觉上面总是空谈链式调用的情况,下面直接来看一个例题:
隐式绑定可是个调皮的东西,一不小心它就会发生绑定的丢失。一般会有两种常见的丢失:
隐式绑定丢失之后,this
的指向会启用默认绑定。
JavaScript
对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。
上面将obj.foo
赋值给foo
,就是将foo
也指向了obj.foo
所指向的堆内存,此后再执行foo
,相当于直接执行的堆内存的函数,与obj
无关,foo
为默认绑定。笼统的记,只要fn前面什么都没有,肯定不是隐式绑定。
不要把这里理解成
window.foo
执行,如果foo
为let/const
定义,foo
不会挂载到window
上,但不会影响最后的打印结果
如果取函数别名没有发生在全局,而是发生在对象之中,又会是怎样的结果呢?
用函数预编译的知识来解答这个问题:函数预编译四部曲前两步分别是:
undefined
obj.foo
作为实参,在预编译时将其值赋值给形参fn
,是将obj.foo
指向的地址赋给了fn
,此后fn
执行不会与obj
产生任何关系。fn
为默认绑定。
将上面的题略作修改,doFoo
不在window
上执行,改为在obj2
中执行
fn()
: 没有于obj2
产生联系,默认绑定,打印2
下面这个题目我们写代码时会经常遇到:
setTimeout
是异步调用的,只有当满足条件并且同步代码执行完毕后,才会执行它的回调函数。
所以如果我们想在setTimeout
或setInterval
中使用外界的this
,需要提前存储一下,避免this
的丢失。
本题目不做解析,具体可以参照上面的题目。
显式绑定比较好理解,就是通过call()、apply()、bind()
等方法,强行改变this
指向。
上面的方法虽然都可以改变this
指向,但使用起来略有差别:
bind()
函数会返回新函数,不会立即执行函数
foo.bind(obj)
: 显式绑定,但不会立即执行函数,没有返回值
题目3.4
发生隐式绑定的丢失,如下代码:我们可不可以通过显式绑定来修正这个问题。
接着上一个题目的风格,稍微变点花样:
乍一看上去,这个题看起来有些莫名其妙,setTimeout
那是传了个什么东西?
这样一看,本题就清楚多了,类似题目4.2
,修正了回调函数内fn
的this
指向。
上面由于foo
没有返回函数,无法执行call
函数报错,因此修改一下foo
函数,让它返回一个函数。
这里千万注意:最后一个foo().call(obj)
有两个函数执行,会打印2个值。
将上面的call
全部换做bind
函数,又会怎样那?
call是会立即执行函数,bind会返回一个新函数,但不会执行函数
首先我们要先确定,最后会输出几个值?bind
不会执行函数,因此只有两个foo()
会打印a
。
foo.bind(obj)
: 返回新函数,不会执行函数,无输出
做到这里,不由产生了一些疑问:如果使用call、bind
等修改了外层函数的this
,那内层函数的this
会受影响吗?
foo.call(obj)
: 第一层函数foo
通过call
将this
指向obj
,打印1
;第二层函数为匿名函数,默认绑定,打印2
。
把上面的代码移植到对象中,看看会发生怎样的变化?
看着这么多括号,是不是感觉有几分头大。没事,咱们来一层一层分析:
显式绑定一开始讲的时候,就谈过call/apply
存在传参差异,那咱们就来传一下参数,看看传完参数的this会是怎样的美妙。
要注意call
执行的位置:
obj.foo(a)
: foo的AO中b值为传入的a(形参与实参相统一),值为2,返回匿名函数fn
麻了吗,兄弟们。进度已经快过半了,休息一会,争取把this
一次性吃透。
上面提了很多call/apply
可以改变this
指向,但都没有太多实用性。下面来一起学几个常用的call与apply
使用。
日常编码中,我们会经常用到forEach、map
等,但这些数组高阶方法,它们还有第二个参数thisArg
,每一个回调函数都是显式绑定在thisArg
上的。
关于数组高阶函数的知识可以参考: JavaScript之手撕高阶数组函数
使用new
来构建函数,会执行如下四部操作:
JavaScript
对象(即{}
);
__proto__
,将该属性链接至构造函数的原型对象 ;
this
的上下文 ;
this
。
关于new更详细的知识,可以参考:
通过new来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this。
这个题很难不让人想到如下代码,都是函数嵌套,具体解法是类似的,可以对比来看一下啊。
new
界的天王山,每次看懂后,没过多久就会忘掉,但这次要从根本上弄清楚该题。
接下来一起来品味品味:
分析后面三个打印结果之前,先补充一些运算符优先级方面的知识(图源:)
从上图可以看到,部分优先级如下:new(带参数列表) = 成员访问 = 函数调用 > new(不带参数列表)
首先从左往右看:new Foo
属于不带参数列表的new(优先级19),Foo.getName
属于成员访问(优先级20),getName()
属于函数调用(优先级20),同样优先级遵循从左往右执行。
这里有一个误区:很多人认为这里的
new
是没做任何操作的的,执行的是函数调用。那么如果执行的是Foo.getName()
,调用返回值为undefined
,new undefined
会发生报错,并且我们可以验证一下该表达式的返回结果。
可见在成员访问之后,执行的是带参数列表格式的new操作。
步骤4
一样分析,先执行new Foo()
,返回一个以Foo
为构造函数的实例
从左往右分析: 第一个new不带参数列表(优先级19),new Foo()
带参数列表(优先级20),剩下的成员访问和函数调用优先级都是20
new Foo()
执行,返回一个以Foo
为构造函数的实例
通过这一步比较应该能更好的理解上面的执行顺序。
兄弟们,革命快要成功了,再努力一把,以后this都小问题啦。
箭头函数没有自己的this
,它的this
指向外层作用域的this
,且指向函数定义时的this
而非执行时。
this指向外层作用域的this
: 箭头函数没有this
绑定,但它可以通过作用域链查到外层作用域的this
指向函数定义时的this而非执行时
: JavaScript
是静态作用域,就是函数定义之后,作用域就定死了,跟它执行时的地方无关。更详细的介绍见。
上文说到,箭头函数的this
通过作用域链查到,intro
函数的上层作用域为window
。
箭头函数由于没有this
,不能通过callapplyind
来修改this
指向,但可以通过修改外层作用域的this
来达成间接修改
call
修改this
为obj2
,打印obj2
。第二层函数为箭头函数,它的this
与外层this
相同,同样打印obj2
。
this
,它的this
是通过作用域链查到外层作用域的this
,且指向函数定义时的this
而非执行时。
new
命令,否则会报错
arguments
对象,如果要用,使用rest
参数代替
yield
命令,因此箭头函数不能用作Generator
函数。
call/apply/bind
修改this
指向,但可以通过修改外层作用域的this
来间接修改。
箭头函数不能作为构造函数
DOM中事件的回调函数中this已经封装指向了调用元素,如果使用构造函数,其this会指向window对象
学完上面的知识,是不是感觉自己已经趋于化境了,现在就一起来华山之巅一决高下吧。
这个题目并不难,就是把上面很多题做了个整合,如果上面都学会了,此题问题不大。
突然出现了一个代码很少的题目,还乍有些不习惯。
(foo.bar)()
: 上面提到过运算符优先级的知识,成员访问与函数调用优先级相同,默认从左到右,因此括号可有可无,隐式绑定,打印20
(foo.bar, foo.bar)()
: 隐式绑定丢失,起函数别名,将逗号表达式的值(第二个foo.bar)赋值给新变量,之后执行新变量所指向的函数,默认绑定,打印10
。
上面那说法有可能有几分难理解,隐式绑定有个定性条件,就是要满足
XXX.fn()
格式,如果破坏了这种格式,一般隐式绑定都会丢失。
这个题要注意一下,有坑。
arguments[0]()
: 这种执行方式看起来就怪怪的,咱们把它展开来看看:
arguments[0]
: 这是访问对象的属性0?0不好理解,咱们把它稍微一换,方便一下理解:
到这里大家应该就懂了,隐式绑定,fn
函数this
指向arguments
,打印2
我们来一句一句的分析:
此时的obj可以类似的看成以下代码(注意存在闭包):
number *= 3
: 当前AO
中没有number
属性,沿作用域链可在立即执行函数的AO
中查到number
属性,修改其值为9
JavaScript采用的静态作用域,当定义函数后,作用域链就已经定死。(更详细的解释文章最开始的推荐中有)
this
指向全局对象,严格模式下this
会绑定到undefined
XXX.fn()
格式,fn
的this
指向XXX
。如果存在链式调用,this永远指向最后调用它的那个对象
new
绑定: 通过new
来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this
。
this
,它的this
是通过作用域链查到外层作用域的this
,且指向函数定义时的this
而非执行时
this到这里基本接近尾声了,松了一口气。
这篇文章写了好久,找资源,修改博文,各种乱七八糟的杂事,导致迟迟写不出满意的博文。有可能天生理科男的缘故吧,怎么写感觉文章都很生硬,但好在还是顺利写完了。
在文章的最后,感谢一下参考的博客和题目的来源
最后按照阿包惯例,附赠一道面试题:
最后祝大家都能学好前端,步步登神,成为大佬。
版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。