JavaScript技术原理

60 阅读20分钟

原型、原型链

在 JavaScript 中,每一个对象都有一个特殊的属性 __proto__,这个属性指向该对象的原型。原型本身也是一个对象,它可能也有自己的原型,这样就形成了一条链。

当访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript 会沿着原型链查找,直到找到该属性为止。如果整个原型链中都没有找到该属性,则返回 undefined

闭包

闭包的基本定义

在 JavaScript 中,闭包是指一个函数可以访问并操作它创建时所在作用域内的变量,即使该函数在外部作用域中被调用。换句话说,闭包是由函数及其相关的引用环境组合而成的一个实体。

闭包的工作原理

当一个函数在一个作用域中定义时,它会记住创建时所处的作用域中的所有变量。即使该函数在另一个作用域中被调用,它仍然可以访问那些变量。这是因为 JavaScript 的执行环境会保留对这些变量的引用,直到没有引用指向它们为止。

闭包的优点

  1. 封装性:闭包可以用来封装变量,使其不被外部作用域访问,从而保护数据不被外部修改。
  2. 持久性:闭包可以让变量保持在内存中,直到没有函数引用它们为止,这样可以在多次调用之间保持状态。
  3. 模块化:通过闭包可以实现模块化的设计模式,将一组相关的函数和数据封装在一起。

闭包的注意事项

  1. 内存泄漏:如果闭包中引用了大量数据,且长时间未被垃圾回收器清理,可能会导致内存占用过高。因此,在使用闭包时需要注意释放不再使用的变量。
  2. 性能影响:由于闭包使得函数可以访问外部作用域中的变量,每次调用闭包函数时都需要查找作用域链,这可能会影响性能。

实际应用

闭包在实际开发中有广泛的应用,例如:

  • 模块模式:使用闭包来创建私有的变量和函数,只暴露必要的接口。
  • 事件处理程序:在事件处理中,闭包可以记住事件发生时的状态。
  • 迭代器和生成器:使用闭包来创建迭代器和生成器,保持内部状态。

作用域链

作用域链(Scope Chain)是 JavaScript 中一个重要的概念,它决定了变量的可访问性和查找机制。理解作用域链对于正确地管理变量的作用域至关重要。

作用域链的基本概念

作用域链是一个指针链表,它连接了当前执行上下文(Execution Context)的变量环境(Variable Environment)与其所有父执行上下文的变量环境。当一个函数执行时,JavaScript 引擎会创建一个执行上下文,并将其加入到当前的作用域链中。

作用域链的组成

作用域链通常由以下部分组成:

  1. 当前执行上下文的变量环境:包含当前执行上下文中的局部变量和参数。
  2. 父执行上下文的变量环境:如果是函数调用,则包含上一层函数的变量环境。
  3. 全局执行上下文的变量环境:始终位于作用域链的末尾,包含全局变量。

查找过程

当尝试访问一个变量时,JavaScript 引擎会按照以下步骤进行查找:

  1. 首先在当前执行上下文的变量环境中查找。
  2. 如果没有找到,则沿着作用域链向上查找。
  3. 一直查找到全局执行上下文的变量环境。
  4. 如果在整个作用域链中都没有找到,则返回 undefined

示例

让我们通过一个具体的例子来说明作用域链的工作原理:

function outer() {
    var outerVar = "I'm in outer";

    function inner() {
        console.log(outerVar); // 访问外部作用域的变量
    }

    inner();
}

outer(); // 输出: I'm in outer

在这个例子中,当 outer 函数被调用时,会创建一个 outer 函数的执行上下文,并将其加入到作用域链中。当 inner 函数被调用时,也会创建一个 inner 函数的执行上下文,并将其加入到作用域链中。此时的作用域链如下所示:

current (inner) -> parent (outer) -> global

inner 函数尝试访问 outerVar 时,它首先在自己的变量环境中查找,没有找到后,继续沿着作用域链向上查找。最终在 outer 函数的变量环境中找到了 outerVar,并输出了正确的结果。

