速通Javascript

面向对象

原型链

  • JavaScript 使用对象实现继承。所有对象都有一个称为原型的内置属性,它指向的也是一个对象,所以原型对象也会有它自己的原型对象,这个链条就叫原型链,原型链终止于拥有null作为原型的对象上。

  • 内置的原型属性名称没有标准,但是实际上所有浏览器都使用 __proto__ 作为原型属性名。访问原型的标准方法是Object.getPrototypeOf()。

    console.log(Object.getPrototypeOf(new Object()))
    
  • 当你试图访问一个对象的属性时:如果在对象中找不到该属性,那么就会沿着原型链上的对象逐个搜索,直到找到该属性或者到达链的末端,仍然找不到则返回undefined。

  • 属性遮蔽:当一个对象自身拥有某个属性时,会遮蔽其原型链上同名的属性(由于原型链查找机制)。

    const a = {
        name: 'a',
        __proto__: {
            name: 'b'
        }
    }
    
    console.log(a.name)             // 打印a,这种现象叫属性遮蔽
    console.log(a.__proto__.name)   // 打印b
    
  • 自有属性:直接在对象中定义的属性,被称为自有属性。

    const a = {
        name: 'a',
        sex: '男',
        __proto__: {
            name: 'b',
            age: 18
        }
    }
    
    // 推荐使用Object.hasOwn检查自有属性,
    console.log(Object.hasOwn(a, 'name'))  // true:自有属性
    console.log(Object.hasOwn(a, 'age'))   // false:非自有属性
    console.log(Object.hasOwn(a, 'sex'))   // true:自有属性
    
    console.log(a.hasOwnProperty('name'))  // true:自有属性
    console.log(a.hasOwnProperty('age'))   // false:非自有属性
    console.log(a.hasOwnProperty('sex'))   // true:自有属性
    
  • 设置原型

    // 创建字面量对象时通过__proto__
    const a = {
        name: 'a',
        __proto__: {
            name: 'b'
        }
    }
    
    // 使用Object.create创建对象时指定原型
    const personPrototype = {
      greet() {
        console.log("hello!");
      },
    };
    const carl = Object.create(personPrototype);
    carl.greet(); // hello!
    
    // 指定构造函数的prototype
    const personPrototype = {
      greet() {
        console.log(`你好,我的名字是 ${this.name}`);
      },
    };
    
    function Person(name) {
      this.name = name;
    }
    
    Object.assign(Person.prototype, personPrototype);
    // 或
    // Person.prototype.greet = personPrototype.greet;
    

class

  • class是一个创建原型链的语法糖,它让 JavaScript 的面向对象看起来更像经典的面向对象实现,在引擎底层仍然是使用的原型。

  • 构造函数使用 constructor 关键字来声明,在子类的构造函数中须先使用 super() 来调用父类的构造函数,并传递父类构造函数期望的参数。

    // 定义了 Person 类
    class Person {
      name;
      // 构造函数
      constructor(name) {
        this.name = name;
      }
      // 公共方法
      introduceSelf() {
        console.log(`嗨! 我是 ${this.name}`);
      }
    }
    
    // 定义了 Professor 类,继承自Person方法
    class Professor extends Person {
      teaches;
    
      constructor(name, teaches) {
        super(name);
        this.teaches = teaches;
      }
    
      introduceSelf() {
        console.log(
          `我的名字叫 ${this.name}, 我是你们 ${this.teaches} 课程的教授.`,
        );
      }
    
      grade() {
        const grade = Math.floor(Math.random() * (5 - 1) + 1);
        console.log(grade);
      }
    }
    
    const walsh = new Professor("Walsh", "Psychology");
    walsh.introduceSelf(); 	// 打印:我的名字叫 Walsh, 我是你们 Psychology 课程的教授.
    walsh.grade();					// 打印:随机的分数
    
  • 私有成员:私有属性和方法必须在类声明中定义,并且已#开头,私有字段不能被 for…in、Object.keys()、JSON.stringify() 等访问。也不能在外部调用。

    // 定义了 Person 类
    class Person {
        #id;
        // 构造函数
        constructor(id) {
            this.#id = id;
        }
    
        #countingMoney() {
            return 1000 + '元';
        }
    
        // 公共方法
        introduceSelf() {
            console.log(`我是身份证是 ${this.#id} 我有 ${this.#countingMoney()}`);
        }
    }
    
    const person = new Person('123456789');
    person.introduceSelf();
    // 报错:属性 "#id" 在类 "Person" 外部不可访问,因为它具有专用标识符。
    // console.log(person.#id);                
    // 报错:属性 "#countingMoney" 在类 "Person" 外部不可访问,因为它具有专用标识符。
    // console.log(person.#countingMoney());   
    

