作用域

68 阅读5分钟

1. 作用域

作用域定义了一个区域,在这个区域内可以访问某些变量和函数。JavaScript 中的作用域有两种类型:

  • 全局作用域(Global Scope) :全局作用域中的变量可以在整个程序中访问。
  • 局部作用域(Local Scope) :局部作用域中的变量只能在特定的代码块或函数内部访问。

2. 作用域链(Scope Chain)

作用域链是用来查找变量的机制。当你访问一个变量时,JavaScript 引擎会沿着作用域链从当前作用域开始查找,直到全局作用域。每个作用域都有一个与外部作用域连接的链条,直到最外层的全局作用域。

let a = 'global';

function outer() {
    let b = 'outer';
    function inner() {
        let c = 'inner';
        console.log(a, b, c);
    }
    inner();
}

outer();  // 输出: global outer inner

inner 函数中,ab 从外部作用域(outer 和全局作用域)中查找。

3. 变量提升(Hoisting)

JavaScript 中的变量声明(使用 var)和函数声明会被提升到当前作用域的顶部,但变量的初始化会留在原来的位置。letconst 声明不会被提升。

console.log(x);  // 输出: undefined
var x = 5;

function test() {
    console.log(x);  // 输出: undefined
    var x = 10;
}
test();

4. 闭包(Closures)

闭包允许函数访问外部函数的变量,即使外部函数已经执行完毕。闭包非常强大,它可以保持对外部函数变量的引用

function outer() {
    let count = 0;

    return function inner() {
        count++;
        console.log(count);
    };
}

const increment = outer();
increment();  // 输出: 1
increment();  // 输出: 2
increment();  // 输出: 3

inner 函数是一个闭包,它记住了 outer 函数的作用域,因此每次调用 increment() 时,count 变量的值会累加。

5. 块级作用域(Block Scope)

JavaScript 中使用 letconst 声明的变量具有块级作用域。也就是说,变量只在 {} 内有效,而 var 声明的变量具有函数作用域。

if (true) {
    let a = 1;  // 块级作用域
    var b = 2;  // 函数作用域
}

console.log(a);  // 会报错,因为 `a` 在块级作用域内
console.log(b);  // 输出 2,因为 `b` 在函数作用域内

6. 箭头函数与作用域

箭头函数与普通函数不同,它的 this静态绑定 的,也就是说,箭头函数的 this 继承自定义时的外部作用域。而普通函数的 this 会根据调用时的上下文来确定。

var name = 'Amaly'
const obj = {
    name: 'Alice',
    greet: function() {
        setTimeout(() => {
            console.log(this.name);  // `this` 会指向 `obj`
        }, 1000);
    },
    next: function() {
        setTimeout(function () {
            console.log(this.name);  // `this` 会指向 `window`
        }, 1000);
    }
};

obj.greet();  // 输出: Alice
obj.next();  // 输出: Amaly

8. letconst 的作用域

letconst 被引入 JavaScript ES6,它们具有块级作用域。在 if 语句、for 循环等块级代码块中使用 letconst 时,它们的作用域仅限于当前块。

for (let i = 0; i < 3; i++) {
    console.log(i);  // 输出: 0, 1, 2
}

console.log(i);  // 会报错,`i` 只在 for 循环块内有效

9. this 和作用域

this 的值由函数调用时的上下文决定,而不是由作用域决定。this 在不同的场景中会有所不同:

  • 全局作用域普通函数调用 中,this 指向全局对象(在浏览器中是 window,在严格模式下是 undefined)。
  • 方法调用 中,this 指向调用方法的对象。
  • 构造函数调用 中,this 指向新创建的实例对象。
  • 箭头函数 中,this 继承自定义时的外部作用域,不会根据调用时的上下文改变。 举例说明
console.log(this);  // 在浏览器中输出: window

function normalFunction() {
    console.log(this);  // 在浏览器中输出: window
}

normalFunction();

const obj = {
    name: 'Alice',
    greet: function() {
        console.log(this.name);  // `this` 指向 obj,输出: Alice
    }
};

