前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JavaScript执行机制:变量提升、作用域链、词法作用域、块级作用域、闭包和this

JavaScript执行机制:变量提升、作用域链、词法作用域、块级作用域、闭包和this

作者头像
陆业聪
发布2024-07-23 18:37:49
780
发布2024-07-23 18:37:49
举报
文章被收录于专栏:大前端修炼手册

所以,JavaScript是ArkTS的基础,本文就来介绍一下JavaScript执行机制的一些核心概念。

在JavaScript中,函数是一等公民,可以像其他数据类型一样进行传递和操作。这使得JavaScript具有强大的表达能力,但同时也带来了一些复杂性。本文将围绕JavaScript中的变量提升、作用域链、词法作用域、块级作用域、闭包和this进行详细介绍。

一、作用域与变量查找

1.1 作用域链

作用域链是JavaScript中变量查找和访问的基本机制。当访问一个变量时,JavaScript引擎会首先在当前作用域内查找这个变量。如果找不到,它会继续在外层作用域查找,直到找到这个变量或者到达全局作用域。这种由内到外的查找顺序形成了作用域链。

作用域链的主要作用是保证变量的正确访问。通过作用域链,JavaScript引擎可以在多层嵌套的作用域中找到正确的变量。以下是一个简单的示例:

代码语言:javascript
复制
var globalVar = "Global variable";

function outerFunc() {
    var outerVar = "Outer variable";
    
    function innerFunc() {
        var innerVar = "Inner variable";
        console.log(innerVar); // Inner variable
        console.log(outerVar); // Outer variable
        console.log(globalVar); // Global variable
    }
    
    innerFunc();
}

outerFunc();

在这个示例中,innerFunc函数可以访问其自身作用域内的innerVar变量、外层outerFunc函数作用域内的outerVar变量和全局作用域内的globalVar变量。这是因为作用域链的查找机制。

1.2 词法作用域

词法作用域是JavaScript中作用域的静态结构。词法作用域是在代码编写时就确定的,与代码的执行无关。换句话说,词法作用域是由函数的嵌套结构决定的,而不是函数的调用方式。

词法作用域使得JavaScript引擎可以在编译阶段就确定变量的查找顺序。这种静态结构有助于提高代码的可读性和可维护性。以下是一个词法作用域的示例:

代码语言:javascript
复制
function foo() {
    var x = 1;

    function bar() {
        console.log(x);
    }

    return bar;
}

var baz = foo();
baz(); // 1

在这个示例中,bar函数在foo函数的作用域内定义,因此它的词法作用域包含了foo函数的作用域。当baz函数被调用时,它可以访问foo函数作用域内的变量x,即使foo函数已经执行完毕。这是因为词法作用域的静态结构。

1.3 块级作用域

块级作用域是指由大括号{}包围的代码块内的作用域。在ES6(ECMAScript 2015)之前,JavaScript只有全局作用域和函数作用域,没有块级作用域。这导致了一些问题,如变量提升、循环变量泄漏等。

ES6引入了letconst关键字,用于声明块级作用域的变量。这使得JavaScript具有了类似于其他编程语言(如C和Java)的块级作用域。以下是一个块级作用域的示例:

代码语言:javascript
复制
function foo() {
    var x = 1;
    let y = 2;

    if (true) {
        var x = 3;  // This will affect the outer 'x'
        let y = 4;  // This will not affect the outer 'y'
        console.log(x); // 3
        console.log(y); // 4
    }

    console.log(x); // 3
    console.log(y); // 2
}

foo();

在这个示例中,var声明的变量x没有块级作用域,因此在if语句块内的赋值会影响到外部的x。而let声明的变量y具有块级作用域,因此在if语句块内的赋值不会影响到外部的y

二、函数与变量特性

2.1 变量提升(Hoisting)

变量提升是JavaScript中的一个特性,它指的是变量和函数声明在编译阶段被提升至其作用域的顶部。这意味着在代码的执行顺序中,你可以在声明变量或函数之前就引用它们。需要注意的是,仅声明会被提升,而初始化(赋值)不会被提升。

2.1.1 变量提升示例

以下是一个变量提升的示例:

代码语言:javascript
复制
console.log(x); // undefined
var x = 5;
console.log(x); // 5

