关于this
this被自动定义在所有函数的作用域中。this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中用到。对this的误解
- this指向函数自身
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function foo(num){
console.log('foo:' + num);
this.count++;
}
foo.count = 0;
var i;
for (i =0; i < 10; i++) {
if (i > 6) {
foo(i)
}
}
// foo:7
// foo:8
// foo:9
console.log(foo.count) // 0
上面的代码中,我们使用
foo.count
来记录函数foo被调用的次数,每次调用foo的时候count
的值就加1。但是在循环结束之后我们打印函数foo的count属性发现还是0。这和我们想象的不一样。这是因为此时的函数是在全局作用域下被调用的,这时this指向的是全局作用域,并不是指向函数foo自身。- this指向函数的作用域
this指向函数的作用域是第二个常见的误解。this在任何情况下都不指向函数的词法作用域。
- this指向函数自身
this解析
在上面的讲解中我们知道每个函数的this是在调用时(运行时)被绑定的,完全取决于函数的调用位置
。- 调用位置
所谓的调用位置就是函数在代码中被调用的位置(不是声明的位置)。想要准确的寻找到函数的调用位置,我们需要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。而调用位置就在当前正在执行的函数的前一个调用中
。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function baz(){
// 调用栈为baz
// 当前的调用位置是全局作用域
console.log('baz')
bar() // bar的调用位置
}
function bar(){
// 调用栈为baz->bar
// 当前的调用位置是baz
console.log('bar')
foo() // foo的调用位置
}
function foo(){
// 调用栈为baz->bar->foo
// 当前的调用位置是bar
console.log('foo')
}
baz() // baz的调用位置:全局作用域
- 调用位置
this绑定规则
在理解了上面的调用位置之后,我们来看看this的绑定规则,在找到this所绑定的对象之前,必须先找到函数的调用位置,然后在判断适用于那条绑定规则。默认绑定
在非严格模式下,this的默认绑定指向的是全局对象。如果在严格模式下this将会绑定到undefined。注意⚠️
虽然this的绑定规则完全取决于调用位置,但是只有运行在非严格模式下时,默认你绑定才能绑定到全局对象;在严格模式下调用函数则不会影响默认绑定1
2
3
4
5function foo(){
console.log(this.a)
}
var a = 2
foo() // 2; 在全局作用域下调用foo,所以this采用的是默认绑定,此时this指向的是全局作用域下面我们看看函数运行在严格模式下的情况
1
2
3
4
5
6function foo(){
// foo运行在严格模式下
console.log(this.a)
}
var a = 2
foo() // VM500:3 Uncaught TypeError: Cannot read property 'a' of undefined;此时this绑定的是undefined接下来我们看看函数运行在非严格模式下,但是在严格模式下调用的情况
1
2
3
4
5
6
7
8
9
10function foo(){
// foo运行在非严格模式下
console.log(this.a)
}
var a = 2
(function(){
// foo在严格模式下被调用
foo() // 2 ;虽然在严格模式下调用,但是不影响this的默认绑定为全局作用域
})()隐式绑定
隐式绑定的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。1
2
3
4
5
6
7
8
9function foo() {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
obj.foo() // 2在上面的代码中,我们先声明了一个函数foo,然后声明了一个对象obj,在obj中我们的一个属性foo引用了foo函数,但是无论我们是直接在obj对象中定义foo函数,还是像上面的先定义然后再引用,这个函数严格来说都不属于obj对象。但是函数的调用位置会使用obj上下文来引用函数,因此这时候可以说函数被调用时obj对象
拥有
或者包含
它。
因此,当函数引用有上下文对象时,隐士绑定规则会把函数调用中的this绑定到这个上下文对象。
对象属性链中只有上一层或者说最后一层在调用位置中起作用.1
2
3
4
5
6
7
8
9
10
11
12function foo(){
console.log(this.a)
}
var obj1 = {
a:1,
foo: foo
}
var obj2 = {
a:2,
obj1:obj1
}
obj2.obj1.foo() // 1此时this绑定的是对象属性链中的最后一层也就是obj1-. 隐式丢失
最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。1
2
3
4
5
6
7
8
9
10function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
var bar = obj.foo // obj引用foo,并且将其赋值给bar属性
var a = 'global'
bar() // 'global'在上面的代码中,bar引用的是foo函数本身,所以调用bar函数会将foo中的this采用默认绑定,从而绑定到全局作用域.
再来看看下面的代码1
2
3
4
5
6
7
8
9
10
11
12function foo(){
console.log(this.a)
}
function doFn(fn){
fn() // foo的调用位置
}
var obj = {
a:2,
foo: foo
}
var a = 'gloabl'
doFn(obj.foo) // 'gloabl'在上面的例子中,我们的foo函数被当成函数的参数传递,其实也是传递的函数的引用,所以也会将this绑定到全局作用域中,另外比如在全局作用域中调用setTimeout(fn,1000)函数时,如果fn中存在this的话,那么其也会被绑定到全局作用域中。
显示绑定
js中显示绑定采用的是call和apply方法。call和apply
他们的第一个参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因此可以直接指定this的绑定对象,称之为显示绑定。1
2
3
4
5
6
7function foo(){
console.log(this.a)
}
var obj = {
a: 2
}
foo.call(obj) // 2- 装箱
如果你传入了一个原始值(字符串、布尔类型或者数字类型)来当作this的绑定对象,这个原始值会被转换成它的对象形式。这就叫做装箱。 硬绑定
显示绑定和隐式绑定一样无法解决this绑定丢失的问题,但是我们通过在绑定外部创建一个函数,并在该函数内部强制将this绑定到我们需要的对象上,这个就是硬绑定。1
2
3
4
5
6
7
8
9
10
11
12function foo(){
console.log(this.a)
}
var obj = {
a: 2
}
var bar = function(){
foo.call(obj)
}
bar() // 2
setTimeout(bar, 1000) // 2
bar.call(window) // 2如上所示,我们将foo的this绑定封装在匿名函数中,并将该函数赋值给bar变量,这样无论外部如何传入参数都无法改变this的绑定对象。
- 另外一种创建硬绑定的方式是创建一个可以重复使用的辅助函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// 辅助函数
function bind(fn, obj){
return function(){
return fn.apply(obj, arguments)
}
}
function foo(num){
console.log(this.a, num)
return this.a + num
}
var obj = {
a: 2
}
var bar = bind(foo, obj)
bar(3) // 5
另外ES5提供了内置的硬绑定bind方法,bind方法返回一个硬编码的新函数,它会把你的参数设置为this的上下文并调用原始函数。
- 装箱
new绑定
js的new操作符
在js中的构造函数只是使用new操作符时被调用的函数,他们并不会属于某个类,也不会实例化一个类。实际上它们只是被new操作符调用的普通函数而已。
使用new来调用函数时会发生以下的几个步骤- 创建一个全新的对象
- 这个新对象会被执行[[prototype]]连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
1
2
3
4
5function Foo(a){
this.a = a
}
var foo = new Foo(2)
foo.a // 2
使用new来调用函数时,会构造一个新对象并把它绑定到foo调用中的this上。
通过绑定优先级来判断this
通过优先级来判断this有以下规则:函数是否在new中调用(new绑定)?如果是的话this绑定的是新创建的对象。
1
var bar = new Foo()
函数是否通过call、apply(显示绑定)或者硬绑定来调用?如果是的话,this绑定的是指定的对象。
1
var bar = foo.call(obj)
函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。
1
var bar = obj1.foo()
如果以上都不是的话,则采用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
1
var bar = foo()
凡事都有例外,绑定也是如此;在某些场景下,this的绑定行为会出乎意料
如果把null和undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际上应用的是默认绑定规则。即是全局对象或则会是undefined。
1
2
3
4
5function foo(){
console.log(this.a)
}
var a = 3
foo.call(null) // 3传入null或者undefined的情况:如果一个函数并不关心this的话,你仍然需要传入一个占位符,这时候null或者undefined是一个不错的选择。
间接引用:当创建一个函数的间接引用(函数赋值)的时候,调用函数会应用默认绑定规则
1
2
3
4
5
6
7function foo(){
console.log(this.a)
}
var a = 2
var obj = {a: 3, foo: foo}
var otherObj = {a: 4}
(otherObj.foo = foo.foo)() // 2以上otherObj.foo是指向foo的引用,所以otherObj.foo被调用相当于foo直接在全局作用域中执行。
注意⚠️:
对于默认绑定来说,决定this绑定对象的并不是函数调用位置是否处于严格模式,而是被调用的函数体内是否处于严格模式。如果函数体内处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。- 软绑定
软绑定是相对于硬绑定来说的,硬绑定这种方式可以把this潜质绑定到指定的对象(除了使用new调用函数),是为了防止函数调用应用默认绑定规则。但是这样却导致以后没有办法修改this的绑定对象,所以我们可以采用软绑定来实现修改this的绑定对象。(类似于ES5内置的dind的实现)1
2
3
4
5
6
7
8
9
10
11
12
13if (!Function.prototype.softBind) {
Function.prototype.softBind = function(obj){
var fn = this
// 获取到所有的参数
var curried = [].slice.call(arguments, 1)
var bind = function(){
// 如果this不存在或者this采用了默认绑定的话,那么则强制将this绑定到指定的obj对象中,否则不修改this的绑定.此this和上面的this不一样。
return fn.apply((!this || this === window)? obj : this, curried.concat.apply(curried, arguments))
}
bind.prototype = Object.create(fn.prototype) // 将fn的原型赋值给bind的原型是为了能在bind中调用fn原型上的方法或者属性
return bind
}
}
this词法
以上规则适合于普通的常规函数,但是对于ES6中的箭头函数中的this则不使用。
箭头函数是根据外层(函数或者全局)作用域来决定this的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function foo(){
// 返回一个箭头函数
return (a) => {
// 此时箭头函数内部的this是继承于foo(),因此foo中的this也就是箭头函数的this
console.log(this.a)
}
}
var obj1 = {
a: 2
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1) // 将foo内部的this通过显示绑定到obj1中,所以箭头函数中的this指向的也是obj1
bar.call(obj2) // 2。虽然是通过显示绑定想修改this的绑定,但是箭头函数中的this是根据外层的作用域来决定的,也就是foo的作用域。
this
Last updated: