this绑定的基本原则
this
绑定的基本原则大致上可以分成下列四种:
预设绑定 (Default Binding)
隐含式绑定 (Implicit Binding)
显式绑定 (Explicit Binding)
「new」关键字绑定
预设绑定 (Default Binding)
宣告在全域范畴 (global scope) 的变数,与同名的全域物件 (window 或 global) 的属性是一样的意思。\ 因为预设绑定的关系,当 function 是在普通、未经修饰的情况下被呼叫,也就是当 function 被呼叫的当下如果没有值或是在 func.call(null)
或 func.call(undefined)
此类的情况下,此时里面的 this
会自动指定至全域物件。
但若是加上 "use strict" 宣告成严格模式后,原本预设将 this
绑定至全域物件的行为,会转变成undefined
。
隐含式绑定 (Implicit Binding)
指的是,即使 function 被宣告的地方是在 global scope 中,只要它成为某个物件的参考属性 (reference property),在那个 function 被呼叫的当下,该 function 即被那个物件所包含。
白话说就是:✅this
代表的是function 执行时"所属的物件",而不是 function "本身"
测试你真的懂了吗?
第一题:
var foo = function() { this.count++;};foo.count = 0;for( var i = 0; i < 5; i++ ) { foo();}
foo.count
会是多少?
答案是0
。我知道你可能不能接受,来听我解释。
前面讲过,this
代表的是「function 执行时所属的物件」对吧?
在上面范例中,foo
是 function,同时也是「全域变数」。相信已经看到 DAY 20 这篇的你,一定很清楚「全域变数」的定义吧!
复习一下,「全域变数」代表的是「全域物件的属性」。所以说,foo
其实就是window.foo
。
所以说,当 foo()
在 for
回圈里面跑得很开心的时候,this.count++
始终都是对 window.count
在做递增的处理,因为这个时候的 this
实际上就是window
。
而 window.count
理论上一开始会是undefined
,在做了五次的 ++
之后,你会得到一个 NaN
的结果,而 foo.count
依然是个0
。
记住,this
代表的是function 执行时所属的物件,而不是 function 本身。
第二题:
var bar = function() { console.log( this.a );};var foo = function() { var a = 123; this.bar();};foo();
相信经过前一个例题后,聪明的你应该知道 foo()
的执行结果应该是 undefined
了!
在这个范例中,foo()
可以透过 this.bar
取得bar()
,是因为 this.bar
实际上是指向window.bar
。\ 而 bar()
的 this.a
并非是 foo
中的123
,而是指向window.a
,所以会得到 undefined
的结果。
第三题:
function func() { console.log( this.a );}var obj = { a: 2, foo: func};func(); // undefinedobj.foo(); // 2
在上面的范例中可以看到,根据「预设绑定」的原则,直接呼叫 func()
的情况下,此时的 this.a
实际上会指向window.a
,所以结果是undefined
。
而当我们在 obj
物件中,将 foo
这个属性指到 func()
的时候,再透过 obj
来呼叫 obj.foo()
的时候,虽然实际上仍是 func()
被呼叫, 但此时的 this
就会指向至 obj
这个 owner
的物件上,于是此时的 this.a
就会是 obj.a
也就是2
。
第四题:
理解了隐含式绑定的原则后,继续来看看这个变化过的版本:
function func() { console.log( this.a );}var obj = { a: 2, foo: func};obj.foo(); // 2var func2 = obj.foo;func2(); // ??
在这个版本中,我们宣告另一个变数 func2
指向obj.foo
,那么聪明的你是否可以猜到呼叫 func2()
的结果为何呢?
答案是undefined
。
虽然 func2
看起来是对 obj.foo
的参考,但实际上 func2
参考的对象是window.func
。
决定 this 的关键不在于它属于哪个物件,而是在于 function「呼叫的时机点」,当你透过物件呼叫某个方法 (method) 的时候,此时 this 就是那个物件 (owner object)。
补充测试:巢状回圈中的 this
var obj = { func1: function(){ console.log( this === obj ); var func2 = function(){ // 这里的 this 跟上层不同! console.log( this === obj ); }; func2(); }};obj.func1();
在这个范例当中,会有两次的console
。
在 obj.func1()
里面的 console.log( this === obj );
会印出true
,原因是因为 func1
是透过 obj
来呼叫的。
但 obj.func1()
里面的 func2()
在执行时的 console.log( this === obj );
却会印出false
。
这里必须说明两个重点:
JavaScript 中,用来切分变数的最小作用范围 (scope),也就是我们说的有效范围的单位,就是function
。
当没有特定指明 this 的情况下,预设绑定 (Default Binding)this
为「全域物件」,也就是window
。
换言之,在 func2
里头的this
,若是没有特别透过call()
、apply()
或是 bind()
来指定 this
的话,那么这里的 this
就是window
。
显式绑定(Explicit Binding)
相较于前两种,显式绑定就单纯许多,简单来说就是透过.bind()
/ .call()
/.apply()
这类直接指定 this 的 function 都可被归类至显式绑定的类型。
.bind()
延续上个范例,我们先看bind()
。在前面范例中,我们用 that
这个变数来替代this
,以便取得触发 click
事件的元素。
如果用 bind()
改写的话:
el.addEventListener("click", function(event) { console.log( this.textContent ); // 透过 .bind(this) 来强制指定该 scope 的 this $ajax('[URL]', function(res) { console.log(this.textContent, res); }.bind(this));}, false);
但要注意的是,无论是使用 'use strict'
或是再加上 .bind(xxx)
都无法改变 this
的内容,也不能作为物件建构子 (constructor) 来使用。箭头函式方便归方便,若是你的 function 内会有需要用到 this
的情况时,就需要特别小心你的 this
是不是在不知不觉中换了人来当。
.call()
与.apply()
既然讲到了强制指定 this
的方式,看完了 bind()
与「箭头函式」,接下来就不能不讲到 call()
与apply()
。
假设今天有个 function 长这样:
function func( ){ // do something}
那么我们可以透过 func()
来呼叫它。
当然你也可以用 .call()
或是 .apply()
来呼叫它:
func.call( );func.apply( );
你可能会觉得奇怪,看起来没什么不同对吧,还要多打几个字岂不是自找麻烦。但如果遇上了需要带参数的时候,就又显得有些不同。
基本上 .call()
或是 .apply()
都是去呼叫执行这个 function ,并将这个 function 的 context 替换成第一个参数带入的物件。换句话说,就是强制指定某个物件作为该 function 执行时的this
。
而 .call()
与 .apply()
的作用完全一样,差别只在传入参数的方式有所不同:
function func( arg1, arg2, ... ){ // do something}func.call( context, arg1, arg2, ... );func.apply( context, [ arg1, arg2, ... ]);
.call()
传入参数的方式是由「逗点」隔开,而 .apply()
则是传入整个阵列作为参数,除此之外没有明显的差别。
bind, call, apply 的差异
bind()
让这个 function 在呼叫前先绑定某个物件,使它不管怎么被呼叫都能有固定的this
。 尤其常用在像是 callback function 这种类型的场景,可以想像成是先绑定好 this,然后让 function 在需要时才被呼叫的类型。
而 .call()
与 .apply()
则是使用在 context 较常变动的场景,依照呼叫时的需要带入不同的物件作为该 function 的this
。在呼叫的当下就立即执行。
new」关键字绑定
在建构式下会 new 一个新物件,此时的 this 会指向新的物件。建构式在后续的章节会介绍,此部分只要了解建构式的 this 也是指像物件本身即可。
function FamilyConstructor () { this.mom = '老妈'}var myFamily = new FamilyConstructor();console.log(myFamily.mom);
这一个 this 不会是全域且可以在生成的物件上重新定义 (所以他指向的是该生成的物件)。
var bar = function() { console.log( this.a );};var foo = function() { var a = 123; this.bar();};foo();0
事件中的“this”指的是「触发事件的元素」/event.currentTarget
范例: 像这样,当事件被触发时,此时 this
就会是触发事件的元素,也就是这个范例中的label
。
注意:this
代表的会是「触发事件的目标」元素,也就是 event.currentTarget
而不是e.target
。
var bar = function() { console.log( this.a );};var foo = function() { var a = 123; this.bar();};foo();1
然而,要是我们在事件的 callback function 加入 ajax 的请求,那么根据前面所说的,预设绑定 (Default Binding) 会把这个 callback function 的 this
指定给global object
,也就是window
。
有个很简单的方式可以解决这个问题,那就是透过另一个变数来对目前的 this
做参考:
将事件内的 this
先用一个叫 that
的变数储存它的参考,那么在 ajax 的 callback function 就可以透过 that
来存取到原本事件中的 this
了。
(可以想像成某个 function 在执行的时候,「暂时」把它挂在某个物件下,以便透过 this
去取得该物件的 Context。)
var bar = function() { console.log( this.a );};var foo = function() { var a = 123; this.bar();};foo();2
实务上除了 ajax 的 callback function 以外,另外像是setTimeout
、setInterval
这类的 function,也是常见需要特别处理 this
的场景。
结论 What's "this" in JavaScript?
综合上述范例介绍,我们可以简单总结出一个结论:
这个 function 的呼叫,是透过 new
进行的吗?如果是,那 this
就是被建构出来的物件。
这个 function 是以 .call()
或 .apply()
的方式呼叫的吗?或是 function 透过 .bind()
指定?如果是,那 this
就是被指定的物件。
这个 function 被呼叫时,是否存在于某个物件?如果是,那 this
就是那个物件。
如果没有满足以上条件,则此 function 里的 this 就一定是全域物件,在严格模式下则是undefined
。
好啰嗦....下章讲个精简版的,就酱
原文:https://juejin.cn/post/7094811718013960206