值与类型:变量保存了什么
读程序时,先问“这个表达式现在产生什么类型的值”,再问下一步会做什么。JavaScript 的变量不固定属于某个类型:同一个 let 名字可以先关联数字,再关联字符串。类型属于值,这一点比背八个名称更重要。
七种原始类型和对象
| 类型 | 例子 | 用途 |
|---|---|---|
undefined | undefined | 未提供、未初始化为其他值 |
null | null | 程序主动约定的空值 |
boolean | true、false | 真假判断 |
number | 12、0.5、NaN | 普通数值计算 |
bigint | 12n | 大整数计算 |
string | '章节' | 文本 |
symbol | Symbol('id') | 唯一标识、属性键 |
object | { title: '变量' } | 组织有关联的数据与行为 |
前七种是原始类型。数组、日期、函数等都是对象类别中的成员;函数额外具有可调用能力。对象在第三章系统展开,这里只用一个最简单的对象:{ title: '变量' } 含有名为 title 的属性,可以用 对象.title 读取或修改它。
let value = 12;
console.log(typeof value); // number
value = '12';
console.log(typeof value); // string
console.log(typeof null); // object:历史行为,不代表 null 是对象
console.log(typeof undefined); // undefinedtypeof 返回字符串,不是一张完整类型身份证:它把数组和普通对象都报告成 'object',把可调用函数报告成 'function'。判断 null 用 value === null;判断数组用第三章的 Array.isArray。不要先用 typeof value === 'object' 就放心读取属性,null.title 会抛错。
原始值不可变,变量可以重新赋值
let label = 'JS';
const saved = label;
label = label + ' 基础';
console.log(saved, label); // JS JS 基础拼接产生新字符串,并让 label 关联新值。旧字符串没有被“改长”,所以 saved 仍然是 'JS'。数字也一样:n = n + 1 不会改变数字 1 本身。
原始值可以调用一些方法,例如 'js'.toUpperCase() 得到 'JS'。这属于语言对属性访问提供的包装机制,不应推导出“一切都是对象”。严格模式下向字符串添加属性会抛出 TypeError;非严格模式可能静默失败。无论哪种,都不能拿原始值当可扩展记录。
两个独立问题:能换值吗,能改值内部吗
常见混淆来自把 let/const 和“原始值/对象”混成同一条轴:
| 声明与当前值 | 能给变量重新赋值 | 当前值的内容能否直接修改 |
|---|---|---|
| let保存字符串 | 能 | 字符串内容不可变,只能产生新字符串 |
| const保存字符串 | 不能 | 字符串内容仍不可变 |
| let保存普通对象 | 能 | 对象属性通常可以改 |
| const保存普通对象 | 不能 | 对象属性仍通常可以改 |
“通常”是因为对象属性还可以设置只读、冻结等限制,第四章会解释。这里的关键是这些限制来自对象机制,不来自保存它的变量叫什么。
let text = '入门';
const retained = text;
text += '篇';
console.log(text, retained); // 入门篇 入门
const record = { title: '入门' };
record.title += '篇';
console.log(record.title); // 入门篇text += '篇' 是 text = text + '篇' 的简写:先拼接,再赋值。
上半段 text += '篇' 产生新字符串,并重新赋给变量;下半段同样产生新字符串,但赋给了对象的属性。对象属性中保存的旧字符串也没有被原地修改。对象可变,不代表其每一个属性值都变成了可变值。
两个名字可以访问同一个对象
const first = { title: '变量' };
const second = first;
second.title = '类型';
console.log(first.title); // 类型
console.log(first === second); // true
console.log(first === { title: '类型' }); // false赋值没有克隆对象。first 和 second 关联同一对象身份,修改其属性后,两边都能看见变化。第三次比较创建了另一个对象;即使属性内容一样,对象身份仍不同。
first ─┐
├── 同一个对象 { title: '类型' }
second ─┘const 保证 first 不能改为另一个值,不能阻止 first.title 改动。把 const 翻译成“内容永恒不变”会让后续很多问题无从解释。第三章会区分浅复制、深复制和冻结。
JavaScript 的赋值和参数传递都复制值。值若关联一个对象,复制后会共享对象身份;这不等于函数能够重新绑定调用者的变量。第二章通过“改属性”和“替换参数”的对比验证这一点。
身份与内容要分别比较
试着只改变一个条件:两个变量是从同一个对象赋值得到,还是各执行了一次对象字面量?
let current = { page: 1 };
const saved = current;
current = { page: 2 };
console.log(current.page, saved.page); // 2 1
console.log(current === saved); // falsecurrent 重新关联了第二个对象,saved 仍关联第一个。与前面 second.title = ... 的区别在于左侧:赋给变量是在更换关联,赋给属性是在修改已关联对象中的字段。把左侧圈出来,通常就能判断是哪一种动作。
这里可以画“变量到对象身份的箭头”,但不必推导成“原始值一定在栈、对象一定在堆”。引擎可以采用多种优化和内存布局;语言承诺的是观察到的赋值与相等行为,而不是让程序员看到某个固定物理地址。现在掌握语义,后面学引擎时再讨论实现。
缺失、空值和假值不是同一个集合
undefined 常来自没有提供值,null 通常表达明确的空缺。但这属于常见约定,不是语言强制的业务语义。是否允许空字符串、零、false,应按字段定义决定。
普通语言值中的假值有 false、0、-0、0n、''、null、undefined、NaN。空数组和空对象都是真值;字符串 'false' 也是真值,因为非空。浏览器历史遗留的 document.all 具有特殊行为,属于兼容性例外,不用它表示业务状态。
console.log(Boolean('false')); // true
console.log(Boolean(0)); // false
console.log(0 ?? 20); // 0
console.log(0 || 20); // 20?? 只在左边是 null 或 undefined 时取右边;|| 在左边为假值时取右边。免费价格是 0 时,price || 20 会凭空收出 20 元。第四篇会把转换与短路规则串起来。
Symbol 的唯一性
const a = Symbol('chapter');
const b = Symbol('chapter');
console.log(a === b); // false
console.log(Symbol.for('chapter') === Symbol.for('chapter')); // true描述文本只用于辨认,不参与身份相等。Symbol.for 查找或登记全局符号注册表,所以同名得到同一符号。第三章会将它们用作对象键,第六章讲 Symbol.iterator 等协议。符号不是字符串的一种特殊写法,也不能随便参与字符串拼接。
练习
const a = { n: 1 }; const b = a; b.n = 2;后a.n是多少?若改成const b = { n: a.n };呢?分别说明
0、'0'、null、{}在布尔判断中的结果;为允许零的数量字段选择缺省运算符。let a = { n: 1 }; const b = a; a = { n: 2 };后 b.n 是多少?与写a.n = 2的版本为何不同?草稿为
const draft = { title: '初稿', pages: 0 };。另存一份名为published的普通对象,只把标题改成'发布稿',保留零页数;之后修改published的页数,draft仍应为0。只用已学的属性读取和对象字面量完成,说明为什么const published = draft不满足要求。