更复杂的例子

让我们再看一个更复杂的例子,展示多层嵌套的作用域:

function outermost() {
    var outermostVar = "I'm in outermost";

    function outer() {
        var outerVar = "I'm in outer";
        
        function inner() {
            console.log(outermostVar); // 访问最外层作用域的变量
            console.log(outerVar);     // 访问中间层作用域的变量
        }
        
        inner();
    }

    outer(); // 输出: I'm in outermost 和 I'm in outer
}

outermost();

在这个例子中,当 outermost 函数被调用时,创建了 outermost 的执行上下文。当 outer 函数被调用时,创建了 outer 的执行上下文。最后,当 inner 函数被调用时,创建了 inner 的执行上下文。此时的作用域链如下所示:

current (inner) -> parent (outer) -> grandparent (outermost) -> global

inner 函数尝试访问 outermostVar 时,它沿着作用域链查找,最终在 outermost 函数的变量环境中找到了 outermostVar。同样,当 inner 函数访问 outerVar 时,它在 outer 函数的变量环境中找到了 outerVar

词法作用域、动态作用域

词法作用域(Lexical Scope)是 JavaScript 中一个基本的概念,它决定了变量在何处可以被访问。词法作用域是一种静态作用域,意味着变量的作用域是在编写代码时就已经确定的,而不是在运行时动态决定的。

词法作用域的基本概念

词法作用域指的是变量的可访问范围(即作用域)是由变量声明的位置决定的。具体来说,函数内部可以访问其外部函数声明的变量,但外部函数不能访问内部函数声明的变量。这是因为函数在定义时就已经确定了它可以访问哪些变量。

词法作用域 vs 动态作用域

与词法作用域相对的是动态作用域(Dynamic Scope),在动态作用域中,变量的作用域是在运行时根据调用栈来决定的。而在 JavaScript 中,使用的是词法作用域。

示例

下面通过几个例子来说明词法作用域的工作原理:

示例 1:基本的词法作用域
function outer() {
    var outerVar = "I'm in outer";

    function inner() {
        console.log(outerVar); // 输出: I'm in outer
    }

    inner();
}

outer();

在这个例子中,inner 函数可以访问 outer 函数作用域中的变量 outerVar,因为 inner 函数是在 outer 函数内部定义的。

示例 2:嵌套函数
function outermost() {
    var outermostVar = "I'm in outermost";

    function outer() {
        var outerVar = "I'm in outer";

        function inner() {
            console.log(outermostVar); // 输出: I'm in outermost
            console.log(outerVar);     // 输出: I'm in outer
        }

        inner();
    }

    outer();
}

outermost();

在这个例子中,inner 函数可以访问 outer 函数作用域中的变量 outerVar,同时也可以访问 outermost 函数作用域中的变量 outermostVar。这是因为 inner 函数的作用域链包括了 outer 函数的作用域和 outermost 函数的作用域。

示例 3:函数声明与调用分离
function outer() {
    var outerVar = "I'm in outer";

    var inner = function() {
        console.log(outerVar); // 输出: I'm in outer
    };

    inner();
}

outer();

在这个例子中,尽管 inner 函数是在 outer 函数内部定义的,但它仍然可以访问 outer 函数作用域中的变量 outerVar,因为它的作用域是在定义时确定的。

闭包与词法作用域

闭包(Closure)是词法作用域的一种常见应用。闭包是指一个函数可以访问其创建时所在的词法作用域中的变量。即使这个函数在其他地方被调用,它仍然可以访问原始作用域中的变量。

示例 4:闭包
function outer() {
    var counter = 0;

    function increment() {
        counter++;
        console.log(counter); // 输出: 1, 2, 3...
    }

    return increment;
}

var closureExample = outer();
closureExample(); // 输出: 1
closureExample(); // 输出: 2
closureExample(); // 输出: 3