在这个示例中,虽然x的声明和初始化在console.log(x)之后,但由于变量提升,x的声明在编译阶段被提升至作用域顶部。因此,在第一个console.log(x)处,x已经被声明,但尚未初始化,所以输出undefined

需要注意的是,使用letconst声明的变量不会发生变量提升。以下是一个使用let声明的示例:

代码语言:javascript
复制
console.log(y); // ReferenceError: y is not defined
let y = 5;

在这个示例中,由于let声明的变量不会发生提升,因此在y声明之前引用y会导致ReferenceError

2.1.2 函数提升示例

函数声明也会发生提升,以下是一个函数提升的示例:

代码语言:javascript
复制
console.log(foo()); // "Hello, world!"

function foo() {
    return "Hello, world!";
}

在这个示例中,虽然foo函数的声明在console.log(foo())之后,但由于函数提升,foo函数在编译阶段被提升至作用域顶部。因此,在console.log(foo())处,foo函数已经可以被调用,输出"Hello, world!"

需要注意的是,函数表达式(包括箭头函数)不会发生函数提升。以下是一个函数表达式的示例:

代码语言:javascript
复制
console.log(bar()); // TypeError: bar is not a function

var bar = function() {
    return "Hello, world!";
};

在这个示例中,由于bar是一个函数表达式,它不会发生函数提升。因此,在bar被赋值之前调用bar会导致TypeError

2.1.3 变量提升的注意事项

虽然变量提升在某些情况下可以带来便利,但它也可能导致一些问题,如意外覆盖全局变量、引用未初始化的变量等。为了避免这些问题,建议遵循以下最佳实践:

  1. 尽量在作用域顶部声明变量和函数,以避免意外的提升行为。
  2. 使用letconst代替var声明变量,以避免变量提升和其他var带来的问题。
  3. 避免在同一作用域内使用相同的变量名,以防止意外覆盖。

2.2 闭包

闭包是指一个函数可以访问其外部作用域中的变量。在JavaScript中,由于函数是一等公民,因此可以返回一个函数或将函数作为参数传递。这使得函数可以“携带”其外部作用域,并在其他地方使用这些外部作用域的变量。这种特性就是闭包。

闭包是JavaScript中的重要特性,它使得函数具有了“记忆”能力,可以用于实现各种高级特性,如函数柯里化、模块化编程、异步编程等。

2.2.1 函数柯里化

函数柯里化(Currying)是一种处理函数中多个参数的技术,它将一个带有多个参数的函数转换成一系列使用一个参数的函数。

举个例子,假设我们有一个带有两个参数的函数add(a, b),我们可以通过柯里化将其转换为一个新的函数curriedAdd。这个新函数首先接受一个参数,然后返回一个新的函数来接受第二个参数,最后返回两个参数的和。

以下是一个简单的柯里化示例:

代码语言:javascript
复制
function add(a, b) {
    return a + b;
}

function curriedAdd(a) {
    return function(b) {
        return add(a, b);
    };
}

var add5 = curriedAdd(5);
console.log(add5(3)); // 8

在这个示例中,curriedAdd函数接受第一个参数a,并返回一个新的函数。这个新的函数接受第二个参数b,并返回ab的和。这样,我们可以创建一个新的函数add5,它接受一个参数并返回该参数与5的和。

下面这段代码展示了如何实现一个通用的柯里化函数curry,它可以将任何具有多个参数的函数转换成柯里化函数:

代码语言:javascript
复制
// 定义一个通用的柯里化函数
function curry(fn) {
    // 返回一个新的柯里化函数
    return function curried(...args) {
        // 如果传递的参数数量大于等于原函数的参数数量,则直接调用原函数
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        } else {
            // 否则,返回一个新的函数,用于接收剩余的参数
            return function(...args2) {
                // 将新传入的参数与之前传入的参数合并,并递归调用curried函数
                return curried.apply(this, args.concat(args2));
            };
        }
    };
}

// 定义一个带有三个参数的求和函数
function sum(a, b, c) {
    return a + b + c;
}

// 使用curry函数将sum函数转换成柯里化函数
var curriedSum = curry(sum);