定义对象属性

  • Object.defineProperty() 静态方法会可以在一个对象上定义一个新属性,或修改已有属性,并返回此对象。

    Object.defineProperty(obj, prop, descriptor)
    
    • obj:操作的对象。
    • prop:操作的属性名。
    • descriptor:属性描述
  • 添加一个只读属性

    let obj = {};
    Object.defineProperty(obj, 'id', {
        value: 123,
        writable: false,        // 不可修改
        enumerable: true,       // 可以被枚举
        configurable: false     // 不能删除或重新定义
    });
    console.log(obj.id);    // 打印123
    obj.id = 456;
    console.log(obj.id);    // 仍然打印123
    delete obj.id;
    console.log(obj.id);    // 仍然打印123
    
  • 添加一个访问器属性:具有get和set(不能同时有value和writable)

    let obj = {};
    let _age = 0;
    Object.defineProperty(obj, 'age', {
        enumerable: true,
        configurable: true,
        get() {
            return _age;
        },
        set(v) {
            _age = v;
        },
    });
    
    console.log(obj.age);   // 打印0
    obj.age = 10;
    console.log(obj.age);   // 打印10
    
  • 通过 . 或者 [] 操作符设置属性等价于

    const obj = {};
    obj.name = "Alice";
    

    等价于

    Object.defineProperty(obj, 'name', {
      value: "Alice",
      writable: true,
      enumerable: true,
      configurable: true
    });
    
  • 数据描述符:如果描述符没有value、writable、get 和 set 中的任何一个,则被视为数据描述符。

作用域

  • 作用域是当前的执行上下文中的值和表达式是否可见(可被访问)。如果一个变量或表达式不在当前的作用域中,那么它是不可用的。作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行。
  • JavaScript 的作用域分以下三种:
    • 全局作用域:脚本模式运行所有代码的默认作用域。
    • 模块作用域:模块模式中运行代码的作用域。
    • 函数作用域:由函数创建的作用域。
    • 块级作用域:用一对花括号创建出来的作用域(只对 letconst 声明有效,对 var 声明无效)。

闭包

  • 闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。

    • 速记:函数记住了它被创建时所在作用域中的变量,即使该函数在其原始作用域之外被调用,仍能访问这些变量。
  • 闭包也是模块模式的基础,可用于实现命名空间和单例。

  • 使用闭包实现数据隐蔽和封装

    const makeCounter = function () {
      let privateCounter = 0;
      function changeBy(val) {
        privateCounter += val;
      }
      return {
        increment() {
          changeBy(1);
        },
    
        decrement() {
          changeBy(-1);
        },
    
        value() {
          return privateCounter;
        },
      };
    };
    
    const counter1 = makeCounter(); 
    const counter2 = makeCounter();
    
    console.log(counter1.value()); // 打印结果是0.
    
    counter1.increment();					 // counter1的私有变量privateCounter加1.
    counter1.increment();					 // counter1的私有变量privateCounter加1.
    console.log(counter1.value()); // 打印结果是2.
    
    counter1.decrement();					 // counter1的私有变量privateCounter减1.
    console.log(counter1.value()); // 打印结果是1.
    console.log(counter2.value()); // 打印结果是0,counter2的词法环境和counter1是互不影响的。
    
  • 每个函数实例管理着它自己的作用域和闭包,在函数内创建不必要的闭包会对处理速度和内存消耗产生负面影响。例如,在创建一个新对象或类时,方法通常应该关联到对象的原型上,而不是定义到对象的构造函数中。

    // 这段代码中的闭包并没有什么额外的好处,我们可以使用原型链避免使用闭包
    function MyObject(name, message) {
      this.name = name.toString();
      this.getName = function () {
        return this.name;
      };
    }
    
    function MyObject(name, message) {
      this.name = name.toString();
    }
    // 继承的原型由所有的对象共享,因此方法定义不需要出现在对象创建中。
    MyObject.prototype.getName = function () {
      return this.name;
    };
    