在这个例子中,increment 函数是一个闭包,它可以访问并修改 outer 函数作用域中的变量 counter。即使 increment 函数在 outer 函数外部被调用,它仍然可以访问 outer 函数作用域中的变量。

执行上下文

执行上下文(Execution Context)是 JavaScript 中一个非常重要的概念,它描述了在执行代码时 JavaScript 引擎的工作环境。执行上下文决定了变量和函数的可访问性,以及当前执行的代码段。理解执行上下文对于深入理解 JavaScript 的运行机制至关重要。

执行上下文的类型

执行上下文主要有三种类型:

  1. 全局执行上下文:当 JavaScript 代码开始执行时,默认进入的第一个执行上下文。
  2. 函数执行上下文:每当一个函数被调用时,都会创建一个新的函数执行上下文。
  3. Eval执行上下文:当使用 eval() 函数执行字符串形式的代码时,会创建一个新的 Eval 执行上下文。

执行上下文的生命周期

执行上下文的生命周期可以分为三个阶段:

  1. 创建阶段
    • 变量环境(Variable Environment) :创建阶段会初始化变量环境,存储当前执行上下文中的变量和函数声明。
    • 作用域链(Scope Chain) :创建阶段还会初始化作用域链,用于确定变量的可访问性。
    • this绑定:在创建阶段还会确定 this 关键字的绑定。全局执行上下文中的 this 通常指向全局对象(通常是 window),而函数执行上下文中的 this 则取决于函数的调用方式。
  1. 执行阶段
    • 在执行阶段,代码被执行,变量被赋值,函数被调用。执行上下文中的变量和函数在此阶段发挥作用。
  1. 销毁阶段
    • 当执行上下文完成其任务后,会被销毁。对于全局执行上下文,它通常不会被销毁;而对于函数执行上下文,一旦函数执行完毕,就会被销毁。

执行上下文栈

JavaScript 引擎使用一个执行上下文栈来管理执行上下文。每当进入一个新的执行上下文时,都会将该上下文压入栈顶;当退出一个执行上下文时,会将该上下文从栈顶弹出。执行上下文栈确保了执行上下文的正确管理和清理。

示例

下面通过一个具体的例子来说明执行上下文的工作原理:

function outer() {
    var outerVar = "I'm in outer";

    function inner() {
        console.log(outerVar); // 输出: I'm in outer
    }

    inner();
}

outer();

在这个例子中,执行上下文的创建和销毁过程如下:

  1. 全局执行上下文
    • 创建全局执行上下文。
    • 初始化全局作用域链(仅包含全局变量环境)。
    • 执行全局代码,定义 outer 函数。
  1. 函数执行上下文
    • outer 函数被调用时,创建 outer 函数执行上下文。
    • 初始化 outer 函数的作用域链(包含全局变量环境和 outer 函数的变量环境)。
    • 执行 outer 函数的代码,定义 inner 函数,并调用 inner 函数。
  1. 内层函数执行上下文
    • inner 函数被调用时,创建 inner 函数执行上下文。
    • 初始化 inner 函数的作用域链(包含全局变量环境、outer 函数的变量环境和 inner 函数的变量环境)。
    • 执行 inner 函数的代码,输出 outerVar 的值。
  1. 销毁执行上下文
    • inner 函数执行完毕后,销毁 inner 函数执行上下文。
    • outer 函数执行完毕后,销毁 outer 函数执行上下文。

执行上下文栈

执行上下文栈(Execution Context Stack)是 JavaScript 引擎用来管理执行上下文的一个关键数据结构。理解执行上下文栈有助于更好地理解 JavaScript 的执行机制,特别是函数调用和作用域的相关概念。

执行上下文栈的作用

执行上下文栈用于跟踪当前正在执行的函数调用。每当一个函数被调用时,一个新的执行上下文(Execution Context)就会被创建并压入栈顶;当该函数执行完毕后,执行上下文就会从栈中弹出。执行上下文栈确保了函数调用的正确顺序,并且管理着当前执行上下文的生命周期。

