JavaScript的this机制很复杂,虽然从一开始从事前端工做就和它打交道,但一直未能弄清楚,道明白。在工做中遇到this相关问题,就知道
var self = this
,一旦去面试遇到各类this相关面试题目时脑子就一片空白,拿不定结果。本文综合了一些书籍和网上文章对this的分析和讲解,提供一些实例来分析各类场景下this是如何指向的。javascript
在浏览器宿主环境中,this
指向window
对象,而且在全局做用域下,使用var
声明变量其实就至关于操做全局this
。html
this === window; // true var foo = 'bar'; this.foo === window.foo; // true
在严格模式下,this
会绑定到undefined
。前端
var a = 2; function foo() { 'use strict'; console.log(this.a); } foo(); // TypeError: this is not undefined
若是在变量的声明过程没有使用let
或者var
,会隐式建立一个全局变量,但这个变量和普通全局变量的区别在于它是做为window
的一个属性建立的。两者在使用delete
操做符上有明显的区别:变量不能够删除,而对象的属性是能够删除的java
var a = 2; b = 3; a; // 2 b; // 3 delete a; delete b; a; // 2 b; // Uncaught ReferenceError: b is not defined
这里的做用域主要是指在对象、函数中的this
指向。面试
做为函数调用时,函数中的this
默认指向window
。浏览器
var a = 1; function foo() { console.log(this.a); } foo(); // 1
若是在当即执行函数中使用了this
,它一样指向window
。app
var a = 1; (function() { var a = 2; console.log(this.a); })(); // 1
做为方法调用时,函数中的this
老是指向方法所在的对象。函数
var obj = { a: 1, foo: function() { console.log(this.a); } } obj.foo();
构造函数调用将一个全新的对象做为this
变量的值,并隐式返回这个新对象做为调用结果。也就是说指向新生成的实例。this
function Foo(name) { this.name = name; this.getName = function() { console.log(this.name); } } var a = new Foo('a'); a.getName(); // "a"
能够经过call()
和apply()
方法显示改变函数的this
指向。prototype
var a = 1; var obj = { a: 2 } function foo() { console.log(this.a); } foo(); // 1 foo.call(obj); // 2 foo.apply(obj); // 2
bind()方法建立一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供以前提供一个给定的参数序列,而后返回由指定的this值和初始化参数改造的原函数拷贝。
var a = 1; var obj = { a: 2 } function foo() { console.log(this.a); } var bar = foo.bind(obj); bar();
ES6引入了箭函数的概念,在箭头函数中因为没有this
绑定,因此它的默认指向是由外围最近一层非箭头函数决定的。
var a = 1; function Foo(a) { this.a = a; this.getA = function() { var x = () => { this.a = 3; // 改变了外围函数Foo属性a的值 console.log(this.a); // 3 } x(); console.log(this.a); // 3 } } var foo = new Foo(1); foo.getA();
上面列举了在正常状况下this
的指向结果。可是在实际开发过程当中,对于不一样场景,不一样的声明方式、调用方式、赋值和传值方式都会影响到this
的具体指向。
函数的调用方式最多见的是方法调用、构造函数调用,或者使用apply/bind/call
调用,也能够是当即执行函数。
var a = 10; var obj = { a: 20, fn: function() { var a = 30; console.log(this.a); } } obj.fn(); // 20 obj.fn.call(); // 10 (obj.fn)(); // 20 (obj.fn, obj.fn)(); // 10 (obj.fn = obj.fn)(); // 10 new obj.fn(); // undefined
对于apply
和call
第一个参数若是不传或者传递undefined
和null
则默认绑定到全局对象,因此obj.fn.call()
的调用实际上把this
指向了window
对象。
对于(obj.fn)()
,咋一看,是当即执行函数,那么它的this
确定指向了window
对象,其实否则,这里obj.fn
只是一个obj
对象方法的引用,并无改变this
的指向。
对于(obj.fn, obj.fn)()
,这种操做比较少见,工做中也不会去这样写。这里首先咱们须要了解逗号操做符会对每一个操做数求值,并返回最后一个操做数的值,其次是这里使用了逗号操做符,里面必然是一个表达式,这种状况下里面的函数this
指向其实已经改变了,指向了全局。对于(obj.fn = obj.fn)()
中this
一样指向全局。所以能够大胆猜想:若是(x)();中x
是一个表达式,而且返回一个函数(引用),那么函数x
中的this
指向全局window
。这里还更多的方式来达到一样目的,好比:(true && obj.fn)()
或者 (false || obj.fn)()
。总的来讲,咱们经过这种方式建立了一个函数的“间接引用”,从而致使函数绑定规则的改变。
对于new obj.fn()
的结果其实也没有什么好说的,函数使用new
操做符调用后返回一个新的实例对象,因为该对象并无一个叫a
的属性,因此返回undefined
。
不少时候,函数的定义在一个地方,而对象定义方法时只是引用了该函数。一样在调用对象方法时,先把它赋值给一个变量(别名),而后使用函数别名进行调用。使用时有可能致使this
绑定的改变。
var a = 10; function foo() { console.log(this.a); } var obj = { a: 20, foo: foo } var bar = obj.foo; // 函数别名 bar(); // 10
虽然bar
是obj.foo
的一个引用,可是实际上,它引用的是foo
函数自己,所以应用了函数的默认绑定规则。
var a = 10; function foo() { console.log(this.a); } function doFoo(cb) { cb(); // cb 实际上引用的仍是foo } var obj = { a: 20, foo: foo } doFoo(obj.foo); // 10 setTimeout(obj.foo, 100); // 10
这里咱们将obj.foo
以参数的形式传递给函数doFoo
和内置函数setTimeout
。参数传递实际上就是一种赋值,和示例一的结果是同样的。所以,调用回调函数的函数会丢失this的指向。
构造函数使用new
操做符调用后会返回一个新的实例对象,可是在定义构造函数时,能够在函数中返回任何值来覆盖默认该返回的实例,这样一来极可能致使实例this
的指向改变。
var a = 10; function f() { this.a = 20; function c() { console.log(this.a); } return c(); } new f(); // 10
这里咱们将构造函数foo
的默认返回值改为返回一个函数c
执行后的结果。当调用new f()
后,内部函数c
中的this
实际上指向的是全局。可是若是咱们将return c()
改为return new c()
的话,那么new foo()
执行的结果是返回一个构造函数c
的实例,因为实例对象中并无属性a
,所以结果为undefined
。
在方法的调用中由调用表达式自身来肯定this
变量的绑定。绑定的this
变量的对象被称为调用接收者。
var buffer = { entries: [], add: function(s) { this.entries.push(s); }, concat: function() { return this.entries.join(''); } } var source = ['123', '-', '456']; source.forEach(buffer.add); // Uncaught TypeError: Cannot read property 'push' of undefined
因为方法buffer.add()
的接收者不是buffer
自己,而是forEach
方法。事实上,forEach
方法的实现使用全局对象做为默认的接收者。因为全局没有entries
属性,所以会抛出一个错误。
要解决上面的问题,一个是使用forEach
方法提供的可选参数做为函数的接收者。
source.forEach(buffer.add, buffer);
其次是使用bind
方法来指定接收者
source.forEach(buffer.add.bind(buffer));
这里想要说明的是,在一个对象的实例中,this
便可以访问实例对象的值,也能够获取原型上的值。
function Foo() {} Foo.prototype.name = 'bar'; Foo.prototype.logName = function() { console.log(this.name); } Foo.prototype.setName = function(name) { this.name = name; } Foo.prototype.deleteName = function() { delete this.name; } var foo = new Foo(); foo.setName('foo'); foo.logName(); // "foo" foo.deleteName(); foo.logName(); // "bar" delete foo.name; foo.logName(); // "bar"
当执行foo.setName('foo')
后,给实例对象foo
增长了一个属性name
,同时覆盖了原型中的同名属性。当执行foo.deleteName()
时,其实是将新增值删除了,还原了初始状态。执行delete foo.name
时,试图删除的仍是新增的属性,可是如今已经不存在这个值了。若是须要删除原始值,能够经过delete foo.__proto__.name
来实现。