// 使用不同的方式调用柯里化函数
console.log(curriedSum(1, 2, 3)); // 6
console.log(curriedSum(1)(2, 3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
console.log(curriedSum(1)(2)(3)); // 6

在这段代码中,curry函数的主要逻辑是检查传入的参数数量。当传入的参数数量足够时,直接调用原函数;否则,返回一个新的函数来接收剩余的参数。这样,我们可以通过柯里化函数curriedSum以不同的方式传入参数,实现相同的功能。

函数柯里化在函数式编程中非常重要,它可以用于创建具有特定参数的新函数,以及实现部分应用(Partial Application)、延迟计算(Lazy Evaluation)等高级技术。

2.2.2 模块化编程
代码语言:javascript
复制
var counterModule = (function() {
    var counter = 0;

    return {
        increment: function() {
            counter++;
        },
        getCounter: function() {
            return counter;
        }
    };
})();

counterModule.increment();
console.log(counterModule.getCounter()); // 1
2.2.3 异步编程
代码语言:javascript
复制
function asyncFunc() {
    var callback;

    setTimeout(function() {
        callback('Hello, world!');
    }, 1000);

    return {
        then: function(cb) {
            callback = cb;
        }
    };
}

asyncFunc().then(console.log); // 'Hello, world!' after 1 second

在这些示例中,闭包被用来“记住”外部作用域的变量,并在后续的调用中使用这些变量。这使得函数具有了“记忆”能力,可以实现函数柯里化、模块化编程和异步编程等高级特性。

2.3 this

this是JavaScript中的一个特殊关键字,它在函数内部表示函数的调用者。this的值在函数调用时确定,与函数的定义无关。这使得this在不同的调用情况下可能有不同的值。

在JavaScript中,this的值主要由函数的调用方式决定。以下是一些常见的调用方式和对应的this值:

以下是这些点的代码示例:

  • 在全局作用域或函数作用域内调用函数,this等于全局对象(在浏览器中是window):
代码语言:javascript
复制
console.log(this); // window

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

foo(); // window
  • 在对象的方法内调用函数,this等于该对象:
代码语言:javascript
复制
var obj = {
    name: 'Alice',
    sayHello: function() {
        console.log(this.name);
    }
};

obj.sayHello(); // Alice
  • 在构造函数内调用函数,this等于新创建的对象:
代码语言:javascript
复制
function Person(name) {
    this.name = name;
    this.sayHello = function() {
        console.log(this.name);
    };
}

var alice = new Person('Alice');
alice.sayHello(); // Alice
  • 使用callapplybind方法调用函数,this等于指定的对象:
代码语言:javascript
复制
function foo() {
    console.log(this.name);
}

var obj1 = { name: 'Alice' };
var obj2 = { name: 'Bob' };

foo.call(obj1); // Alice
foo.apply(obj2); // Bob

var bar = foo.bind(obj1);
bar(); // Alice
  • 需要注意的是,箭头函数没有自己的this值,箭头函数内部的this等于外部作用域的this
代码语言:javascript
复制
var obj = {
    name: 'Alice',
    sayHello: function() {
        console.log(this.name); // Alice
        var innerFunc = () => {
            console.log(this.name); // Alice
        };
        innerFunc();
    }
};

obj.sayHello();

在这个示例中,innerFunc是一个箭头函数,它没有自己的this值。因此,innerFunc内部的this等于外部作用域(sayHello方法)的this,即obj对象。这就是为什么innerFunc内部的console.log(this.name)输出的是Alice,而不是undefinedwindow

三、总结

本文介绍了JavaScript中的作用域链、词法作用域、块级作用域、闭包和this。这些概念是理解和掌握JavaScript的基础,对于编写高效、可维护的JavaScript代码非常重要。希望通过本文的介绍,可以帮助你更好地理解和使用JavaScript。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-06-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陆业聪 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、作用域与变量查找
    • 1.1 作用域链
      • 1.2 词法作用域
        • 1.3 块级作用域
        • 二、函数与变量特性
          • 2.1 变量提升(Hoisting)
            • 2.1.1 变量提升示例
            • 2.1.2 函数提升示例
            • 2.1.3 变量提升的注意事项
          • 2.2 闭包
            • 2.2.1 函数柯里化
            • 2.2.2 模块化编程
            • 2.2.3 异步编程
          • 2.3 this
          • 三、总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档