深入理解JavaScript中的this指向与函数声明

127 阅读4分钟

本文将带你彻底搞懂JavaScript中的函数声明特性和this指向问题,从基础概念到实际应用,让你在面试和开发中游刃有余!

🎯 前言

在JavaScript的世界里,this和函数声明一直是让开发者头疼的问题。你是否遇到过以下困惑:

  • 为什么函数内部不能修改函数名?
  • var声明的变量为什么会挂载到window上?
  • this的指向到底由什么决定?

今天我们就来一次性解决这些问题!

🔍 函数声明的奇特现象

让我们先看一个有趣的例子:

"use strict"; // 启用严格模式
var b = 10;
(function b(){
    b = 20;  // 这行代码不会生效!
    console.log(b); // 非严格模式输出的是函数本身,而不是20  严格模式抛出异常
})()

为什么会这样?

这涉及到JavaScript函数声明的一个重要特性:函数体内的函数名是只读的

  • 返回结果是函数本身
  • 在严格模式下,尝试修改会报错:Assignment to constant variable
  • 函数在其内部作用域中具有更高的优先级
  • 虽然JavaScript是弱类型语言,可以随意修改变量,但函数名在函数体内部是只读的

这个设计保证了函数的完整性和安全性,避免了意外的函数覆盖。

🌍 不同运行环境下的全局对象

JavaScript可以在多种环境中运行,每种环境的全局对象都不同:

浏览器环境

<!DOCTYPE html>
<html>
<body>
    <script>
        "use strict"
        var a = 1;
        console.log(window.a); // 1
        
        function f() {
            console.log('haha');
        }
        console.log(window.f); // function f(){...}
        
        let aa = 1;
        const bb = 2;
        // aa和bb不会挂载到window上
    </script>
</body>
</html>

Node.js环境

var a = 1;
console.log(window.a); // ReferenceError: window is not defined
console.log(global.a);  // 1 - Node.js中的顶层对象是global

ES5 vs ES6的差异

ES5时代:

  • varfunction声明的全局变量会成为顶层对象的属性
  • 全局变量和顶层对象的属性是等价的(这是当初的设计决定)

ES6时代:

  • letconst声明的全局变量不会污染顶层对象
  • 这些变量存在于一个独立的块级作用域(Script作用域)中
  • 避免了对全局对象的污染

🎪 this指向的终极解析

this是JavaScript中最容易混淆的概念之一。让我们通过实例来彻底理解它:

核心概念

this是函数执行时立即生成的对象,由调用方式决定,指向最后的调用者

1. 普通函数调用

var name = "老帅哥";
function func() {
    console.log(this);      // Window对象(非严格模式)
    console.log(this.name); // "老帅哥"
}
func(); // 普通函数调用,this指向window

在严格模式下:

"use strict";
function func() {
    console.log(this); // undefined
}
func();

2. 对象方法调用

var name = "windowsName";
var a = {
    name: '贝利亚',
    fn: function(){
        console.log(this);      // 对象a
        console.log(this.name); // "贝利亚"
    }
}

a.fn(); // 对象方法调用,this指向调用者对象a

// 但是要注意这种情况:
var b = a.fn;
b(); // 普通函数调用,this指向window,输出"windowsName"

3. 构造函数调用

function Person(name, age){
    this.name = name; // this指向新创建的实例
    this.age = age;
}

const p = new Person("labubu", 2);
console.log(p.name); // "labubu"

4. 事件处理函数

// HTML: <input type="text" id="ipt"/>
document.getElementById('ipt').addEventListener('keydown', function(){
    console.log(this);       // input元素
    console.log(this.value); // 输入框的值
});

5. 箭头函数中的this

箭头函数是ES6的亮点,它的this行为完全不同:

var name = "windowsName";
var a = {
    name: "Tom",
    func1: function(){
        console.log(this.name); // "Tom"
    },
    fun2: function(){
        // 普通函数,this指向对象a
        setTimeout(() => {
            // 箭头函数没有自己的this,继承外层作用域的this
            this.func1(); // 调用成功,输出"Tom"
        }, 1000);
    }
}
a.fun2();

箭头函数的特点:

  • 本身没有this
  • this来自作用域链中的上一层
  • 无法通过callapplybind改变this指向

📊 this指向规则总结

调用方式this指向示例
普通函数调用window(非严格)/undefined(严格)func()
对象方法调用调用者对象obj.method()
构造函数调用新创建的实例new Constructor()
事件处理函数事件源元素element.onclick
箭头函数继承外层作用域() => {}

🚀 实战技巧

1. 确保this指向的方法

// 方法1:使用bind
var obj = {
    name: 'test',
    method: function() {
        console.log(this.name);
    }
};
var fn = obj.method.bind(obj);
fn(); // "test"

// 方法2:使用箭头函数
var obj = {
    name: 'test',
    method: function() {
        setTimeout(() => {
            console.log(this.name); // "test"
        }, 100);
    }
};

2. 避免常见陷阱

// 陷阱:方法赋值后调用
var obj = {
    name: 'obj',
    getName: function() {
        return this.name;
    }
};

var getName = obj.getName;
console.log(getName()); // undefined (在严格模式下会报错)

// 解决方案
var getName = obj.getName.bind(obj);
console.log(getName()); // "obj"

🎯 总结

  1. 函数声明:函数名在函数体内是只读的,这是JavaScript的安全机制
  2. 全局变量:ES5的var会污染全局对象,ES6的let/const不会
  3. this指向:由函数的调用方式决定,而不是定义方式
  4. 箭头函数:没有自己的this,继承外层作用域

掌握这些概念,你就能在JavaScript的世界中游刃有余了!记住,理论结合实践才是王道,多写多练才能真正掌握这些知识点。