Proxy

  • Object.defineProperty有个致命的缺陷:无法监听动态新增或删除的属性,也无法监听数组索引的变化,这正是 Vue2 响应式系统饱受诟病的原因。

  • ES6 引入的 Proxy 和 Reflect 彻底改变了这一局面。它们提供了对整个对象的拦截能力,让真正的响应式成为可能。Vue3 正是基于此重构了其核心。

    • Reflect 是一个内置的全局对象,它提供了与 Proxy 拦截器方法一一对应的静态方法。它是为了将原本分散在 Object 上的方法(如 Object.defineProperty)和操作符(如 in, delete)统一到一个命名空间下。

    • 些内部操作(如 == 相等比较)无法被拦截,这是语言设计的安全边界。

    • Proxy 会在每次操作时引入函数调用开销,不适合对性能极度敏感的热路径(如游戏主循环中的每帧计算)。

    • Proxy不支持IE浏览器,Babel polyfill 也不支持Proxy,这也是Vue3不支持IE的核心原因。

    • Object.defineProperty定义时有额外开销,但访问时无额外开销。Proxy每次操作都有拦截开销。

  • 核心语法:创建一个对象的代理,从而实现对基本操作的拦截(如属性查找、赋值、枚举、函数调用等)。

    // 目标对象
    const target = {
      message: 'Hello, Proxy!',
      count: 0,
      [Symbol('secret')]: 'top secret'
    };
    
    // 拦截器处理器(Handler)
    const handler = {
      // 1. get —— 拦截属性读取 (obj.prop 或 obj['prop'])
      get(target, prop, receiver) {
        console.log(`[get] 读取属性: ${String(prop)}`);
        return Reflect.get(target, prop, receiver);
      },
    
      // 2. set —— 拦截属性赋值 (obj.prop = value)
      set(target, prop, value, receiver) {
        console.log(`[set] 设置属性: ${String(prop)} = ${value}`);
        return Reflect.set(target, prop, value, receiver);
      },
    
      // 3. has —— 拦截 in 操作符 ('prop' in obj)
      has(target, prop) {
        console.log(`[has] 检查属性是否存在: ${String(prop)}`);
        return Reflect.has(target, prop);
      },
    
      // 4. deleteProperty —— 拦截 delete 操作 (delete obj.prop)
      deleteProperty(target, prop) {
        console.log(`[deleteProperty] 删除属性: ${String(prop)}`);
        return Reflect.deleteProperty(target, prop);
      },
    
      // 5. ownKeys —— 拦截 Object.getOwnPropertyNames/ Symbols / keys() / for...in 的键枚举
      ownKeys(target) {
        console.log(`[ownKeys] 获取自身所有键`);
        return Reflect.ownKeys(target);
      },
    
      // 6. getOwnPropertyDescriptor —— 拦截 Object.getOwnPropertyDescriptor
      getOwnPropertyDescriptor(target, prop) {
        console.log(`[getOwnPropertyDescriptor] 获取属性描述符: ${String(prop)}`);
        return Reflect.getOwnPropertyDescriptor(target, prop);
      },
    
      // 7. defineProperty —— 拦截 Object.defineProperty / defineProperties
      defineProperty(target, prop, descriptor) {
        console.log(`[defineProperty] 定义属性: ${String(prop)}`, descriptor);
        return Reflect.defineProperty(target, prop, descriptor);
      },
    
      // 8. preventExtensions —— 拦截 Object.preventExtensions
      preventExtensions(target) {
        console.log(`[preventExtensions] 禁止扩展对象`);
        return Reflect.preventExtensions(target);
      },
    
      // 9. isExtensible —— 拦截 Object.isExtensible
      isExtensible(target) {
        console.log(`[isExtensible] 检查对象是否可扩展`);
        return Reflect.isExtensible(target);
      },
    
      // 10. getPrototypeOf —— 拦截 Object.getPrototypeOf / obj.__proto__
      getPrototypeOf(target) {
        console.log(`[getPrototypeOf] 获取原型`);
        return Reflect.getPrototypeOf(target);
      },
    
      // 11. setPrototypeOf —— 拦截 Object.setPrototypeOf
      setPrototypeOf(target, prototype) {
        console.log(`[setPrototypeOf] 设置原型`);
        return Reflect.setPrototypeOf(target, prototype);
      },
    
      // 12. apply —— 拦截函数调用 (仅当 target 是函数时有效)
      // 注意:本例 target 是普通对象,此方法不会被触发
      apply(target, thisArg, args) {
        console.log(`[apply] 调用函数`, { thisArg, args });
        return Reflect.apply(target, thisArg, args);
      },
    
      // 13. construct —— 拦截 new 操作 (仅当 target 是构造函数时有效)
      // 注意:本例 target 是普通对象,此方法不会被触发
      construct(target, args, newTarget) {
        console.log(`[construct] 使用 new 构造实例`, { args, newTarget });
        return Reflect.construct(target, args, newTarget);
      }
    };
    
    // 创建代理
    const proxy = new Proxy(target, handler);
    