执行上下文栈的组成

执行上下文栈中的每个执行上下文包含以下信息:

  1. 变量环境(Variable Environment) :存储当前上下文中的变量和函数声明。
  2. 作用域链(Scope Chain) :用于确定变量的可访问性。
  3. this绑定:确定当前上下文中 this 关键字的值。
  4. 代码执行上下文:当前执行的代码段。

执行上下文栈的生命周期

执行上下文栈的生命周期可以分为以下几个阶段:

  1. 全局执行上下文
    • 当 JavaScript 代码开始执行时,首先创建全局执行上下文。
    • 初始化全局作用域链(仅包含全局变量环境)。
    • 执行全局代码。
  1. 函数执行上下文
    • 每次函数被调用时,都会创建一个新的函数执行上下文并压入执行上下文栈。
    • 初始化函数的作用域链(包含全局变量环境和当前函数的变量环境)。
    • 执行函数代码。
  1. 销毁执行上下文
    • 当函数执行完毕后,函数执行上下文从栈中弹出。
    • 最终,当所有函数调用都完成后,全局执行上下文成为唯一的执行上下文。

示例

下面通过一个具体的例子来说明执行上下文栈的工作原理:

function outer() {
    var outerVar = "I'm in outer";

    function inner() {
        console.log(outerVar); // 输出: I'm in outer
    }

    inner();
}

outer();

在这个例子中,执行上下文栈的操作如下:

  1. 创建全局执行上下文
    • 创建全局执行上下文。
    • 初始化全局作用域链。
    • 定义 outer 函数。
  1. 创建 outer 函数执行上下文
    • outer 函数被调用时,创建 outer 函数执行上下文并压入栈顶。
    • 初始化 outer 函数的作用域链。
    • 定义 inner 函数并调用 inner 函数。
  1. 创建 inner 函数执行上下文
    • inner 函数被调用时,创建 inner 函数执行上下文并压入栈顶。
    • 初始化 inner 函数的作用域链。
    • 输出 outerVar 的值。
  1. 销毁执行上下文
    • inner 函数执行完毕后,销毁 inner 函数执行上下文,并从栈中弹出。
    • outer 函数执行完毕后,销毁 outer 函数执行上下文,并从栈中弹出。

执行上下文栈的可视化

我们可以将执行上下文栈的可视化表示如下:

全局执行上下文
|
+-- outer 函数执行上下文
|    |
|    +-- inner 函数执行上下文
|

在这个例子中,全局执行上下文始终存在,当 outer 函数被调用时,创建 outer 函数执行上下文并压入栈顶;当 inner 函数被调用时,创建 inner 函数执行上下文并再次压入栈顶。当函数执行完毕后,执行上下文依次从栈中弹出。

ES6

let和const

let 用于声明变量,这些变量具有块级作用域。这意味着它们只能在声明它们的代码块内访问。块级作用域通常是指 {} 内的代码块,如 if 语句或循环体。

let 的特点:
  1. 块级作用域:使用 let 声明的变量只能在声明它们的代码块内访问。
  2. 暂时性死区(Temporal Dead Zone, TDZ) :在 let 声明的变量之前访问该变量会导致 ReferenceError。这是因为在变量声明之前,该变量处于暂时性死区。
  3. 不允许重复声明:在同一作用域内,使用 let 无法重复声明同一个变量。
  4. 提升但不可初始化:与 var 不同,let 声明的变量不会被提升并初始化为 undefined。相反,它会处于暂时性死区。
const 的特点:
  1. 块级作用域:与 let 类似,const 声明的变量也是块级作用域。
  2. 暂时性死区(TDZ) :与 let 相同,const 声明的变量在声明之前访问也会导致 ReferenceError。
  3. 不允许重新赋值:一旦使用 const 声明一个变量并赋值,就不能再次给它赋值。
  4. 不允许重复声明:在同一作用域内,使用 const 无法重复声明同一个变量。

