# this的迷惑行为:JS的"我"到底是谁?
引言:一个常见的困惑
在学习JavaScript的过程中,你是否遇到过这样的困惑:
const bar = {
myName: 'bar',
sayHello: function() {
console.log(myName); // 期望输出'bar',但实际输出undefined
}
};
bar.sayHello();
为什么console.log(myName)拿不到bar中的myName?这个问题看似简单,却触及了JavaScript中this机制的核心,以及JavaScript设计中一个"不好的设计"。今天,让我们深入探讨this的奥秘,从历史背景、作用域链、执行上下文等多个维度来理解它。
一、JavaScript的"历史bug":为什么会有this的困惑?
早期JavaScript的设计背景
在JavaScript诞生之初(1995年),它被设计为一种"轻量级"的脚本语言,用于在浏览器中添加简单的交互功能。当时,JavaScript没有类(class)的概念,也没有面向对象编程(OOP)的完整支持。
为了实现OOP,JavaScript的作者Brendan Eich设计了this机制,让函数可以"绑定"到对象上。然而,这个设计存在一个根本性问题:
"JS做了一个不好的设计,this由函数的调用方式决定的"
为什么this会成为问题?
在JavaScript中,this不是由函数定义时决定的,而是由函数调用时的上下文决定的。这与大多数面向对象语言(如Java、C#)中的this行为完全不同。
在Java中,this总是指向当前对象实例;而在JavaScript中,this可能指向全局对象、当前对象,甚至可能是undefined,这取决于函数是如何被调用的。
这导致了JavaScript中一个常见的"陷阱":函数在不同上下文中被调用时,this的指向会变化,使得代码难以预测。
为什么this是"不必要的"?
JavaScript的函数特别灵活,可以作为普通函数、对象方法、构造函数等被调用。这种灵活性使得this的指向变得复杂。如果JavaScript设计得更"纯粹",可能不需要this,而是通过闭包和作用域链来实现OOP。
但历史已经定型,我们只能理解并适应这个设计。
二、变量查找规则与词法作用域链
2.1 为什么console.log(myName)拿不到bar中的myName?
让我们详细分析一下这个例子:
const bar = {
myName: 'bar',
sayHello: function() {
console.log(myName); // 问题所在
}
};
bar.sayHello();
这里的关键在于,myName是一个自由变量(free variable),它不是在sayHello函数内部定义的,也不是通过this来访问的。
在JavaScript中,变量查找遵循词法作用域链(Lexical Scope):
- 首先在当前函数作用域内查找
- 如果没有找到,向上查找外层作用域
- 一直查找到全局作用域
在这个例子中:
sayHello函数内部没有定义myName- 外层作用域(
bar对象所在的作用域)也没有myName定义 - 因此,
myName被查找为全局变量,但全局作用域中也没有myName,所以输出undefined
2.2 正确的访问方式
要正确访问bar中的myName,应该使用this:
const bar = {
myName: 'bar',
sayHello: function() {
console.log(this.myName); // 使用this,输出'bar'
}
};
bar.sayHello();
这里,this指向调用sayHello的对象,即bar,所以this.myName就是bar.myName。
三、普通函数被调用,this指向全局对象
3.1 普通函数调用的this指向
普通函数运行时,this指向全局对象
让我们验证这一点:
function sayHello() {
console.log(this);
}
sayHello(); // 在浏览器中输出window对象
这里,sayHello作为普通函数被调用,this指向全局对象(在浏览器中是window)。
3.2 为什么是全局对象?
在JavaScript中,当函数被作为普通函数调用(而不是作为对象方法、构造函数或通过call/apply绑定)时,this指向全局对象。
这是JavaScript设计的一个"历史包袱"。如果JavaScript设计得更严谨,普通函数调用时this应该指向undefined或一个空对象,而不是全局对象。
3.3 变量声明的污染问题
var声明的变量,会挂载到全局window对象上不好,容易造成全局变量的污染
让我们看一个例子:
function setVar() {
var myVar = 'hello';
}
setVar();
console.log(myVar); // 输出'hello',但myVar是局部变量
在这个例子中,myVar是setVar函数的局部变量,但console.log(myVar)仍然能访问到它。这是因为var声明的变量会被提升到函数作用域的顶部,但更重要的是,如果在全局作用域中使用var,它会成为全局对象的属性。
var myVar = 'hello';
console.log(window.myVar); // 输出'hello'
这就是为什么var声明的变量会"污染"全局作用域。
而let声明的变量则不会:
function setLet() {
let myLet = 'hello';
}
setLet();
console.log(myLet); // ReferenceError: myLet is not defined
四、this由函数的调用方式决定
this由函数的调用方式决定的
这是理解this的关键。this的指向取决于函数如何被调用,而不是函数在哪里被定义。
4.1 函数的四种调用方式
- 作为普通函数调用:
this指向全局对象(或undefined在严格模式下) - 作为对象方法调用:
this指向调用该方法的对象 - 作为构造函数调用:
this指向新创建的实例对象 - 通过
call/apply/bind调用:this指向指定的对象
4.2 为什么this是"例外"?
在JavaScript中,大多数变量和函数的行为在编译阶段就确定了(词法作用域)。但this是个例外,它的指向在执行阶段(函数调用时)才确定。
这是JavaScript设计中的一个特殊点,也是导致困惑的主要原因。
五、从执行上下文角度看待this
5.1 执行上下文与this
在JavaScript中,执行上下文(Execution Context)是代码执行时的环境。每个函数调用都会创建一个新的执行上下文。
执行上下文包含三个关键部分:
- 变量环境(Variable Environment) :存储变量和函数声明
- 词法环境(Lexical Environment) :存储词法作用域链
- this:指向当前执行上下文的
this值
this不是变量环境的一部分,而是执行上下文的一个属性。
5.2 执行上下文的结构
在第一个例子中,bar.sayHello的执行上下文中的this指向bar对象,而词法环境的outer指向bar对象所在的作用域。
图:执行上下文的结构,展示了变量环境、词法环境和this的关系
5.3 严格模式下的this
2.html中使用了严格模式,这改变了this的行为:
'use strict';
function sayHello() {
console.log(this);
}
sayHello(); // 输出undefined,而不是window
在严格模式下,普通函数调用时this不会指向全局对象,而是undefined。这有助于避免全局变量污染,也是JavaScript设计的一个改进。
六、this指向的各种情况
我们总结一下this指向的几种常见情况:
6.1 作为对象方法被调用
const obj = {
name: 'obj',
sayHello: function() {
console.log(this.name); // this指向obj
}
};
obj.sayHello(); // 输出'obj'
6.2 作为普通函数被调用
function sayHello() {
console.log(this); // 普通函数调用
}
sayHello(); // 浏览器中输出window,严格模式下输出undefined
6.3 作为构造函数被调用
function Person(name) {
this.name = name;
}
const person = new Person('John');
console.log(person.name); // 输出'John'
6.4 使用call、apply绑定this
const obj = { name: 'obj' };
function sayHello() {
console.log(this.name);
}
sayHello.call(obj); // 输出'obj'
sayHello.apply(obj); // 输出'obj'
6.5 DOM事件处理中的this
const btn = document.querySelector('button');
btn.addEventListener('click', function() {
console.log(this); // 指向当前触发事件的DOM元素
});
// 使用普通函数,this指向触发事件的元素
// 使用箭头函数时,this来自事件绑定时的词法作用域
btn.addEventListener('click', () => {
console.log(this); // 指向window,而非按钮元素
});
七、结语:如何正确使用this
通过今天的学习,我们了解到:
this由函数的调用方式决定,而不是定义方式this是执行阶段的指针,不是编译阶段的词法作用域- 普通函数调用时,
this指向全局对象(非严格模式)或undefined(严格模式) - 正确使用
this需要理解函数的调用方式
在实际开发中,为了避免this带来的困惑,我建议:
- 尽量使用箭头函数,特别是在回调函数中,因为箭头函数的
this是词法作用域的 - 如果必须使用普通函数,可以使用
bind、call或apply显式绑定this - 在方法中,如果需要引用当前对象,使用
this而不是直接引用属性
记住,this不是JavaScript的"错误",而是设计中的一个选择。理解了它的机制,我们就能更有效地使用它,而不是被它困扰。
"JS作者偷懒,直接让this指向全局" - 这句话可能有点尖锐,但确实道出了
this机制设计的"历史包袱"。随着ES6的引入,我们有了更多工具来避免this带来的问题,但理解this的工作原理仍然是每个JavaScript开发者的基本功。
希望这篇博客能帮助你深入理解JavaScript中的this机制