模块系统

  • 模块化是构建可维护、可扩展大型 JavaScript 应用的基石。过去,我们依赖社区方案(如 CommonJS、AMD)来组织代码。如今,ECMAScript 模块(ES Modules, ESM) 已成为语言标准,并得到所有现代浏览器和 Node.js 的原生支持。

  • ESM 的模块依赖关系在代码解析阶段就已确定,而非运行时。

  • 实时绑定:ESM 导入的不是值的拷贝,而是对导出值的只读实时引用。如果模块内部的值发生变化,导入方能立即看到更新(前提是导出的是变量而非常量)。

  • 旧规范(CommonJS)的本质区别

    特性ES Modules (ESM)CommonJS (CJS)
    加载时机编译时(静态)运行时(动态)
    依赖分析构建工具/引擎可在执行前分析依赖图无法静态分析,依赖在 require() 时才确定
    值 vs 绑定实时绑定(Live Binding)值拷贝(Copy of Value)
    this 上下文undefined(严格模式)指向 module.exports
    循环依赖通过绑定机制处理,但需注意初始化顺序返回 module.exports 的当前快照
    • 在 Node.js 中,.mjs 文件总是被当作 ESM 处理,而 .cjs 文件总是被当作 CJS 处理。对于 .js 文件,其解析方式取决于 package.json 中的type字段(“module” 或 “commonjs”)。
  • 基本导出

    // mathUtils.js
    
    // 命名导出 (Named Exports) - 可导出多个
    export const PI = 3.14159;
    export function add(a, b) { return a + b; }
    export class Calculator { ... }
    
    // 默认导出 (Default Export) - 一个模块只能有一个
    export default function multiply(a, b) { return a * b; }
    
  • 基本导入

    // main.js
    
    // 导入命名导出 - 必须使用大括号,且名称必须匹配
    import { PI, add, Calculator } from './mathUtils.js';
    
    // 导入默认导出 - 无需大括号,可自定义名称
    import multiply from './mathUtils.js';
    
    // 同时导入默认导出和命名导出
    import multiply, { PI as piConstant } from './mathUtils.js';
    
    // 导入整个模块为一个命名空间对象
    import * as math from './mathUtils.js';
    math.multiply(2, 3); // 调用默认导出
    math.add(1, 2);      // 调用命名导出
    
  • html使用模块

    <!DOCTYPE html>
    <html>
    <head>
        <!-- 内联模块 -->
        <script type="module">
            import { greet } from './utils.js';
            greet('World');
        </script>
    </head>
    <body>
        <!-- 外部模块文件 -->
        <script type="module" src="./main.js"></script>
    </body>
    </html>
    
    • 自动延迟执行:模块脚本总是像带有 defer 属性一样,在文档解析完成后、DOMContentLoaded 事件触发前执行。
    • 严格模式:模块内部自动启用严格模式。
    • CORS 限制:通过 file:// 协议直接打开 HTML 文件会因 CORS 策略失败,必须通过本地服务器(如 http-server)进行测试。
  • 动态导入

    // 根据用户操作动态加载功能模块
    document.getElementById('loadChart').addEventListener('click', async () => {
        const { renderChart } = await import('./chartModule.js');
        renderChart(data);
    });
    
    • import() 作为一个函数,允许你在运行时按需加载模块。它返回一个 Promise,是实现懒加载和代码分割的关键。
  • 聚合导出

    // shapes/index.js
    // 重新导出子模块的所有命名导出
    export { default as Circle } from './circle.js';
    export { default as Square } from './square.js';
    export { default as Triangle } from './triangle.js';
    
    // 或者直接转发
    export { drawCircle, calculateArea } from './circle.js';
    
    • 现在,使用者只需导入 shapes/index.js 即可访问所有形状类。

