当一个函数调用时,会创建一个执行上下文,这个上下文包括函数调用的一些信息(调用栈,传入参数,调用方式),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到这里基本接近尾声了,松了一口气。
这篇文章写了好久,找资源,修改博文,各种乱七八糟的杂事,导致迟迟写不出满意的博文。有可能天生理科男的缘故吧,怎么写感觉文章都很生硬,但好在还是顺利写完了。
在文章的最后,感谢一下参考的博客和题目的来源
最后按照阿包惯例,附赠一道面试题:
最后祝大家都能学好前端,步步登神,成为大佬。
这题不难,IIFE 中的赋值过程其实是(赋值过程从右到左):
接下去就不难了,a 是局部变量,b 是全局变量。
前面两个输出没有问题,都是 bar,问题出在后面两个。用了 IIFE 后,匿名函数内的 this 其实已经指到了 window,所以第三个输出 this.foo 其实是 window.foo,而全局对象并没有 foo 这个 key,所以输出 undefined,而第四个输出,因为 self 引用了 myObject,所以还是 bar。
为什么要用 IIFE?
简单来说就是为了能模块化,创建私有变量等等,很多类库(比如 jQuery)都用了这样的写法。
可以参考我以前翻译的一篇文章
严格模式下进行 Javascript 开发有啥好处?
这个就不展开来了,可以参考阮一峰老师的 或者自行谷歌百度。
执行以上两个函数,会返回相同的东西吗?
不会,第二个函数会返回 undefined
。这是由于 Javascript 的封号插入机制决定的,如果某行代码,return 关键词后没有任何东西了,将会自动插入一个封号,显然 foo2 函数中,当 return 后被插入一个封号后,尽管后面的语句不符合规定,但是因为没有执行到,所以也不会报错了。没有 return 任何东西的函数,默认返回
所以很多 Javascript 规范建议把 { 写在一行中,而不是另起一行。
NaN 是什么鬼?typeof 的结果是?如果一个变量的值是 NaN,怎么确定?
NaN 是 'not a number' 的缩写,表示 "不是一个数字",通常会在运算过程中产生:
NaN 和任何变量都不相等,包括 NaN 自己:
上面代码的输出结果是什么?
这个问题正好我之前研究过,有兴趣的可以参考下 ,看懂了还有兴趣的可以看下这篇
写一个方法 isInterger(x),可以用来判断一个变量是否是整数。
ES6 中自带了 Number.isInteger()
方法。但是目前 ES5 中没有自带的方法,可以把一个数去掉小数点后和原数进行比较,判断是否相等,那么问题就演变成如何对一个数进行取整了。
以上代码的输出结果是?
这题不难,只要知道 Javascript 是单线程的语言, 一些异步事件是在主体 js 执行完之后执行即可,所以主体的 1、4 先输出,而后是 3、2,没有问题,因为 3 的定时设置比 2 早。
具体可以参考我之前的文章
判断一个字符串是不是回文。
这里想到一个进阶题,求字符串最长回文子串,可以参考
写一个 sum 方法,使得以上代码得到预期结果。这题可以参考我以前的文章 中的最后一题,理论上此题更简单,因为它没要求能扩展(比如 sum(2)(3)(4)),甚至可以这样:
点击 'Button 4' 后输出什么?如何使得输出能跟预期相同?
答案是输出 5,事实上点击任意的 button,输出都是 5。因为循环结束后,i 值变成了 5。如何改,使得输出分别是 0, 1, 2, 3, 4?用闭包在内存中保存变量,可以参考我之前的文章 中的第 8 题。
这道题我答错了,忽略了 reverse() 方法的一个要重性质,reverse() 方法执行的结果并不是创建一个副本,而是在原数组上直接操作,并返回该数组的引用。
知道了这一点,该题也就迎刃而解了。arr2 其实和 arr1 引用了同一个对象,所以在 arr2 上的操作也会同时反映到 arr1 上。
+"2" 能将字符串 "2" 转换成整数 2,-"2" 同理,而两个变量进行 "+" 运算时,如果都是数字和字符串,则分别进行数字相加和字符串拼接,如果一个是数字一个是字符串,则将数字转为字符串,如果是 "-" 运算呢?则将字符串转为数字。
"A" - "B" 会返回 NaN,因为 "A" 和 "B" 无法转成数字进行运算,这里不要以为 "A" 和 "B" 能转为 ASCII码 进行运算(不要和 C 语言搞混了)。而 NaN 和字符串相加,会转成 "NaN" 和字符串去拼接,NaN 和任何数字相加结果还是 NaN。
以上代码可能会由于递归调用导致栈溢出,如何规避这个问题?
首先,任何递归都可以用迭代来代替,所以改写成迭代方式肯定没有问题。
而原文给的解答令人深思:
利用 setTimeout 的异步性质,完美地去除了这个调用栈。
如果你还是摸不着头脑,简单举个栗子:
上面的代码会依次输出 0 和 1,因为程序中形成了一个调用栈,1 被压到了栈底,最后出栈。
这回就是 1 和 0 了,因为 setTimeout 的回调只有当主体的 js 执行完后才会去执行,所以先输出了 1,自然也就没有栈这一说法了。
事实上,并不是所有递归都能这样改写,如果下一次递归调用依赖于前一次递归调用返回的值,就不能这么改了。
以上代码输出什么?如何能输出期望值?
很显然,输出都是 5。这题和第十三题有些类似,用立即执行函数+闭包即可。
还有种优雅的解法,使用 :
||
和 &&
是短路运算符。先说说 ||,如果前面变量值为 false(包括 0、null、undefined、flase、空字符串等等),则返回后面变量值,否则返回前面变量值。&& 恰恰相反,如果前面变量为 false,则返回前面变量值,否则返回后面变量值。
==
和 ===
的区别, 后者是全等,只有两个值完全相同(或者两个对象引用相同)时,才会返回 true,而前者在比较时会进行隐式的转换。
一道有趣的题目,答案是 456。
其实可以写成这样,清楚些:
其实就是一个立即执行函数+递归,求个阶乘而已(10!)。给立即执行函数加了个名字 f,方便在递归里调用,其实完全可以用 arguments.callee
代替:
显然是 1,闭包,能引用函数外的变量。
执行第一次输出时,this 指向了 window,如何规避这个问题?用 bind 绑定 this 指向,具体可以参考 ,注意低版本 IE 的兼容。
遍历 DOM 树,不难,深度优先搜索即可。
这道题可以拓展,先序遍历 DOM树,中序遍历,甚至后序遍历的结果是?具体可以参考前文 ,都是树,原理是一样一样的。
事件委托,就是某个事件本来该自己干的,但是自己不干,交给别人来干,就叫事件委托。打个比方:一个button对象,本来自己需要监控自身的点击事件,但是自己不来监控这个点击事件,让自己的父节点来监控自己的点击事件。
B,新添加的元素还会有之前的事件
闭包就是能够读取其他函数内部变量的函数
通俗的讲:就是函数a的内部函数b,被函数a外部的一个变量引用的时候,就创建了一个闭包。
==判断内容是否相等不比较类型
===判断内容相等且类型也相等
4、函数没有返回值时,默认返回undefined。
2、作为对象原型链的终点。
json(JavaScript Object Notation)是一种轻量级的数据交换格式。它是基于JavaScript的一个子集。数据格式简单,易于读写,占用带宽小
版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。