JavaScript 中 this 的行为与作用域链的深度解析

43 阅读6分钟

JavaScript 中 this 的行为与作用域链的深度解析

在 JavaScript 的世界中,this 关键字一直是一个让人又爱又恨的存在。它灵活多变,却又常常令人困惑。尤其是在函数被赋值、传递或作为回调使用时,this 的指向可能与我们的直觉大相径庭。本文将结合一段典型代码,深入剖析 this 的工作机制,并澄清它与**词法作用域(Lexical Scope)执行上下文(Execution Context)**之间的关系。


示例代码回顾

<script>
'use strict';

var bar = {
    myName: "time.geekbang.com",
    printName: function() {
        console.log(myName);         // 自由变量查找
        console.log(bar.myName);     // 显式访问对象属性
        console.log(this.myName);    // this 绑定问题
    }
};

function foo() {
    let myName = '极客时间';
    return bar.printName;
}

let myName = '极客邦';
let _printName = foo();
_printName();  // 普通函数调用
</script>

这段代码看似简单,却蕴含了 JavaScript 中几个核心概念:自由变量、作用域链、this 绑定规则、严格模式的影响


一、变量查找:词法作用域 vs 自由变量

1. 什么是自由变量?

printName 函数内部:

console.log(myName);

这里的 myName 并不是该函数的参数,也不是用 var/let/const 在函数内部声明的局部变量,因此它是一个自由变量(free variable)

JavaScript 引擎会沿着词法作用域链(Lexical Scope Chain)向上查找这个变量。由于 printName 是在全局作用域中定义的(尽管它是 bar 对象的一个方法),它的外层作用域就是全局作用域。

关键点:函数的作用域由声明位置决定,而不是调用位置。

所以:

  • console.log(myName) 会输出全局的 myName,即 '极客邦'
  • 即使 foo() 内部有 let myName = '极客时间',也不会影响 printName,因为 printName 并不是在 foo 内部定义的!

二、this 的绑定机制:动态绑定与执行上下文

1. this 是执行上下文的一部分

在 JavaScript 中,每当一个函数被调用,都会创建一个新的执行上下文(Execution Context) 。执行上下文包含四个核心组成部分:

  • 变量环境(Variable Environment)
  • 词法环境(Lexical Environment)
  • outer(外部作用域引用)
  • this 值

lQLPJx3K3uaR2wvNAmfNBHawXclrYyN-NxIJCq90RQlXAA_1142_615.png

🧠 重点强调this 不是变量,而是执行上下文的一个属性,其值在函数调用时确定,属于“动态绑定”范畴。


三、this 指向的五种常见情况

✅ 1. 作为对象的方法被调用 → this 指向该对象

当函数是某个对象的属性,并通过对象调用时,this 指向该对象。

var obj = {
    name: "Alice",
    sayHello: function() {
        console.log(this.name); // 输出 "Alice"
    }
};

obj.sayHello(); // 此时 this === obj

💡 原理:调用表达式 obj.sayHello(),JS 引擎将 obj 作为 this 绑定到函数执行上下文中。


✅ 2. 作为普通函数被调用 → this 指向全局对象(非严格模式)或 undefined(严格模式)

如果函数没有通过对象调用,而是直接执行,this 的行为取决于是否开启严格模式。

function greet() {
    console.log(this);
}

greet(); // 非严格模式:window(浏览器)或 global(Node.js)
       // 严格模式:undefined

⚠️ 注意:即使函数是对象的方法,但若被提取后调用(如本例中的 _printName()),也会失去原始绑定。


✅ 3. 使用 call() / apply() 显式绑定 this

这两个方法允许你手动指定 this 的值。

var obj1 = { name: "Bob" };
var obj2 = { name: "Charlie" };

function sayName() {
    console.log(this.name);
}

sayName.call(obj1); // 输出 "Bob"
sayName.apply(obj2); // 输出 "Charlie"
  • call() 接受参数列表:func.call(context, arg1, arg2)
  • apply() 接受参数数组:func.apply(context, [arg1, arg2])