异步编程

回调函数

  • 早期的 异步API 通过事件回调来实现,例如XMLHttpRequest通过监听不同的事件并执行对应的回调函数来处理数据。
  • 回调函数就是一个被传递到另一个函数中的会在适当的时候被调用的函数:回调函数曾经是 JavaScript 中实现异步函数的主要方式。
  • 回调函数的问题在于如果函数之间有先后顺序时,需要一层层的处理函数调用,并且异步只能在每一层处理无法统一处理。

Promise

  • Promise 是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

  • Promise对象有以下两个特点:

    • 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
    • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
  • 基本用法

    const promise = new Promise(function(resolve, reject) {
      // ... some code
    
      if (/* 异步操作成功 */){
        resolve(value);
      } else {
        reject(error);
      }
    });
    
    promise.then(function(value) {
      // success
    }, function(error) {
      // failure
    });
    
    • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
    • resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”。
    • reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”。
    • Promise实例生成以后可以用then方法分别指定resolved状态和rejected状态的回调函数,这两个函数都是可选的。
  • Promise.prototype.finally()

    • finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});
    
  • Promise.all()

    • all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
    const p = Promise.all([p1, p2, p3]);
    
    • 1)只有p1p2p3的状态都变成 fulfilledp 的状态才会变成 fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。
    • 2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
  • Promise.race()

    • Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
    • 只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
    // 如果5秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。
    const p = Promise.race([
      fetch('/resource-that-may-take-a-while'),
      new Promise(function (resolve, reject) {
        setTimeout(() => reject(new Error('request timeout')), 5000)
      })
    ]);
    
    p
    .then(console.log)
    .catch(console.error);
    
  • Promise.allSettled()

    • ES2020引入了Promise.allSettled()方法,用来确定一组异步操作是否都结束了(不管成功或失败)。所以,它的名字叫做”Settled“,包含了”fulfilled“和”rejected“两种情况。
    const promises = [
      fetch('/api-1'),
      fetch('/api-2'),
      fetch('/api-3'),
    ];
    
    await Promise.allSettled(promises);
    // 三个请求都结束(无论成功还是失败)removeLoadingIndicator()才会执行
    removeLoadingIndicator();
    
  • Promise.any()

    • ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。
    Promise.any([
      fetch('https://v8.dev/').then(() => 'home'),
      fetch('https://v8.dev/blog').then(() => 'blog'),
      fetch('https://v8.dev/docs').then(() => 'docs')
    ]).then((first) => {  // 只要有一个 fetch() 请求成功
      console.log(first);
    }).catch((error) => { // 所有三个 fetch() 全部请求失败
      console.log(error);
    });
    
  • Promise.resolve()

    • 有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。
    Promise.resolve('foo')
    // 等价于
    new Promise(resolve => resolve('foo'))
    
  • Promise.reject(reason)

    • Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected
    const p = Promise.reject('出错了');
    // 等同于
    const p = new Promise((resolve, reject) => reject('出错了'))
    
    p.then(null, function (s) {
      console.log(s)
    });
    // 出错了
    

async 函数

  • ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

    // async函数返回的是Promise
    async function timeout(ms) {
      await new Promise((resolve) => {
        setTimeout(resolve, ms);
      });
    }
    
    async function asyncPrint(value, ms) {
      await timeout(ms);
      console.log(value);
    }
    // 50 毫秒以后,输出hello world。
    asyncPrint('hello world', 50);
    

workers

  • Worker 给了你在不同线程中运行某些任务的能力。
  • Workers 和主代码运行在完全分离的环境中,只有通过相互发送消息来进行交互,这意味着 workers 不能访问 DOM(窗口、文档、页面元素等等),也不能直接访问彼此的变量。
  • dedicated workers
  • shared workers
    • 可以由运行在不同窗口中的多个不同脚本共享。
    • 要使 SharedWorker 连接到多个不同的页面,这些页面必须是同源的(相同的协议、host 以及端口)。
  • service workers
    • Service worker 的行为就像代理服务器,缓存资源以便于 web 应用程序可以在用户离线时工作。他们是渐进式 Web 应用的关键组件。