对比 var

var 相比,letconst 有几个显著的优势:

  1. 块级作用域var 声明的变量具有函数级作用域,即在声明它们的函数体内任何位置都可以访问。而 letconst 声明的变量具有块级作用域,只能在声明它们的代码块内访问。
  2. 提升(Hoisting)var 声明的变量会被提升至作用域顶部,并初始化为 undefined。而 letconst 声明的变量不会被提升,并且在声明之前访问会导致 ReferenceError。
  3. 重复声明var 允许在同一作用域内重复声明同一个变量,而 letconst 不允许这样做。

实际应用

在实际开发中,建议尽可能使用 letconst,因为它们提供了更好的作用域控制和安全性。以下是一些建议:

  • 使用 const:如果你确定一个变量不会被重新赋值,那么应该使用 const 声明,以避免意外的重新赋值。
  • 使用 let:如果你需要在一个作用域内声明一个变量,并且可能需要重新赋值,那么应该使用 let

模版字符串

模板字符串是 ES6 中一个非常实用的特性,它使得字符串的构造更加直观和灵活。通过模板字符串,你可以轻松地嵌入表达式、处理多行文本,并且利用标签模板函数自定义字符串的处理逻辑。理解模板字符串的用法,可以帮助你编写更清晰、更易维护的代码。

箭头函数

箭头函数提供了一种更简洁的语法来定义函数,并且具有以下特点:

  1. 简洁的语法:可以更简洁地定义函数,尤其是当函数体只有一个表达式时。
  2. this 绑定:箭头函数内部的 this 值是定义时所在的上下文的 this 值,而不是调用时的上下文。
  3. arguments 对象:在箭头函数中,arguments 对象是未定义的,可以使用剩余参数(rest parameters)来代替。

迭代器 for of

迭代协议

迭代协议要求对象实现一个名为 [Symbol.iterator] 的方法,该方法返回一个迭代器对象(Iterator),该迭代器对象实现了 next 方法。next 方法每次调用都会返回一个包含两个属性的对象:valuedonevalue 表示当前迭代的值,而 done 是一个布尔值,表示是否已经完成了迭代。

for...of 循环的基本用法

for...of 循环的基本语法如下:

for (const value of iterable) {
    // 处理 value
}

示例

示例 1:遍历数组
const numbers = [1, 2, 3, 4, 5];

for (const number of numbers) {
    console.log(number);
}
// 输出: 1, 2, 3, 4, 5
示例 2:遍历字符串
const text = "hello world";

for (const char of text) {
    console.log(char);
}
// 输出: h, e, l, l, o,  , w, o, r, l, d
示例 3:遍历 Map
const map = new Map([
    ['key1', 'value1'],
    ['key2', 'value2']
]);

for (const [key, value] of map) {
    console.log(`${key}: ${value}`);
}
// 输出: key1: value1, key2: value2
示例 4:遍历 Set
const set = new Set([1, 2, 3, 4, 5]);

for (const item of set) {
    console.log(item);
}
// 输出: 1, 2, 3, 4, 5

特点和优势

  1. 简洁性for...of 循环提供了一种简洁的方式来遍历可迭代对象。
  2. 自动迭代for...of 循环内部自动调用迭代器的 next 方法,直到 donetrue
  3. 直接获取值:每次迭代直接获取当前迭代的值,不需要像传统 for 循环那样手动索引或使用 Array.prototype.forEach 方法。

for...in 的区别

for...in 循环主要用于枚举对象的可枚举属性(包括原型链上的属性),而 for...of 用于遍历可迭代对象。

示例 5:对比 for...infor...of
const obj = { a: 1, b: 2, c: 3 };

// 使用 for...in
for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
        console.log(`${key}: ${obj[key]}`);
    }
}
// 输出: a: 1, b: 2, c: 3

