JavaScript 的变量保存值。数值、字符串、布尔值等是原始值;对象值允许多处代码访问同一个对象。函数参数也按值传递,传入对象时复制的是对象引用这个值。修改对象属性会被其他持有者看到,重新给参数赋值不会替调用者换掉变量。

const original = { score: 1 };
function update(value) {
  value.score += 1;
  value = { score: 99 };
}
update(original);
console.log(original.score); // 2

展开语法复制数组或对象时通常只复制一层。嵌套对象仍可能共享;需要避免共享时先设计数据边界,不能把 JSON 序列化当成通用深复制,它会改变或丢失部分类型。

词法作用域与闭包

函数能访问定义位置可见的绑定;调用地点不会重写这套词法关系。let/const 是块级绑定;const 限制重新赋值,不冻结对象。var 是函数级作用域,循环中的闭包可能观察同一个绑定。

function makeCounter(start = 0) {
  let count = start;
  return {
    next: () => ++count,
    value: () => count,
  };
}
const a = makeCounter();
const b = makeCounter(10);
console.log(a.next(), a.next(), b.next()); // 1 2 11

闭包保留的是对词法环境的访问能力,环境里的值可以继续变化。长时间存在的订阅、定时器或缓存会让其引用的对象继续可达;不能把所有内存增长都叫“闭包泄漏”,要查引用链和释放时机。

对象、this 与模块

对象的属性查找可沿原型链继续,class 是构造与继承机制的一种语法。实例自己的属性和原型方法并非同一份东西。普通函数的 this 取决于调用方式;箭头函数使用外层 this,不能通过 call 改写。把 obj.method 取出来直接调用,可能丢失原来的接收者。

默认比较优先用 ===,在明确需要转换时显式写 Number、String 等。null 与 undefined 表达的意图不同;空字符串、0 和 false 不能无条件当成“没填”。为默认值选 ?? 还是 ||,应先决定这些值是否有效。

ES module 用 import/export 声明依赖,模块顶层作用域不等于全局。浏览器 module 脚本的加载、跨域和文件协议限制与经典 script 不同;用本地 HTTP 服务测试模块。

Node 可运行的断言示例 用原生 assert 检查对象共享、闭包隔离和 this。运行 node example.mjs,断言失败会中止。它验证语言行为,不验证浏览器 DOM。

练习:实现 makeHistory,返回 add 与 list;list 每次返回新的数组,调用者 push 不影响内部历史。若数组元素本身也是对象,再说明是否需要额外复制。

面试自查:const 对象为什么还能变?闭包和作用域是什么关系?箭头函数为什么不适合所有对象方法?

参考:ECMAScript 语言规范、MDN 闭包、对象原型。