浏览器

渲染机制

  • 核心步骤:

    1. 第一步:处理 HTML 标记并构造 DOM 树。
      • 遇到 <script> 标签时,默认会阻塞 HTML 解析(除非使用 asyncdefer)。
      • async:脚本异步下载,下载完立即执行(可能打断 HTML 解析)。
      • defer:脚本异步下载,等 HTML 解析完再按顺序执行。
    2. 第二步:处理 CSS 并构建 CSSOM 树。
      • CSSOM 是阻塞渲染的:在 CSSOM 构建完成前,页面不会渲染。
    3. 第三步:将 DOM 和 CSSOM 组合成渲染树。
      • 只包含需要显示的节点(不包括 display: none 的元素)。
    4. 第四步:计算布局。
      • 每个渲染树节点在窗口中的精确位置和大小,这个过程也叫回流。
      • DOM 结构变化、窗口大小改变、读取某些布局属性(如 offsetWidth)都会触发回流。
    5. 第五步:绘制。
      • 将渲染树转换为屏幕上的像素,包括颜色、边框、阴影等。
      • 拆分为多个图层(Layers),便于后续合成。
    6. 第六步:合成。
      • 将多个图层按正确顺序合成最终图像,利用 GPU 加速提升性能。
      • 使用 transformopacity 等属性可触发合成层(Compositing Layer),避免重绘/回流。
  • 预加载器:现代浏览器在主线程解析HTML构建DOM的同时,会启动一个轻量级的预加载扫描器,它快速扫描HTML字节流(无需完整解析DOM),识别出需要的资源标签如link、script、img、video、audio、source、iframe等并理解发起网络请求。

    • 预加载的资源会进入浏览器缓存,主解析器使用时直接命中。

    • 可以使用 rel="preload" 显式预加载资源。

      <!-- 预加载关键字体 -->
      <link rel="preload" as="font" href="font.woff2" type="font/woff2" crossorigin>
      
      <!-- 预加载首屏图片 -->
      <link rel="preload" as="image" href="hero.webp">
      
      <!-- 预加载关键 JS 模块 -->
      <link rel="preload" as="script" href="critical.js">
      
  • 回流:当 DOM 的几何尺寸或布局发生变化时,浏览器需要重新计算元素的位置、大小及其在渲染树中的结构,这个过程称为回流。

  • 重绘:当元素的外观样式(如颜色、背景、边框颜色等)发生变化,但不影响布局时,浏览器只需重新绘制该元素的像素,称为重绘。

  • 减少回流与重绘:

    • 避免连续读写布局属性,因为会触发强制同步回流,性能极差!
    • 将多个单独修改样式操作合并为批量操作。
    • 将元素暂时移出文档流,修改后再放回。
    • 能用 transform 就不用 left/top

DOM事件模型

  • W3C 标准定义的三阶段事件流:

    1. 捕获阶段(Capturing Phase):事件从 window 开始,逐级向下传递到目标元素的父级。此阶段默认不触发监听器。
    2. 目标阶段(Target Phase):事件到达目标元素本身。
    3. 冒泡阶段(Bubbling Phase):事件从目标元素开始,逐级向上传递回 window。
      • 除非特别指定,否则我们添加的事件监听器都默认在冒泡阶段执行。
    window
      ↓ (捕获)
    document
      ↓ (捕获)
    <html>
      ↓ (捕获)
    <body>
      ↓ (捕获)
    <div id="parent">      ← 监听器在此(冒泡阶段触发)
      ↓ (捕获)
      <button id="child">Click me</button> ← 事件目标
      ↓ (冒泡)
    </div>
      ↓ (冒泡)
    <body>
      ↓ (冒泡)
    <html>
      ↓ (冒泡)
    document
      ↓ (冒泡)
    window
    
  • 控制事件阶段

    const parent = document.getElementById('parent');
    const child = document.getElementById('child');
    
    // 默认:冒泡阶段触发
    parent.addEventListener('click', () => {
        console.log('Parent (Bubble)');
    });
    
    // 设置为 true:捕获阶段触发
    parent.addEventListener('click', () => {
        console.log('Parent (Capture)');
    }, true);
    
    child.addEventListener('click', () => {
        console.log('Child (Target)');
    });
    
    --- 输出结果 ---
    
    Parent (Capture)  // 捕获阶段
    Child (Target)    // 目标阶段
    Parent (Bubble)   // 冒泡阶段
    
  • 事件委托

    • 利用事件冒泡机制,将监听器委托给父容器。父容器通过 event.target 判断实际被点击的子元素。

      <ul id="list">
          <li data-id="1">Item 1</li>
          <li data-id="2">Item 2</li>
          <!-- ... 动态添加更多项 ... -->
      </ul>
      
      document.getElementById('list').addEventListener('click', (event) => {
          // 检查实际被点击的元素是否是我们关心的 <li>
          if (event.target.matches('li')) {
              const id = event.target.dataset.id;
              console.log(`Clicked item ${id}`);
              // 处理逻辑...
          }
      });
      
      • 事件委托的优势:
        • 内存高效:只需一个监听器。
        • 自动适配动态内容:新添加的 <li> 无需额外绑定。
        • 代码简洁:逻辑集中,易于维护。
      • 阻止冒泡:Event 对象的stopPropagation()函数,可以阻止事件向其他元素传递。

