JavaScript高级程序设计笔记 4

第4章 变量、作用域与内存

1. 原始值与引用值

1.1 核心区别

特性 原始值 引用值
类型 Undefined, Null, Boolean, Number, String, Symbol, BigInt Object (Array, Function, Date, RegExp 等)
存储位置 栈内存 堆内存
访问方式 按值访问 按引用访问
动态属性 不可添加 可随时添加、修改、删除
复制方式 复制值副本 复制指针(引用)
比较方式 值比较 引用比较(比较是否为同一对象)

1.2 复制值

  • 原始值:两个变量独立,互不影响。
    let num1 = 5;
    let num2 = num1;  // 独立副本
    num2 = 10;        // num1 仍为 5
    
  • 引用值:两个变量指向同一对象,修改会相互影响。
    let obj1 = {};
    let obj2 = obj1;   // 复制的是指针
    obj1.name = 'Nicholas';
    console.log(obj2.name);  // 'Nicholas'
    

1.3 传递参数(关键考点!)

JavaScript 中所有函数参数都是按值传递的。

  • 传递原始值:将值复制给局部变量,函数内修改不影响外部。

    function addTen(num) {
      num += 10;
      return num;
    }
    let count = 20;
    let result = addTen(count);
    console.log(count);  // 20(未变)
    
  • 传递引用值:复制的是地址副本,仍指向同一对象。但重新赋值不会影响原变量

    function setName(obj) {
      obj.name = 'Nicholas';     // 修改原对象
      obj = new Object();        // 重新赋值,切断关联
      obj.name = 'Greg';         // 修改新对象
    }
    let person = {};
    setName(person);
    console.log(person.name);    // 'Nicholas'(不是'Greg')
    

1.4 确定类型

  • typeof:最适合判断原始值。null 返回 "object"(历史遗留 Bug)。
  • instanceof:判断引用值的具体类型。
    console.log(person instanceof Object);  // true
    console.log([] instanceof Array);       // true
    console.log('hello' instanceof String); // false
    

2. 执行上下文与作用域

2.1 执行上下文(Execution Context)

JavaScript 代码解析和执行时的环境。

类型 数量 生命周期
全局执行上下文 唯一(浏览器中为 window 页面关闭时销毁
函数执行上下文 多个 函数调用时创建,执行完销毁
eval 执行上下文 按需 不推荐使用

2.2 执行栈(Call Stack)

后进先出的栈结构,用于管理执行上下文。调用函数时入栈,执行完后出栈。

2.3 执行上下文的创建过程

  1. 生成变量对象(Variable Object)
  2. 建立作用域链
  3. 确定 this 的指向

2.4 变量对象与活动对象

概念 说明
全局对象(GO) 存放内置属性和方法
变量对象(VO) 存放执行上下文中创建的变量和值
活动对象(AO) 函数私有上下文中的变量对象

2.5 作用域(静态)与上下文(动态)

  • 作用域是静态的:函数定义时就确定了访问范围。
  • 执行上下文是动态的:函数调用时才创建,执行完销毁。

2.6 作用域链

当前执行环境有权访问的变量和函数的有序层级结构

  • 查找规则:从自身活动对象开始,逐级向外层查找,直到全局作用域。
  • 作用域链增强try/catchcatch 块和 with 语句会临时增加作用域链长度。

3. 垃圾回收与内存管理

3.1 主要垃圾回收策略

引用计数(已淘汰)

  • 基本原理:记录每个对象的引用次数。
  • 致命缺陷:循环引用导致内存泄漏。
    let objA = {};
    let objB = {};
    objA.ref = objB;
    objB.ref = objA;  // 相互引用,引用计数永不为 0
    

标记清除(主流)

  • 基本原理:标记所有可达对象,清除未被标记的对象。
  • 可达性:从根(window、活动栈、闭包环境、DOM 树)出发,能被访问到的对象。

3.2 性能优化

  • V8 分代回收:新生代使用 Scavenge,老生代使用 Mark-Sweep/Mark-Compact。
  • 增量/并发标记:降低垃圾回收造成的页面卡顿。

3.3 常见内存泄漏场景

场景 说明 解决方案
意外的全局变量 function foo() { bar = [] }bar 隐式挂到 window 启用严格模式 + ESLint
定时器未清理 setInterval 闭包持有大对象 组件销毁时 clearInterval
事件监听未解绑 闭包持有已移除的 DOM 元素 使用 AbortController 或事件委托
闭包滥用 外部函数变量持续被引用 及时置为 null,用完后解除引用

4. 常见面试题速查

基础概念

  1. 原始值和引用值的区别?
    → 原始值存栈、不可变、复制值;引用值存堆、可变、复制指针。

  2. typeof null 为什么返回 'object'
    → JavaScript 的 Bug,在语言实现早期就存在,为保持兼容性至今未修改。

  3. 如何准确判断一个值是数组?
    Array.isArray(value)value instanceof Array

  4. nullundefined 的区别?
    undefined 是声明未初始化,null 是空对象指针。

执行上下文与作用域

  1. 什么是执行上下文?有哪几种类型?
    → 代码执行时的环境。有全局、函数、eval 三种。

  2. 什么是作用域链?如何查找变量?
    → 从当前作用域开始,逐级向外层查找,直到全局。找不到则报 ReferenceError

  3. 作用域和执行上下文有什么区别?
    → 作用域是静态的(定义时确定),执行上下文是动态的(调用时创建)。

  4. 函数执行上下文何时创建和销毁?
    → 函数调用时创建,执行完毕后销毁(闭包可使变量保留)。

参数传递

  1. JavaScript 是值传递还是引用传递?
    → 按值传递。原始值传值副本,引用值传递地址的副本(共享传递)。

  2. 为什么函数内 obj = new Object() 不会影响外部变量?
    → 因为重新赋值改变的是形参的指向,外部变量仍指向原对象。

  3. 如何实现函数参数的“按引用传递”效果?
    → 将参数包装为对象属性,或通过返回值赋值。

垃圾回收

  1. 引用计数有何缺陷?为什么被淘汰?
    → 循环引用导致对象无法回收。如对象 A 和 B 相互引用,引用计数永不为 0。

  2. 标记清除的基本原理是什么?
    → 标记所有可达对象,清除未被标记的对象。

  3. 什么情况下会造成内存泄漏?如何避免?
    → 意外全局变量、未清理定时器、未解绑事件监听、滥用闭包。使用严格模式、组件销毁时清理。

  4. 闭包为什么可能导致内存泄漏?
    → 闭包会持有外部函数的变量,如果这些变量长期不被释放(如存到全局数组),就会占用内存。

posted @ 2024-04-08 09:25  Li_pk  阅读(5)  评论(0)    收藏  举报