当我JavaScript基础还不是很扎实的时候,去面试时最怕被问及的两个知识点,一个是闭包,另外一个就是this。
而恰恰这两个知识点都是非常重要,偏偏又不容易理解的知识。如果你能够把他们掌握好,我可以大胆的说,你的基础知识已经比前端从业者的一半甚至跟多的人扎实了。
因此我们与必要调整一下自己可能略显浮躁的心态,认真的花一点时间把this这个知识点掌握好。
如果你通过百度或者其他方式在网上学习过this,我也建议你能够认真的把这节内容过一遍,因为网上的文章存在不少的误导性,可能会导致你学到的知识并不是那么准确。
前面我们已经知道了,当函数被调用执行时,变量对象会生成,这个时候,this的指向会确定。因此我们首先要牢记一个非常重要的结论,当前函数的this是在函数被调用执行的时候才确定的。如果当前的执行上下文处于函数调用栈的栈顶,那么这个时候变量对象会变成活动对象,同时this的指向确认。
真是因为这个原因,导致一个函数内部的this到底指向谁是非常灵活而不确定的,这也是this难以被真正理解的原因所在。例如下面的例子,同一个函数由于调用的方式不同,它内部的this指向了不同的对象。
var a = 10;
var obj = {
a: 20
}
function fn () {
console.log(this.a);
}
fn(); // 10
fn.call(obj); // 20
通过a值的不同表现,我们可以知道this分别指向了window与obj。
接下来,我们一点一点的来分析this中的具体表现。
全局对象中的this
在之前变量对象的学习中我有提到过,全局对象的变量对象是一个比较特殊的存在。在全局对象中,this指向它的本身。因此这也相对简单,没有那么多复杂的情况需要考虑。
// 通过this绑定到全局对象
this.a2 = 20;
// 通过声明绑定到变量对象,但在全局环境中,变量对象就是它自身
var a1 = 10;
// 仅仅只有赋值操作,标识符会隐式绑定到全局对象
a3 = 30;
// 输出结果会全部符合预期
console.log(a1);
console.log(a2);
console.log(a3);
函数中的this
在上面的例子中,同一个函数中的this由于调用方式的不同导致了this的不同指向,因此,this的最终指向谁,与调用该函数的方式息息相关。
在一个函数的执行上下文中,this由该函数的调用者提供,由调用函数的方式来决定。 下面的例子中展示了谁是调用者。
function fn() {
console.log(this);
}
fn(); // fn为调用者
如果调用者被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果调用者函数独立调用,那么该函数内部的this,则指向undefined。 但是在非严格模式中,当this指向undefined时,它会自动指向全局对象。
// 为了能够准确判断,我们在函数内部使用严格模式,因为非严格模式会自动指向全局
function fn() {
'use strict';
console.log(this);
}
fn(); // fn是调用者,独立调用,this为undefined
window.fn(); // fn是调用者,被window所拥有,this为Window对象
函数是独立调用,还是被某个对象拥有,我想应该还是非常容易辨认的,结合上面的结论,我们来结合一些简单的例子来分析。
// demo01
var a = 20;
var obj = {
a: 40
}
function fn() {
console.log('fn this: ', this);
function foo() {
console.log(this.a);
}
foo();
}
fn.call(obj);
fn();
这个例子中fn最终的调用方式不同,因此在fn的环境中,this会有所变化。但是无论fn如何调用,在fn执行时,foo始终都是独立调用。因此foo内部的this都是指向undefined,但是由于这是非严格模式,因此自动转向Window。所以上面的例子输出结果如下:
fn this: Object { a: 40 }
20
fn this: Window {}
20
// demo02
'use strict';
var a = 20;
function foo () {
var a = 1;
var obj = {
a: 10,
c: this.a + 20
}
return obj.c;
}
console.log(window.foo()); // 20
console.log(foo()); // 报错 TypeError
我们需要知道,对象字面量的写法并不会产生自己的作用域,因此demo02中的,obj.c
上的this属性并不会指向obj,而是与foo函数内部的this是一样的。
因此当使用window.foo()
调用时,foo内部的this指向window对象,这个时候this.a
则能访问到全局的a变量。但是当foo()
独立调用时,foo内部的this指向undefined,由于这个例子是在严格模式中,因此并不会转向window对象,此时执行代码会报错Uncaught TypeError: Cannot read property 'a' of undefined
。
// demo03
var a = 20;
var foo = {
a: 10,
getA: function () {
return this.a;
}
}
console.log(foo.getA()); // 10
var test = foo.getA;
console.log(test()); // 20
这是一个非常容易理解错误的例子。但是只要我们牢牢记住调用者的独立调用与被某个对象所拥有的区别,就不怕任何迷魂阵。
demo03中,foo.getA()
中,getA为调用者,被foo所拥有,因此当getA执行时,this指向foo。因此执行结果返回10。
而test()
执行时,test为调用者,它是独立调用。虽然test与foo.getA
的引用指向同一个函数,但是调用方式不同。因此这个时候,getA内部的this指向了undefined,自动转向window。因此结果返回20。
看了这些例子,相信大家对this的指向基本都有了一定的掌握,那么下面留几个思考题,大家自己来分析分析this的指向。如果你拿不定结果,请通过断点调试的方式在chrome中查看。相信通过之前的文章大家已经掌握了这个基本技能。
// demo04
function foo() {
console.log(this.a)
}
function active(fn) {
fn();
}
var a = 20;
var obj = {
a: 10,
getA: foo,
active: active
}
active(obj.getA); // 输出的值是多少?
obj.active(obj.getA); // 输出的值是多少?
// demo05
var n = 'window';
var object = {
n: 'object',
getN: function() {
return function() {
return this.n;
}
}
}
console.log(object.getN()()); // 输出的结果是多少?
call/apply/bind显示指定this
JavaScript内部提供了一种可以手动设置函数内部this指向的方式,他们就是call/apply/bind。所有的函数都能够调用这三个方法。在最初学习他们的时候可能会有一些理解上的困惑,但是我想通过接下来的分析,你一定能完全掌握他们。
假设有如下一个例子。
var a = 20;
var object = {
a: 40
}
function fn() {
console.log(this.a);
}
如果我们正常调用函数fn
,那么我们很容易想到,fn
为独立调用,因此this最终会指向window,所以函数的输出结果会为20。
fn(); // 20
我们还可以通过如下的方式,当fn
运行时,显示指定函数内部的this。
fn.call(object); // 40
fn.apply(object); // 40
当函数调用call/apply时,则表示会执行该函数,并且函数内部的this指向call/apply的第一个参数。
而call/apply的不同之处在于参数的传递形式。有以下一个例子。
function fn(num1, num2) {
return this.a + num1 + num2;
}
var a = 20;
var object = { a: 40 }
call的第一个参数是我们为函数内部指定的this指向,后续的参数则是函数执行所需要的参数,一个一个的传递。
apply的第一个参数与call相同,为函数内部this指向,而函数的参数,则以数组的形式传递。作为apply的第二个参数。
// 正常执行
fn(10, 10); // 40
// 通过call改变this指向
fn.call(object, 10, 10); // 60
// 通过apply改变this指向
fn.apply(object, [10, 10]); // 60
bind方法也能够指定函数内部的this指向,但是它与call/apply有所不同。
当函数调用call/appy时,函数的内部this被显示指定,并且函数会立即执行。
而当函数调用bind时,函数并不会立即执行,而是返回了一个新的函数,这个新的函数与原函数具有共同的函数体,但它并非原函数,并且新函数的参数与this指向都已经被绑定,参数为bind的后续参数。
通过一个例子来理解。
function fn(num1, num2) {
return this.a + num1 + num2;
}
var a = 20;
var object = { a: 40 }
var _fn = fn.bind(object, 1, 2);
console.log(_fn === fn); // false
_fn(); // 43
_fn(1, 4); // 43 因为参数被绑定,因此重新传入参数是无效的
call/apply/bind的特性,让JavaScript变得十分灵活,他们的应用场景十分广泛,例如将类数组转化为数据,实现继承,实现函数柯里化等。在这里我们先记住他们的基础知识与基本特性,在后续章节中,我们还会遇到this相关的知识,希望那个时候,大家不要感到惊讶与困惑。