本地存储

  • Cookies:
    • Secure 属性的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端,有助于防范中间人攻击。
    • HttpOnly 属性的 cookie无法被Document.cookie API访问,此类 Cookie 仅作用于服务器。有助于缓解跨站点脚本(XSS)攻击
    • Domain 属性指定了哪些主机可以接受 Cookie。如果不指定,该属性默认为同一host,不包含子域名。如果指定了 Domain,则一般包含子域名。
    • Path 属性指定了一个 URL 路径,该 URL 路径必须存在于请求的 URL 中,以便发送 Cookie 标头。以字符 %x2F (“/”) 作为路径分隔符,并且子路径也会被匹配。
    • SameSite 属性允许服务器指定跨站点请求发送情况:
      • 设置值 Strict cookie 仅发送到它来源的站点。
      • 设置值 Lax 与 Strict 相似,只是在用户导航到 cookie 的源站点时发送 cookie。
      • 设置值 None 指定浏览器会在同站请求和跨站请求下继续发送 cookie,但仅在安全的上下文中(即,如果 SameSite=None还必须设置 Secure 属性)
  • Web Storage API:
    • sessionStorage:会话期间可用。
    • localStorage:永久存储。
    • Storage API的操作都是同步的,会阻塞主页面。
  • IndexedDB_API:
    • IndexedDB 是一个事务型数据库系统,类似于基于 SQL 的 RDBMS。然而,不像 RDBMS 使用固定列表,IndexedDB 是一个基于 JavaScript 的面向对象数据库。IndexedDB 允许你存储和检索用键索引的对象;可以存储结构化克隆算法支持的任何对象。

JS执行模型

  • 域(Realm)是 JavaScript 执行环境的隔离单元,每一段 JavaScript 代码在加载时都会与一个域相关联,即使从另一个领域调用也不会改变。域由以下信息组成:

    • 固有对象列表,如 ArrayArray.prototype 等。
    • 全局声明的变量、globalThis 的值以及全局对象。
    • 模板字面数组的缓存,因为对同一标记的模板字面表达式的求值总是会导致标记接收到相同的数组对象。
  • Agent 是 ECMAScript 规范中定义的一个逻辑执行单元,代表一个独立的能够执行 JavaScript 代码的引擎实例。它维护着自己的代码执行设施:堆(对象)、队列(作业)、栈(执行上下文)。每个Agent可以拥有多个域(Realm),多个 Agent 可以通过共享内存进行通信,形成一个 Agent 集群。
    在这里插入图片描述

