JavaScript的this机制很复杂,虽然从一开始从事前端工作就和它打交道,但一直未能弄清楚,道明白。在工作中遇到this相关问题,就知道
var self = this
,一旦去面试遇到各种this相关面试题目时脑子就一片空白,拿不定结果。本文综合了一些书籍和网上文章对this的分析和讲解,提供一些实例来分析各种场景下this是如何指向的。
全局作用域
在浏览器宿主环境中,this
指向window
对象,并且在全局作用域下,使用var
声明变量其实就相当于操作全局this
。
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
操作符上有明显的区别:变量不可以删除,而对象的属性是可以删除的
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
。
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
变量的值,并隐式返回这个新对象作为调用结果。也就是说指向新生成的实例。
function Foo(name) {
this.name = name;
this.getName = function() {
console.log(this.name);
}
}
var a = new Foo('a');
a.getName(); // "a"
使用call和apply方法
可以通过call()
和apply()
方法显示改变函数的this
指向。
var a = 1;
var obj = {
a: 2
}
function foo() {
console.log(this.a);
}
foo(); // 1
foo.call(obj); // 2
foo.apply(obj); // 2
使用bind方法
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
来实现。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。