✅ 这是解决 this 指向问题的经典方式,尤其适用于回调函数。


✅ 4. 构造函数调用 → this 指向新创建的实例

当函数用 new 关键字调用时,this 指向新创建的对象实例。

function Person(name) {
    this.name = name; // this 指向新实例
}

var p = new Person("David");
console.log(p.name); // "David"

🔁 流程:

  1. 创建一个空对象 {}
  2. 将该对象的原型指向构造函数的 prototype
  3. 执行构造函数,this 指向该对象;
  4. 返回该对象(除非显式返回其他对象)。

✅ 5. 事件处理函数中 → this 指向事件绑定的元素

在 DOM 事件中,this 默认指向触发事件的元素。

<button id="btn">点击我</button>

<script>
document.getElementById('btn').onclick = function() {
    console.log(this); // 指向 <button> 元素
};
</script>

📌 说明:这是浏览器环境下的特殊行为,底层是通过 addEventListener 或直接赋值实现的,this 被隐式绑定到目标元素。


四、this 的绑定优先级(从高到低)

优先级绑定方式示例
1new 构造调用new MyConstructor()
2call() / apply()func.call(obj)
3对象方法调用obj.method()
4普通函数调用(严格模式)func()undefined
5普通函数调用(非严格模式)func()global/window

记忆口诀new > call/apply > 对象方法 > 普通调用


五、this 是执行上下文的属性,而非变量

再次强调:this 不是一个变量,它是执行上下文的一个属性,在函数执行前就确定了。

function testThis() {
    console.log(this);
}

// 无论怎么调用,this 的值都是在调用时才确定
testThis();           // undefined (strict)
(new testThis)();     // testThis 实例
testThis.call({});    // {}

🔄 每次函数调用都会创建新的执行上下文,其中包含独立的 this 值。


六、避免 this 陷阱的最佳实践

  1. 使用箭头函数:箭头函数没有自己的 this,继承外层作用域的 this

    var obj = {
        name: "Eve",
        method: () => {
            console.log(this.name); // this 指向全局,不是 obj
        }
    };
    
  2. 使用 .bind() 预绑定 this

    var obj = { name: "Frank" };
    var func = function() { console.log(this.name); }.bind(obj);
    func(); // 输出 "Frank"
    
  3. 避免在循环中使用 this 未绑定的函数

    var buttons = document.querySelectorAll('button');
    buttons.forEach(function(btn) {
        btn.onclick = function() {
            console.log(this); // 可能不是预期的 btn
        };
    });
    

    改进方式:

    buttons.forEach(function(btn) {
        btn.onclick = function() {
            console.log(btn); // 使用闭包保存 btn
        }.bind(btn); // 或者 bind
    });
    

七、总结:this 的本质与设计哲学

类别特性
变量查找词法作用域决定,静态绑定
this 绑定动态绑定,由调用方式决定
执行上下文包含 this、变量环境、词法环境等
最佳实践使用 bind、箭头函数、new 控制 this

💬 总结一句话:
this 是执行上下文的一部分,它的值由调用方式决定,而非函数定义位置。


结语

JavaScript 的 this 和作用域机制看似混乱,实则有其内在逻辑。理解“词法作用域管变量,调用方式管 this”这一原则,就能在复杂场景中游刃有余。正如那句老话所说:“知道坑在哪,才能绕着走。

希望本文能帮你彻底理清 this 与作用域的关系。欢迎在评论区分享你的踩坑经历!


lQLPJx3K3uaR2wvNAmfNBHawXclrYyN-NxIJCq90RQlXAA_1142_615.png

  • 变量环境:存储变量和函数声明(如 var, function
  • 词法环境:存储块级作用域变量(如 let, const
  • outer:指向外部词法环境,形成作用域链
  • this:当前执行上下文的 this 值,由调用方式决定

🌟 理解执行上下文,是掌握 JS 执行机制的关键一步。