事件循环

  • 核心概念:

    • 执行栈(Call Stack): 存储当前正在执行的函数调用。JavaScript 是单线程的,同一时间只能执行一个函数。
    • 堆(Heap):存放对象等动态分配的数据结构。
    • 消息队列(Message Queue / Task Queue):存放待处理的异步回调函数。当 Web API(如 setTimeout、fetch)完成工作后,会将回调推入此队列。
  • 任务优先级和分类

    • 任务的分类管理是事件循环的核心:

      任务类型常见来源执行时机
      宏任务 (Macrotask)script 整体代码、setTimeoutsetIntervalsetImmediate (Node.js)、I/O、UI 渲染每次事件循环只执行一个宏任务
      微任务 (Microtask)Promise.then/catch/finallyqueueMicrotask()MutationObserver在当前宏任务执行完毕后,UI 渲染之前,清空整个微任务队列
    • 执行流程

      1. 从宏任务队列中取出第一个宏任务并执行。
      2. 执行过程中产生的所有微任务,都被推入微任务队列。
      3. 当前宏任务执行完毕后,立即清空微任务队列(全部执行完)。
      4. (浏览器环境)进行 UI 渲染(如果需要)。
      5. 开始下一次事件循环,回到步骤 1。
  • 避免性能陷阱:queueMicrotask

    • Window 接口的 queueMicrotask() 方法,可以向任务队列增加微任务。如果在微任务中不断添加新的微任务,会导致宏任务(如用户交互、UI 渲染)被无限期阻塞,造成页面卡死。
  • 测试优先级

    console.log('1');
    
    setTimeout(() => {
        console.log('2');
        Promise.resolve().then(() => {
            console.log('3');
        });
    }, 0);
    
    Promise.resolve().then(() => {
        console.log('4');
    });
    
    console.log('5');
    
    • 输出结果:1 → 5 → 4 → 2 → 3
      1. 宏任务1(script整体)开始执行。
        • console.log('1') → 输出 1
        • 遇到 setTimeout,将其回调(打印 ‘2’)交给 Web API,稍后推入宏任务队列。
        • 遇到 Promise.resolve().then(...),将 then 回调(打印 ‘4’)推入微任务队列。
        • console.log('5') → 输出 5
      2. 宏任务1执行完毕。
      3. 清空微任务队列。
        • 执行微任务:console.log('4') → 输出 4
      4. (假设此时需要渲染)
      5. 下一次事件循环开始,宏任务2(setTimeout回调)开始执行。
        • console.log('2') → 输出 2
        • 执行过程中遇到新的 Promise.resolve().then(...),将其回调(打印 ‘3’)推入微任务队列。
      6. 宏任务2执行完毕。
      7. 清空微任务队列。
        • 执行微任务:console.log('3') → 输出 3

内存管理

  • JavaScript 是在创建对象时自动分配内存,并在不再使用时自动释放内存(垃圾回收)。
  • 垃圾回收算法:以前JavaScript使用引用计数垃圾回收算法,但是该算法无法解决循环引用的问题,现在浏览器都是标记清除垃圾回收器算法,过去几年中针对该算法的分代/增量/并发/并行垃圾回收等改进都是在该算法之上的优化。
    • 引用计数垃圾回收算法:对象有个变量引用加1,少一个变量引用减1,当引用数量为0,那么该对象称作“垃圾”或者可回收的。
    • 标记清除垃圾回收器算法:法假定有一组叫做根的对象。在 JavaScript 中根是全局对象。垃圾回收器将定期从这些根开始,找到从这些根能引用到的所有对象,然后找到从这些对象能引用到的所有对象。从根开始,垃圾回收器将找到所有可到达的对象并收集所有不能到达的对象。
  • 弱引用值:WeakMap 和 WeakSet 和非 weak 版的 Map 和 Set 功能一样,
    • WeakMap 和 WeakSet 仅能存储对象或 symbol。这是因为仅对象是可垃圾回收的——原始值总是被复制的。
    • WeakMap 和 WeakSet 是不可迭代的。
    • WeakRef 是对象的弱引用。
  • FinalizationRegistry 提供了一个更强的机制观察垃圾回收。它让你注册对象以及对象被垃圾回收时得到通知。

参考资料

  • https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes

  • https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Extensions/Async_JS

  • https://developer.mozilla.org/zh-CN/docs/Web/Performance/Guides/Critical_rendering_path

  • https://developer.mozilla.org/zh-CN/docs/Learn_web_development/Core/Scripting/Events

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Execution_model

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Memory_management

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Closures

  • https://developer.mozilla.org/zh-CN/docs/Web/API/HTML_DOM_API/Microtask_guide

  • https://developer.mozilla.org/zh-CN/docs/Learn_web_development

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules

  • https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Guides/Cookies

  • https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Storage_API

  • https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API

  • https://developer.mozilla.org/zh-CN/docs/Web/API/IndexedDB_API

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值