前言
当一个函数调用时,会创建一个执行上下文,这个上下文包括函数调用的一些信息(调用栈,传入参数,调用方式),this
就指向这个执行上下文。
this不是静态的,也并不是在编写的时候绑定的,而是在运行时绑定的。它的绑定和函数声明的位置没有关系,只取决于函数调用的方式。
本篇文章有点长,涉及到很多道面试题,有难有简单,如果能耐心的通读一编,我相信以后this都不成问题。
学习this之前,建议先学习以下知识:
在文章的最开始,陈列一下本篇文章涉及的内容,保证让大家不虚此行。
- 默认绑定
- 隐式绑定
- 隐式绑定丢失
- 显式绑定
- 显式绑定应用
- new绑定
- 箭头函数绑定
- 综合题
- 总结
this指向哪里
在JavaScript
中,要想完全理解this
,首先要理解this
的绑定规则,this
的绑定规则一共有5种:
- 默认绑定
- 隐式绑定
- 显式(硬)绑定
new
绑定ES6
新增箭头函数绑定
下面来一一介绍以下this
的绑定规则。
1.默认绑定
默认绑定通常是指函数独立调用,不涉及其他绑定规则。非严格模式下,this
指向window
,严格模式下,this
指向undefined
。
题目1.1:非严格模式
var foo = 123; function print(){ this.foo = 234; console.log(this); // window console.log(foo); // 234 } print(); 复制代码
非严格模式,print()
为默认绑定,this
指向window
,所以打印window
和234
。
这个foo
值可以说道两句: 如果学习过预编译的知识,在预编译过程中,foo
和print
函数会存放在全局GO
中(即window
对象上),所以上述代码就类似下面这样:
window.foo = 123 function print() { this.foo = 234; console.log(this); console.log(window.foo); } window.print() 复制代码
题目1.2:严格模式
把题目1.1
稍作修改,看看严格模式下的执行结果。
"use strict"
可以开启严格模式
"use strict"; var foo = 123; function print(){ console.log('print this is ', this); console.log(window.foo) console.log(this.foo); } console.log('global this is ', this); print(); 复制代码
注意事项:开启严格模式后,函数内部this
指向undefined
,但全局对象window
不会受影响
答案
global this is Window{...} print this is undefined 123 Uncaught TypeError: Cannot read property 'foo' of undefined 复制代码
题目1.3:let/const
let a = 1; const b = 2; var c = 3; function print() { console.log(this.a); console.log(this.b); console.log(this.c); } print(); console.log(this.a); 复制代码
let/const
定义的变量存在暂时性死区,而且不会挂载到window
对象上,因此print
中是无法获取到a和b
的。
答案
undefined undefined 3 undefined 复制代码
题目1.4:对象内执行
a = 1; function foo() { console.log(this.a); } const obj = { a: 10, bar() { foo(); // 1 } } obj.bar(); 复制代码
foo
虽然在obj
的bar
函数中,但foo
函数仍然是独立运行的,foo
中的this
依旧指向window
对象。
题目1.5:函数内执行
var a = 1 function outer () { var a = 2 function inner () { console.log(this.a) // 1 } inner() } outer() 复制代码
这个题与题目1.4
类似,但要注意,不要把它看成闭包问题
题目1.6:自执行函数
a = 1; (function(){ console.log(this); console.log(this.a) }()) function bar() { b = 2; (function(){ console.log(this); console.log(this.b) }()) } bar(); 复制代码
默认情况下,自执行函数的
this
指向window
自执行函数只要执行到就会运行,并且只会运行一次,this
指向window
。
答案
Window{...} 1 Window{...} 2 // b是imply global,会挂载到window上 复制代码
2.隐式绑定
函数的调用是在某个对象上触发的,即调用位置存在上下文对象,通俗点说就是**XXX.func()**这种调用模式。
此时func
的this
指向XXX
,但如果存在链式调用,例如XXX.YYY.ZZZ.func
,记住一个原则:this永远指向最后调用它的那个对象。
题目2.1:隐式绑定
var a = 1; function foo() { console.log(this.a); } // 对象简写,等同于 {a:2, foo: foo} var obj = {a: 2, foo} foo(); obj.foo(); 复制代码
foo()
: 默认绑定,打印1
obj.foo()
: 隐式绑定,打印2
答案
1 2 复制代码
obj
是通过var
定义的,obj
会挂载到window
之上的,obj.foo()
就相当于window.obj.foo()
,这也印证了this永远指向最后调用它的那个对象规则。
题目2.2:对象链式调用
感觉上面总是空谈链式调用的情况,下面直接来看一个例题:
var obj1 = { a: 1, obj2: { a: 2, foo(){ console.log(this.a) } } } obj1.obj2.foo() // 2 复制代码
3.隐式绑定的丢失
隐式绑定可是个调皮的东西,一不小心它就会发生绑定的丢失。一般会有两种常见的丢失:
- 使用另一个变量作为函数别名,之后使用别名执行函数
- 将函数作为参数传递时会被隐式赋值
隐式绑定丢失之后,this
的指向会启用默认绑定。
具体来看题目:
题目3.1:取函数别名
a = 1 var obj = { a: 2, foo() { console.log(this.a) } } var foo = obj.foo; obj.foo(); foo(); 复制代码
JavaScript
对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。
上面将obj.foo
赋值给foo
,就是将foo
也指向了obj.foo
所指向的堆内存,此后再执行foo
,相当于直接执行的堆内存的函数,与obj
无关,foo
为默认绑定。笼统的记,只要fn前面什么都没有,肯定不是隐式绑定。
答案
2 1 复制代码
不要把这里理解成
window.foo
执行,如果foo
为let/const
定义,foo
不会挂载到window
上,但不会影响最后的打印结果
题目3.2:取函数别名
如果取函数别名没有发生在全局,而是发生在对象之中,又会是怎样的结果呢?
var obj = { a: 1, foo() { console.log(this.a) } }; var a = 2; var foo = obj.foo; var obj2 = { a: 3, foo: obj.foo } obj.foo(); foo(); obj2.foo(); 复制代码
obj2.foo
指向了obj.foo
的堆内存,此后执行与obj
无关(除非使用call/apply
改变this
指向)
答案
1 2 3 复制代码
题目3.3:函数作为参数传递
function foo() { console.log(this.a) } function doFoo(fn) { console.log(this) fn() } var obj = { a: 1, foo } var a = 2 doFoo(obj.foo) 复制代码
用函数预编译的知识来解答这个问题:函数预编译四部曲前两步分别是:
- 找形参和变量声明,值赋予
undefined
- 将形参与实参相统一,也就是将实参的值赋予形参。
obj.foo
作为实参,在预编译时将其值赋值给形参fn
,是将obj.foo
指向的地址赋给了fn
,此后fn
执行不会与obj
产生任何关系。fn
为默认绑定。
答案
Window {…} 2 复制代码
题目3.4:函数作为参数传递
将上面的题略作修改,doFoo
不在window
上执行,改为在obj2
中执行
function foo() { console.log(this.a) } function doFoo(fn) { console.log(this) fn() } var obj = { a: 1, foo } var a = 2 var obj2 = { a: 3, doFoo } obj2.doFoo(obj.foo) 复制代码
console.log(this)
:obj2.doFoo
符合xxx.fn
格式,doFoo
的为隐式绑定,this
为obj2
,打印{a: 3, doFoo: ƒ}
fn()
: 没有于obj2
产生联系,默认绑定,打印2
答案
{a: 3, doFoo: ƒ} 2 复制代码
题目3.5:回调函数
下面这个题目我们写代码时会经常遇到:
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); 复制代码
setTimeout
是异步调用的,只有当满足条件并且同步代码执行完毕后,才会执行它的回调函数。
Tom.introduce()执行
:console
位于setTimeout
的回调函数中,回调函数的this
指向window
Mary.introduce
直接作为setTimeout
的函数参数(类似题目题目3.3
),会发生隐式绑定丢失,this
为默认绑定Lisa.introduce
执行虽然位于setTimeout
的回调函数中,但保持xxx.fn
模式,this
为隐式绑定。
答案
Window {…} Hello, My name is zcxiaobao Hello,My name is zcxiaobao Hello,My name is Lisa 复制代码
所以如果我们想在setTimeout
或setInterval
中使用外界的this
,需要提前存储一下,避免this
的丢失。
const Tom = { name: 'TOM', introduce: function(){ _self = this setTimeout(function(){ console.log('Hello, My name is ',_self.name); }) } } Tom.introduce() 复制代码
题目3.6:隐式绑定丢失综合题
name = 'javascript' ; let obj = { name: 'obj', A (){ this.name += 'this'; console.log(this.name) }, B(f){ this.name += 'this'; f(); }, C(){ setTimeout(function(){ console.log(this.name); },1000); } } let a = obj.A; a(); obj.B(function(){ console.log(this.name); }); obj.C(); console.log(name); 复制代码
本题目不做解析,具体可以参照上面的题目。
答案
javascriptthis javascriptthis javascriptthis javascriptthis 复制代码
4.显式绑定
显式绑定比较好理解,就是通过call()、apply()、bind()
等方法,强行改变this
指向。
上面的方法虽然都可以改变this
指向,但使用起来略有差别:
call()和apply()
函数会立即执行bind()
函数会返回新函数,不会立即执行函数call()和apply()
的区别在于call
接受若干个参数,apply
接受数组。
题目4.1:比较三种调用方式
function foo () { console.log(this.a) } var obj = { a: 1 } var a = 2 foo() foo.call(obj) foo.apply(obj) foo.bind(obj) 复制代码
foo()
: 默认绑定。foo.call(obj)
: 显示绑定,foo
的this
指向obj
foo.apply(obj)
: 显式绑定foo.bind(obj)
: 显式绑定,但不会立即执行函数,没有返回值
答案
2 1 1 复制代码
题目4.2:隐式绑定丢失
题目3.4
发生隐式绑定的丢失,如下代码:我们可不可以通过显式绑定来修正这个问题。
function foo() { console.log(this.a) } function doFoo(fn) { console.log(this) fn() } var obj = { a: 1, foo } var a = 2 doFoo(obj.foo) 复制代码
- 首先先修正
doFoo()
函数的this
指向。
doFoo.call(obj, obj.foo) 复制代码
- 然后修正
fn
的this
。
function foo() { console.log(this.a) } function doFoo(fn) { console.log(this) fn.call(this) } var obj = { a: 1, foo } var a = 2 doFoo(obj.foo) 复制代码
大功告成。
题目4.3:回调函数与call
接着上一个题目的风格,稍微变点花样:
var obj1 = { a: 1 } var obj2 = { a: 2, bar: function () { console.log(this.a) }, foo: function () { setTimeout(function () { console.log(this) console.log(this.a) }.call(obj1), 0) } } var a = 3 obj2.bar() obj2.foo() 复制代码
乍一看上去,这个题看起来有些莫名其妙,setTimeout
那是传了个什么东西?
做题之前,先了解一下setTimeout
的内部机制:(关于异步的执行顺序,可以参考JavaScript之EventLoop)
setTimeout(fn) { if (回调条件满足) ( fn ) } 复制代码
这样一看,本题就清楚多了,类似题目4.2
,修正了回调函数内fn
的this
指向。
答案
2 {a: 1} 1 复制代码
题目4.4:注意call位置
function foo () { console.log(this.a) } var obj = { a: 1 } var a = 2 foo() foo.call(obj) foo().call(obj) 复制代码
foo()
: 默认绑定foo.call(obj)
: 显式绑定foo().call(obj)
: 对foo()
执行的返回值执行call
,foo
返回值为undefined
,执行call()
会报错
答案
2 1 2 Uncaught TypeError: Cannot read property 'call' of undefined 复制代码
题目4.5:注意call位置(2)
上面由于foo
没有返回函数,无法执行call
函数报错,因此修改一下foo
函数,让它返回一个函数。
function foo () { console.log(this.a) return function() { console.log(this.a) } } var obj = { a: 1 } var a = 2 foo() foo.call(obj) foo().call(obj) 复制代码
foo()
: 默认绑定foo.call(obj)
: 显式绑定foo().call(obj)
:foo()
执行,打印2
,返回匿名函数通过call
将this
指向obj
,打印1
。
这里千万注意:最后一个foo().call(obj)
有两个函数执行,会打印2个值。
答案
2 1 2 1 复制代码
题目4.6:bind
将上面的call
全部换做bind
函数,又会怎样那?
call是会立即执行函数,bind会返回一个新函数,但不会执行函数
function foo () { console.log(this.a) return function() { console.log(this.a) } } var obj = { a: 1 } var a = 2 foo() foo.bind(obj) foo().bind(obj) 复制代码
首先我们要先确定,最后会输出几个值?bind
不会执行函数,因此只有两个foo()
会打印a
。
foo()
: 默认绑定,打印2
foo.bind(obj)
: 返回新函数,不会执行函数,无输出foo().bind(obj)
: 第一层foo()
,默认绑定,打印2
,后bind
将foo()
返回的匿名函数this
指向obj
,不执行
答案
2 2 复制代码
题目4.7:外层this与内层this
做到这里,不由产生了一些疑问:如果使用call、bind
等修改了外层函数的this
,那内层函数的this
会受影响吗? (注意区别箭头函数)
function foo () { console.log(this.a) return function() { console.log(this.a) } } var obj = { a: 1 } var a = 2 foo.call(obj)() 复制代码
foo.call(obj)
: 第一层函数foo
通过call
将this
指向obj
,打印1
;第二层函数为匿名函数,默认绑定,打印2
。
答案
1 2 复制代码
题目4.8:对象中的call
把上面的代码移植到对象中,看看会发生怎样的变化?
var obj = { a: 'obj', foo: function () { console.log('foo:', this.a) return function () { console.log('inner:', this.a) } } } var a = 'window' var obj2 = { a: 'obj2' } obj.foo()() obj.foo.call(obj2)() obj.foo().call(obj2) 复制代码
看着这么多括号,是不是感觉有几分头大。没事,咱们来一层一层分析:
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
题目4.9:带参数的call
显式绑定一开始讲的时候,就谈过call/apply
存在传参差异,那咱们就来传一下参数,看看传完参数的this会是怎样的美妙。
var obj = { a: 1, foo: function (b) { b = b || this.a return function (c) { console.log(this.a + b + c) } } } var a = 2 var obj2 = { a: 3 } obj.foo(a).call(obj2, 1) obj.foo.call(obj2)(1) 复制代码
要注意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
答案
6 6 复制代码
麻了吗,兄弟们。进度已经快过半了,休息一会,争取把this
一次性吃透。