1. 概述
this是JS中的一个关键字,其指向一直是一个难点问题。时而指向全局,时而指向某对象,容易让人摸不着头脑。本文将对this主要使用场景进行概括,分析其指向的规律。其实this九成使用都在函数之中,少数在全局,当然还有this在DOM中的使用未提及。在非严格模式下,this指向的都是一个对象,这个对象或者指向Window(浏览器环境)或者指向某具体对象,后面就尝试把这个二选一的问题讲清楚。具体分六种情况,全局一种和函数中五种this使用情况。
2. 全局中的this
有些早期的博文和书籍中认为this只能在函数中使用,但实际上我们呢在全局环境下也可以直接写this。this的值指向全局对象,不过这种写法基本不被使用,比较鸡肋。具体表现在浏览器和Node环境下略有不同。
浏览器下:window对象
console.log(this)
Node: global对象
本文其他例子就不描述node环境下的行为了,默认浏览器环境。
3. 函数中的this
函数中使用this是重中之重,本文写作的契机也是在看了《javaScript语言精粹》中函数部分的内容后想进行总结记录。该书中认为,每个函数都有两个隐藏的参数,分别是this和arguments。这两个参数都可以在函数中直接使用,无论该函数是否有形参。而不同的函数使用方法,this这个关键字的指向也各不相同。书中列举了四种方法,我这儿再加上ES6中的箭头函数进行总结。
3.1 函数调用this
所谓函数调用,就是调用时这个函数前面啥也没有,没有点号,没有对象,直接调用。最简单的例子:
function fn(){
console.log(this)
}
fn() // window
函数调用this指向window。
这条就是唯一准则,哪怕像下面的例子一样好像有个对象在前面:
var name = 'window';
let obj = {
name: 'obj',
sayName: function () {
let inner = function () {
console.log(this.name)
}
inner(); // 在这儿执行,前面没对象
}
}
obj.sayName(); // window
3.2 方法调用this
当使用this的函数作为某个对象的成员属性被调用时,称为方法调用,this指向该对象。简单而言就是:
谁调用我,我指向谁。
这一点是js中this使用的关键,后文所讲的许多问题都将体现这一点。
具体而言又有两点细节:
- this指向最近一个调用函数的那个对象。
var o = {prop: 37};
function independent() {
return this.prop;
}
o.f = independent;
console.log(o.f()); // 37,此处this指向o
o.b = {g: independent, prop: 42};
console.log(o.b.g()); // 42,此处的this指向o.b
- 方法调用不论实质只论字面,通过原型链调用也指向字面上那个对象。
var o = {
f: function() {
return this.a + this.b;
}
};
var p = {
a: 3,
b: 2,
__proto__: o
}
console.log(p.f()); // 5
此外,构造函数调用this和call/apply显示绑定this都可以用方法调用来解构,可以说其实质就是方法调用。
3.3 构造函数调用this
构造函数中几乎必定使用到this,其中的this指向新产生的那个对象。
function Box(color) {
this.color = color;
}
let red = new Box("red");
console.log(red);
其实质可等价于以下代码:
function Box(color) {
this.color = color;
}
// 任何一个函数定义时都会同时产生一个原型对象,此处为Box.prototype.
// 这个对象有个属性为constructor,指向Box。
// Box.prototype.constructor === Box
function newObj(fn, color) {
let _res = {
__proto__: fn.prototype, // fn.prototype有个叫constructor的属性
};
_res.constructor(color); // 这里还是一个方法调用,通过原型链调用
return _res;
}
let red = newObj(Box, "red");
console.log(red);
以上也可作为面试时手写“new”的答案。
3.4 call/apply调用this
当某函数作为回调函数使用时,函数作为参数自然是无法绑定任何对象,所以在其执行时this会指向全局对象,这通常并非我们需要的结果。示例如下:
var obj1 = {
a: 2,
foo1: function () {
console.log(this.a)
},
foo2: function () {
setTimeout(function () {
console.log(this)
console.log(this.a)
}, 0)
}
}
var a = 3
obj1.foo1() // 2
obj1.foo2() // window, 3
显式绑定可用以解决回调函数中的this丢失问题,具体示例如下:
var obj1 = {
a: 2,
foo1: function () {
console.log(this.a)
},
foo2: function () {
setTimeout((function () {
console.log(this)
console.log(this.a)
}).bind(this), 0)
}
}
var a = 3
obj1.foo1() // 2
obj1.foo2() // obj1, 2
react中常常需要绑定this,也是这个原因。
class Foo extends Component {
handleClick() {
console.log('Click happened');
}
render() {
return <button onClick={this.handleClick.bind(this)}>Click Me</button>;
}
}
此外,forEach、map、filter函数的第二个参数也能显式绑定this。
function foo (item) {
console.log(item, this.a)
}
var obj = {
a: 'obj'
}
var arr = [1, 2, 3]
arr.forEach(foo, obj) // 显式绑定
使用call/Apply的显示绑定this,也可以使用方法调用来进行简单的模拟。下面就简单模拟一个Apply。
let obj = {
a: 1,
};
function add(b) {
console.log(this.a + b);
}
add.apply(obj, [2]);
// 用myApply简单模拟一个Apply
function myApply(fn, obj, params) {
obj._fn = fn;
obj._fn(...params);
delete obj._fn;
}
myApply(add, obj, [3]);
3.5 箭头函数中的this
很多人说箭头函数没有this,其实不是没有,是没有自己的this,它的this来自于它的环境。
箭头函数this指向跟调用他的方法没有关系,只取决于其被定义时的环境。
所以不管箭头函数是方法调用,还是直接调用,他都跟前面那个对象没有必然关系,一切取决于这个函数被定义时的环境。如下例中,虽是方法调用,其中this仍指向于window。
var obj = {
name: 'obj',
foo1: () => {
console.log(this.name)
}
}
var name = 'window'
obj.foo1() // window
这里我们说的“被定义”时的环境,是指得到这个箭头函数时的环境,如果取得箭头函数时该函数所处环境中this并不指向某对象(仅仅是引用了某个函数),则还是指向于window。具体案例如下:
var name = "window";
var obj = {
name: "obj",
foo2: function () {
return () => {
console.log(this.name);
};
},
};
let fn = obj.foo2;
fn()(); // window
obj.foo2()(); // obj
MDN中有个例子可以很好地说明箭头函数中this的指向。
var obj = {
bar: function() {
var x = (() => this);
return x;
}
};
var fn = obj.bar();
console.log(fn() === obj); // true
// 但是注意,如果你只是引用obj的方法,而没有调用,便不会使this转移到obj上
var fn2 = obj.bar;
console.log(fn2()() == window); // true
用ES5的语法解构上述代码,箭头函数等价于以下代码:
var obj = {
bar: function () {
var x = function () {
return this;
};
return x.bind(this);
// 这个this就是bar中的this,指向obj
},
};
4. this经典面试题及解析
Q1:多层函数,内层函数不绑定外层函数的this
var o = {
f1: function () {
console.log(this);
var f2 = function () {
console.log(this);
}();
}
}
o.f1()
A1:f2并不作为某一个对象的方法调用,又无call/apply等显式的对象绑定,所以其中的this指向全局对象window
o //对象o
Window //全局对象
Q2:
function foo () {
console.log(this.a)
}
function doFoo (fn) {
console.log(this)
fn()
}
var obj = { a: 1, foo }
var a = 2
doFoo(obj.foo)
A2:obj.foo这个函数是通过obj这个对象的某个成员得到,但没有调用,此处仅是得到了foo这个函数的地址,与直接使用foo函数无异。所以不存在方法调用,其中this仍指向全局对象window。
Window
2
A3:箭头函数中的this指向其外部环境的this
var obj = {
name: 'obj',
foo1: () => {
console.log(this.name)
},
foo2: function () {
console.log(this.name)
return () => {
console.log(this.name)
}
}
}
var name = 'window'
obj.foo1()
obj.foo2()()
Q3:这里第一个结果中的this虽然时来自于obj的方法调用,但调用的是箭头函数,所以这个this的指向仍是obj本身所处的环境,即全局对象window。第二个结果常规方法调用,为obj。第三个结果是箭头函数绑定的foo2中的this,指向obj。
window
obj
obj
5.总结
总结一下this使用中的关键点:
- 方法调用,指向调用的对象。
- 箭头函数,指向函数外边的环境。
- 若是函数作为参数,或是重命名赋值,则会失去对于对象的绑定。