this的四种绑定规则,你都知道吗?
《你不知道的JavaScript》系列二——–this
绑定详解
关于this
的误解
1.指向自身
不管是JavaScript新手开发者还是之前已经接触过其他语言的开发者,都很容易将this理解为指向函数自身。下面简单的例子将证明this远远不止字面意思的这么简单:
1 | function foo(num){ |
从上面代码的输出可以看出从字面意思来理解this是错误的,那如果我增加的count属性和预期的不一样,那我增加的count哪去了?看看下图就一目了然了
2.指向函数的作用域
需要明确的是,this在任何情况下都不指向函数的词法作用域。在JavaScript内部,作用域确实是与对象类似,可见的标识符都是它的属性,但是作用域“对象”无法通过JavaScript代码访问,它存在与JavaScript引擎内部————《你不知道的JavaScript》
1 | function foo(){ |
这段代码试图使用this来联通foo()和bar()的词法作用域,从而让bar()可以访问foo()作用域里面的变量a。这是不可能实现的,我们不能使用this来引用一个词法作用域内部的东西。
每当你想要把词法作用域和this的查找混合使用时,一定要提醒自己,这是无法实现的
this
到底是什么
当一个函数被调用时,会创建一个活动记录(执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this
就是记录的其中一个属性,会在函数执行的过程中用到。
调用栈:为了到达当前执行位置所调用的所有函数
调用位置:在当前正在执行的函数的前一个调用中
this
的四条绑定规则
1.默认绑定
独立函数调用时使用默认绑定规则,绑定到全局对象(非严格模式)或者undefined(严格模式)
1 | function foo(){ |
分析上述代码:
①:在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,一次只能使用默认绑定,无法使用其他规则。
②:上述代码执行在非严格模式下,因此this绑定到了全局对象上
③:this.a被解析为全局变量a,打印2
Tips:只有在非严格模式下调用foo(),默认绑定才能绑定到全局对象
2.隐式绑定
当调用位置有上下文对象,或者说是否被某个对象拥有或者包含时,考虑隐式绑定。
这个说法非常抽象,具体还是需要通过代码来进行讲解:
1 | function foo(){ |
在上述代码中,调用位置会使用obj上下文来引用函数,因此隐式绑定规则会把函数调用中的this绑定到这个上下文对象,因此this.a
和obj.a
是一样的
Tips:对象属性引用链中只有最顶层或者说最后一层会影响调用位置
1 | function foo(){ |
==隐式丢失==
隐式丢失是非常常见的this绑定问题,也就是说被隐式绑定的函数丢失绑定对象,应用默认绑定,将this绑定到全局对象(严格模式)或者undefined(非严格模式)上;
1 | function foo(){ |
虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。值得注意的是,参数传递就是一种隐式赋值,因此在回调函数中,绑定的this也是全局对象。
1 | function foo(){ |
3.显式绑定
使用call()、apply()直接指定this的绑定对象,称之为显式绑定。
显式绑定的两个应用:
(1)硬绑定
观察以下代码,无论之后如何调用函数bar,它总会手动在obj上调用foo
1 | function foo(){ |
由于硬绑定是一种非常常用的模式,所以在ES5中提供了内置的方法Funtion.prototype.bind
,bind()会返回一个硬绑定的新函数,它会将参数设置为this的上下文。
(2)API调用的上下文
第三方库的许多函数,以及JavaScript语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,通常称之为“上下文”(context),作用和bind()一样,确保回调函数可以使用指定的this
1 | function foo(){ |
4.new绑定
使用new来调用函数,会自动执行下面的操作:
- 创建一个全新的对象
- 这个新对象会被执行原型连接
- 这个新对象会绑定到函数调用的this
- 如果这个函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
1 | function foo(a){ |
使用new来调用foo()时,我们会构建一个新对象并将它绑定到foo()调用的this上。
5.四条绑定规则的优先级
优先级:new绑定>显式绑定>隐式绑定>默认绑定
6.绑定例外
(1)被忽略的this
如果将null或者undefined作为this的绑定对象传入call、apply或者bind中时,这些值会被忽略,应用默认绑定规则
(2)间接引用
1 | function foo(){ |
赋值表达式p.foo = o.foo;
的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo。因此此处应用的是默认绑定
(3)箭头函数
箭头函数不适用this的四种绑定规则,而是根据外层作用域来决定this,并且箭头函数的绑定无法修改,new也不行!
1 | function foo(){ |
7.总结
函数是否在new中调用?如果是的话this绑定的是新创建的对象
var bar = new foo();
函数是否通过call、apply显式绑定或者bind硬绑定?如果是的话,this绑定的是指定对象
var bar = foo.call(obj2);
函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是上下文对象
var bar = obj1.foo()
如果以上都不是的话,使用默认绑定。在严格模式下,绑定到undefined,非严格模式下,绑定到全局对象
var bar = foo()
需要注意的是有些调用,特别是赋值,可能会在无意中使用默认绑定标准。并且箭头函数不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this。
8.课后习题
1.
1 | const object = { |
2.
1 | function Pet(name) { |
3.
1 | const object = { |
4.
如何调用logMessage
函数,让它打印 "Hello, World!"
?
1 | const object = { |
5.
1 | const object = { |
6.
1 | var length = 4; |
7.
1 | var length = 4; |
答案
'Hello, World!'
隐式调用,此时的this绑定object上
'Fluffy'
和'Fluffy'
当函数作为构造函数
new Pet('Fluffy')
调用时,构造函数内部的this
等于构造的对象Pet
构造函数中的this.name = name
表达式在构造的对象上创建name
属性。this.getName = () => this.name
在构造的对象上创建方法getName
。而且由于使用了箭头函数,箭头函数内部的this
值等于外部作用域的this
值, 即Pet
。调用
cat.getName()
以及getName()
会返回表达式this.name
,其计算结果为'Fluffy'
。undefined
object.logMessage作为回调函数实际上引用的是logMessage函数本身,因此此时的logMessage()其实是一个不带任何修饰符的函数调用,因此应用了默认绑定。
显式绑定or硬绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16message: 'Hello, World!'
};
function logMessage() {
console.log(this.message); // logs 'Hello, World!'
}
// Using func.call() method
logMessage.call(object);
// Using func.apply() method
logMessage.apply(object);
// Creating a bound function
const boundLogMessage = logMessage.bind(object);
boundLogMessage();'Hello, World!'
和'Goodbye, undefined!'
当调用
object.greet()
时,在greet()
方法内部,this
值等于 object,因为greet
是一个常规函数。因此object.greet()
返回'Hello, World!'
。但是
farewell()
是一个箭头函数,箭头函数中的this
值总是等于外部作用域中的this
值。farewell()
的外部作用域是全局作用域,它是全局对象。因此object.farewell()
实际上返回'Goodbye, ${window.who}!'
,它的结果为'Goodbye, undefined!'
。4
callback()
是在method()
内部使用常规函数调用来调用的。由于在常规函数调用期间的this
值等于全局对象,所以this.length
结果为window.length
。第一个语句
var length = 4
,处于最外层的作用域,在全局对象window
上创建一个属性length
。3