This在一般的面向对象语言中,它代表了当前对象的一个引用,而在js中却经常让人觉得混乱,它不是固定不变的,而是随着它的执行环境的改变而改变。
this对象是运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。——《Javascript高级程序设计》
在Javascript中this总是指向调用它所在方法的对象。因为this是在函数运行时,自动生成的一个内部对象,只能在函数内部使用
this的设计目的就是在函数体内部,指代函数当前的运行环境。
本文主要从两方面解释this的原理
- 通过案例分析Javascript的内存情况,解释this的作用机理
- 总结this的使用场景及注意事项
this的作用机理
案例分析
学懂 JavaScript 语言,一个标志就是理解下面两种写法,可能有不一样的结果。
1 | var obj = { |
上面代码中,虽然obj.foo
和foo
指向同一个函数,但是执行结果可能不一样。请看下面的例子。
1 | var obj = { |
首先来看一般教科书的解释,this
指的是函数运行时所在的环境。对于obj.foo()
来说,foo
运行在obj
环境,所以this
指向obj
;对于foo()
来说,foo
运行在全局环境,所以this
指向全局环境。所以,两者的运行结果不一样。
我们今天探究的目的是了解函数的运行环境到底是怎么决定的?举例来说,为什么obj.foo()
就是在obj
环境执行,而一旦var foo = obj.foo
,foo()
就变成在全局环境执行?
内存的数据结构
1 | var obj = { foo: 5 }; |
上面的代码将一个对象赋值给变量obj
。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 }
,然后把这个对象的内存地址赋值给变量obj
。
也就是说,变量obj
是一个地址(reference)。后面如果要读取obj.foo
,引擎先从obj
拿到内存地址,然后再从该地址读出原始的对象,返回它的foo
属性。
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo
属性,实际上是以下面的形式保存的。
1 | { |
注意,foo
属性的值保存在属性描述对象的value
属性里面。
函数
这样的结构是很清晰的,问题在于属性的值可能是一个函数。
1
2 > var obj = { foo: function () {} };
>
这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo
属性的value
属性。
1 | { |
由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。
1 | var f = function () {}; |
回到本文开头提出的问题,obj.foo()
是通过obj
找到foo
,所以就是在obj
环境执行。一旦var foo = obj.foo
,变量foo
就直接指向函数本身,所以foo()
就变成在全局环境执行。
this的使用场景及经验总结
this的使用场景包括以下两方面
- 普通方法的调用
- 特殊使用场景
普通方法的调用方式一般有三种:
- 直接调用
- 方法调用
- new调用
全局作用域或者普通函数中this指向全局对象window。
1 | //直接打印 |
方法调用中谁调用this指向谁
1 | //对象方法调用 |
构造函数或者构造函数原型对象中this指向构造函数的实例
1 | //不使用new指向window |
特殊的调用方式:
- 闭包
- ES6中的箭头函数
- call() 、 apply()、bind()等函数调用
闭包调用及回调函数
1 | var obj = { |
其实,setTimeout 也只是一个函数而已,函数必然有可能需要参数,我们把 this.foo 当作一个参数传给 setTimeout 这个函数,就像它需要一个 fun 参数,在传入参数的时候,其实做了个这样的操作 fun = this.foo,看到没有,这里我们直接把 fun 指向 this.foo 的引用;执行的时候其实是执行了 fun() 所以已经和 obj 无关了,它是被当作普通函数直接调用的,因此 this 指向全局对象。
这个问题是很多异步回调函数中普遍会碰到的;
为了解决这个问题,我们可以利用 闭包 的特性来处理:
1 | var obj = { |
可以看到直接用 this 仍然是 Window;因为 foo2 中的 this 是指向 obj,我们可以先用一个变量 _this 来储存,然后在回调函数中使用 _this,就可以指向当前的这个对象了;
setTimeout 的另一个坑
之前啊说过,如果直接执行回调函数而没有绑定作用域,那么它的 this 是指向全局对象(window),在严格模式下会指向 undefined,然而在 setTimeout 中的回调函数在严格模式下却表现出不同:
1 | 'use strict'; |
按理说我们加了严格模式,foo 调用也没有指定 this,应该是出来 undefined,但是这里仍然出现了全局对象,难道是严格模式失效了吗?
并不,即使在严格模式下,setTimeout 方法在调用传入函数的时候,如果这个函数没有指定了的 this,那么它会做一个隐式的操作—-自动地注入全局上下文,等同于调用 foo.apply(window) 而非 foo();
当然,如果我们在传入函数的时候已经指定 this,那么就不会被注入全局对象,比如: setTimeout(foo.bind(obj), 1);;
http://stackoverflow.com/questions/21957030/why-is-window-still-defined-in-this-strict-mode-code
箭头函数
在 ES6 的新规范中,加入了箭头函数,它和普通函数最不一样的一点就是 this 的指向了,还记得在上文中(作为对象的方法调用-一些坑-解决)我们使用闭包来解决 this 的指向问题吗,如果用上了箭头函数就可以更完美的解决了:
1 | var obj = { |
可以看到,在 setTimeout 执行的函数中,本应该打印出在 Window,但是在这里 this 却指向了 obj,原因就在于,给 setTimeout 传入的函数(参数)是一个箭头函数:
函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
根据例子我们理解一下这句话:
在 obj.foo2() 执行的时候,当前的 this 指向 obj;在执行 setTimeout 时候,我们先是定义了一个匿名的箭头函数,关键点就在这,箭头函数内的 this 执行定义时所在的对象,就是指向定义这个箭头函数时作用域内的 this,也就是 obj.foo2 中的 this,即 obj;所以在执行箭头函数的时候,它的 this -> obj.foo2 中的 this -> obj;
简单来说, 箭头函数中的 this 只和定义它时候的作用域的 this 有关,而与在哪里以及如何调用它无关,同时它的 this 指向是不可改变的。
call, apply, bind
在 js 中,函数也是对象,同样也有一些方法,这里我们介绍三个方法,他们可以更改函数中的 this 指向:
call
1
fun.call(thisArg[, arg1[, arg2[, ...]]])
它会立即执行函数,第一个参数是指定执行函数中 this 的上下文,后面的参数是执行函数需要传入的参数;
apply
1
fun.apply(thisArg[, [arg1, arg2, ...]])
它会立即执行函数,第一个参数是指定执行函数中 this 的上下文,第二个参数是一个数组,是传给执行函数的参数(与 call 的区别);
bind
1
var foo = fun.bind(thisArg[, arg1[, arg2[, ...]]]);
它不会执行函数,而是返回一个新的函数,这个新的函数被指定了 this 的上下文,后面的参数是执行函数需要传入的参数;
这三个函数其实大同小异,总的目的就是去指定一个函数的上下文(this),我们以 call 函数为例;
为一个普通函数指定 this
1 | var obj = { |
1 | var obj = { |