// 使用 for...of
// 注意:普通对象不是可迭代对象,因此不能直接使用 for...of
// 若要使用 for...of 遍历对象的键或值,可以先将其转换为可迭代对象
const keys = Object.keys(obj);
for (const key of keys) {
    console.log(`${key}: ${obj[key]}`);
}
// 输出: a: 1, b: 2, c: 3

实现可迭代对象

如果你想让自定义对象支持 for...of 循环,需要在对象中实现 [Symbol.iterator] 方法。

示例 6:自定义可迭代对象
class MyIterable {
    constructor(values) {
        this.values = values;
    }

    *[Symbol.iterator]() {
        for (const value of this.values) {
            yield value;
        }
    }
}

const myIterable = new MyIterable([1, 2, 3, 4, 5]);

for (const value of myIterable) {
    console.log(value);
}
// 输出: 1, 2, 3, 4, 5

在这个例子中,MyIterable 类实现了 [Symbol.iterator] 方法,该方法返回一个生成器,生成器使用 yield 关键字来产生值。这样,myIterable 就成为了可迭代对象,可以使用 for...of 循环来遍历。

Async

异步编程(Asynchronous Programming)是现代 JavaScript 中非常重要的一个概念,特别是在处理网络请求、文件操作以及其他耗时操作时。ES6(ECMAScript 2015)引入了 Promise 作为一种处理异步操作的方式,而 ES7(ECMAScript 2016)则引入了 async/await 语法,使得异步代码更加简洁和易于理解。

async 函数

async 关键字用于定义一个异步函数。异步函数总是返回一个 Promise 对象,即使没有显式返回任何内容。如果异步函数内部抛出了错误,返回的 Promise 会变为 rejected 状态。

语法
async function functionName() {
    // 异步操作
}
示例
async function fetchUser() {
    try {
        const response = await fetch('https://api.example.com/user');
        const data = await response.json();
        return data;
    } catch (error) {
        console.error('Error fetching user:', error);
        throw error;
    }
}

fetchUser().then(user => {
    console.log(user);
}).catch(error => {
    console.error('Failed to fetch user:', error);
});

await 表达式

await 关键字用于等待一个 Promise 完成。await 只能在 async 函数内部使用。

示例
async function fetchData() {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    return data;
}

fetchData().then(data => {
    console.log(data);
}).catch(error => {
    console.error('Failed to fetch data:', error);
});

async/await 的优点

  1. 更简洁的语法:使用 async/await 可以让异步代码看起来更像是同步代码,更容易理解和维护。
  2. 错误处理:使用 try/catch 语句可以更自然地处理错误,而不是层层嵌套的 .catch 方法。
  3. 更易读的代码:相较于传统的回调地狱(Callback Hell)或层层嵌套的 Promise 链,使用 async/await 的代码结构更加清晰。

示例:组合多个异步操作

async function processUser(userId) {
    try {
        const userProfile = await fetchUserProfile(userId);
        const userPosts = await fetchUserPosts(userId);
        const userData = {
            profile: userProfile,
            posts: userPosts
        };
        return userData;
    } catch (error) {
        console.error('Error processing user:', error);
        throw error;
    }
}

async function fetchUserProfile(userId) {
    const response = await fetch(`https://api.example.com/user/${userId}/profile`);
    return await response.json();
}

async function fetchUserPosts(userId) {
    const response = await fetch(`https://api.example.com/user/${userId}/posts`);
    return await response.json();
}

processUser('1234').then(userData => {
    console.log('User data:', userData);
}).catch(error => {
    console.error('Failed to process user:', error);
});

注意事项

  1. 错误处理:虽然 async/await 提供了更自然的错误处理方式,但仍需注意捕获和处理错误。
  2. 性能:虽然 async/await 使代码更简洁,但如果异步操作非常耗时,仍需考虑性能优化。
  3. async 函数中的 awaitawait 只能在 async 函数内部使用,否则会抛出语法错误。