obj.greet();
const greetFunc = obj.greet; 
greetFunc(); // 输出: undefined (因为 this 指向了 global/window)
function Person(name) {
    this.name = name;
}

const alice = new Person('Alice');
console.log(alice.name);  // 输出: Alice

const obj = {
    name: 'Alice',
    greet: function() {
        setTimeout(() => {
            console.log(this.name);  // `this` 指向外部 `greet` 方法的 `obj`,输出: Alice
        }, 1000);
    }
};

obj.greet();

10. 总结

  • 作用域 决定了变量和函数的可见性,作用域链通过查找外部作用域来决定变量的访问顺序。
  • 闭包 允许函数记住和访问其定义时的作用域,即使外部函数已经执行完毕。
  • 非箭头函数**this** 的值由函数调用时的上下文决定,与作用域无关。
  • letconst 具有块级作用域,能够避免 var 可能导致的作用域问题。

练习题

  1. 全局作用域与函数作用域
var a = 10;

function outer() {
    var b = 20;
    function inner() {
        var c = 30;
        console.log(a, b, c);
    }
    inner();
}

outer();

inner 函数执行时,console.log(a, b, c) 会输出什么?请解释为什么。

  1. letconst 的块级作用域
if (true) {
    let a = 1;
    const b = 2;
    var c = 3;
}

console.log(a);  // 会报错还是输出什么?
console.log(b);  // 会报错还是输出什么?
console.log(c);  // 输出什么?

问题:解释 ab 为什么会报错,而 c 不会报错?并说明 letconst 的块级作用域的区别。

  1. 闭包和作用域链
function createCounter() {
    let count = 0;
    return function() {
        count++;
        console.log(count);
    };
}

const counter = createCounter();
counter();  // 输出什么?
counter();  // 输出什么?
counter();  // 输出什么?

解释为什么 count 的值会持续增加。counter 函数是如何记住 count 变量的?

  1. this 和作用域
const obj = {
    name: 'Alice',
    greet: function() {
        console.log(this.name);
    }
};

const greetFunc = obj.greet;
greetFunc();

问题obj.greet()greetFunc()this 指向有什么不同?分别输出什么?为什么?

  1. 箭头函数与作用域
const obj = {
    name: 'Alice',
    greet: function() {
        setTimeout(() => {
            console.log(this.name);  // `this` 会指向什么?
        }, 1000);
    }
};

obj.greet();

问题this 的值会指向谁?请解释为什么?

  1. 函数作用域中的变量提升
console.log(x);  // 输出什么?
var x = 5;

function test() {
    console.log(x);  // 输出什么?
    var x = 10;
}

test();

问题:解释变量提升的行为,为什么 xtest 函数中的输出是 undefined

  1. 闭包与定时器
function createFunctions() {
    const result = [];
    for (var i = 0; i < 3; i++) {
        result[i] = function() {
            console.log(i);
        };
    }
    return result;
}

const funcs = createFunctions();
funcs[0]();  // 输出什么?
funcs[1]();  // 输出什么?
funcs[2]();  // 输出什么?

问题:为什么输出的是 3, 3, 3,而不是 0, 1, 2?如何修改代码使其输出 0, 1, 2?

  1. this 在箭头函数中的行为
const obj = {
    name: 'Alice',
    greet: () => {
        console.log(this.name);
    }
};

obj.greet();

问题obj.greet() 输出什么?为什么 this 在箭头函数中指向 windowundefined

  1. 块级作用域与函数作用域
function test() {
    if (true) {
        let a = 'Hello';
        var b = 'World';
    }
    console.log(a); // 输出什么?
    console.log(b); // 输出什么?
}

test();

问题:解释为什么 a 会报错,而 b 不会报错。

  1. this 和构造函数
function Person(name) {
    this.name = name;
}

const alice = new Person('Alice');
console.log(alice.name);  // 输出什么?

问题this 在构造函数中的指向是什么?为什么 alice.name 能正确输出 Alice