共计 2781 个字符,预计需要花费 7 分钟才能阅读完成。
与其他语言相比,js 中的 this 有所不同,也是比较头疼的问题。在参考了一些资料后,今天,就来深入解析一下 this 指向问题,有不对的地方望大家指出。
为什么要用 this
对于前端开发者来说,this 是比较复杂的机制,那么为什么要花大量时间来学习呢,先来看一段代码。如果不使用 this,要给 identify() 和 speak() 显式传入一个对象:
function identify(context) {
return context.name.toUpperCase();
}
function speak(context) {
var greeting = “Hello, I’m” + identify(context);
console.log(greeting);
}
identify(you);
speak(me);
可以看到,speak() 里面直接写了 identify() 的函数名,然而,随着使用模式越来越复杂,显式传递的上下文会让代码变得混乱,尤其体现在面向对象中。显然,this 提供了一种方式来隐式“传递”一个对象的引用,更加简洁,易于复用。
this 的误解
1. this 指向函数本身
记录函数 foo 被调用的次数:
function foo(num) {
console.log(“foo:” + num);
// 记录次数
this.count++;
}
foo.count = 0;
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
foo(i);
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
//foo 被调用了多少次?
console.log(foo.count); // 0
从前两次的 console.log() 可以看出,foo 确实被调用了 4 次,但是 foo.count 仍然为 0,显然 this 指向函数本身的理解是错误的。
2. this 指向函数作用域
要明确的是,this 在任何情况下都不指向函数的词法作用域。因为,作用域“对象”无法通过 JavaScript 代码访问,它存在于 JavaScript 引擎内部。下面的代码试图使用 this 来隐式引用函数的词法作用域,没有成功:
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo(); // ReferenceError: a is not defined
直接报出了访问不到 foo() 中的 a。ReferenceError 和作用域判别失败相关,而 TypeError 代表作用域判别成功,但是对结果的操作是非法的、不合理的。
this 是什么
排除了以上两个误解之后,来看一下 this 到底是什么。this 是运行时绑定的,它和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(执行上下文),这个记录包含函数在哪里被调用,函数的调用方式、传入的参数等,this 就是这个记录的一个属性,在函数执行的过程中用到。即,this 总是指向调用它所在方法的对象
1. 在浏览器中,调用方法没有明确对象时,this 指向 window
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
在全局中声明变量 a = 2,然后在全局中直接调用 foo(),this 指向了全局对象,得到 a 的值。要注意的是,在严格模式(strict mood)下,如果 this 没有被执行环境定义,那它将绑定为 undefined。
function foo() {
“use strict”;
console.log(this.a);
}
var a = 2;
foo(); // TypeError: this is undefined
在严格模式下,调用 foo() 不影响 this 绑定。
function foo() {
console.log(this.a);
}
var a = 2;
(function() {
“use strict”;
foo(); // 2
})();
2. 在浏览器中,setTimeout、setInterval 和匿名函数执行时的当前对象是全局对象 window
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var a = “global”;
setTimeout(obj.foo, 100); // “global”
JavaScript 中的 setTimeout() 的实现和下面伪代码相似:
function setTimeout(fn, delay) {
// 等待 delay 毫秒
fn(); // 调用函数
}
3. apply / call / bind 可以强制改变 this 指向
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
foo.call(obj); // 2
foo.apply(obj); // 2
foo.bind(obj); // 2
call 和 apply 的区别在于第二个参数,call 是把 args 全部列出来,用“,”分隔,而 apply 是一个类数组。call、apply 是硬绑定,通过硬绑定的函数不能再修改它的 this。
function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
var bar = function() {
foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
bar.call(window); // 2
函数 foo() 内部手动调用了 foo.call(obj),把 foo 的 this 强制绑定到了 obj,所以后面即使又把 bar() 绑定到了 window,还是无法改变 this 指向。
4. new 操作符改变 this 指向
在传统的面向对象语言中,会使用 new 初始化类,然而在 JavaScript 中 new 的机制和面向对象语言完全不同。在 js 中,构造函数只是使用 new 操作符时被调用的函数,它们并不属于一个类,也不会实例化一个类。也就是说,js 中,不存在所谓的“构造函数”,只有对函数的“构造调用”。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
使用 new 调用 foo(),会构造一个新对象并把它绑定到 foo() 调用中的 this 上。
优先级
既然有那么多可以改变 this 的指向,那么它们的优先级是怎么样的呢,记住这句话:范围越小,优先级越高。可以按照下面的顺序来判断:
判断函数是否在 new 中调用过:
var bar = new foo();
判断函数是否通过 call、apply、bind 绑定过:
var bar = foo.call(obj);
判断函数是否在某个上下文对象中调用过:
var bar = obj.foo();
如果以上情况均不存在,那么在严格模式下,绑定到 undefined,否则绑定到全局对象:
var bar = foo();