主题
新增的运算符与symbol 类型(Day3 2/4)
本页对应 Day03_ES6,共 6 段视频,约 1 小时 21 分。
学习目标
- 能创建 Symbol 类型数据,使用
typeof判断其类型,并说明其唯一性及作为对象属性名时的读写方式。 - 能使用
class关键字定义类,说明其本质仍是构造函数,并验证该类只能实例化、不能直接调用。 - 能在
class中定义属性和方法,区分直接赋值属性添加到实例、简写方法添加到原型上的行为。 - 能编写
constructor构造器方法,在实例化时自动执行并完成属性初始化、参数处理和初始值计算。 - 能声明并使用私有属性(
#),说明私有属性需提前声明、类外不可访问,并能定义和调用静态方法。 - 能使用
extends和super实现类继承,说明原型关系、方法重写规则及子类构造器中super的调用要求。
知识点
先看这张表:六个运算符的共同点
本节笔记是一张表,列出 **、?.、??、&&=、||=、??= 六个新增运算符,并给出四列关键信息:操作数个数、操作数类型要求、表达式值的类型、有无副作用。配套转写稿的内容集中在 Symbol 与 class 语法上,因此下面以表格这四列为纲展开——它其实是在教你“读运算符”的方法:先看它吃几个操作数、要不要类型、吐出什么值、有没有副作用。
为什么需要:很多“用错运算符”的 bug,根源是没搞清这四件事。比如把 ?. 当成“判断真假”的简写,把 ?? 当成 || 的别名,都是没读表。 怎么想:把表当契约。前三个(**、?.、??)副作用为“无”,说明它们是纯取值/纯计算,不会改动任何变量;后三个副作用为“有”,且左操作数必须是变量形式,说明它们会重新赋值。 怎么做:遇到任何一个运算符,先问自己这四问,再写代码。 易错点:把“表达式值的类型”误当成“操作数类型”。?? 的操作数没有类型要求,但它的值类型是“多种”——取决于左右哪边被选中。
** 指数运算符
为什么需要:求幂以前只能写 Math.pow(2, 10),或者循环连乘。语言层面给一个运算符,代码更短、更贴近数学写法。
怎么想:它是二元的、两个操作数都要求 number、结果也是 number、无副作用——和 +、* 属于同一类算术运算符,只是优先级和结合性不同。
怎么做:
js
console.log(2 ** 10); // 1024
console.log(2 ** 3 ** 2); // 512,右结合,等价于 2 ** (3 ** 2)易错点:** 是右结合,不是左结合,2 ** 3 ** 2 不等于 (2 ** 3) ** 2。另外,一元负号不能直接写在左侧操作数前面,-2 ** 2 会报语法错误,必须写成 (-2) ** 2 或 -(2 ** 2)。
?. 可选链运算符
为什么需要:深层取属性时,中间任何一层是 null 或 undefined,就会抛 TypeError。以前只能层层 && 短路,或者套 try...catch。
怎么想:表中写“操作数类型要求:无”,说明它不挑类型;值可能是 undefined 或其他,说明一旦链条断了,整个表达式安静地返回 undefined,而不是报错。
怎么做:
js
const user = { info: { name: '张三' } };
console.log(user.info?.name); // 张三
console.log(user.address?.city); // undefined,不报错易错点:?. 只在“左侧为 null/undefined”时短路,0、''、false 不会触发短路。另外它不能用于赋值左侧,a?.b = 1 是非法写法。
?? 空值判断运算符
为什么需要:|| 会把 0、''、false 都当成“假”而取右边,但业务上这些常常是合法值,只有 null/undefined 才该走默认值。
怎么想:名字就叫“空值判断”,判定条件只有 nullish 两种值,不看 falsy。
怎么做:
js
const count = 0;
console.log(count || 10); // 10,错了
console.log(count ?? 10); // 0,正确易错点:?? 与 ||、&& 混用时不加括号会报语法错误,必须写 (a ?? b) || c。另外它与 ?. 一样,无副作用。
&&=、||=、??= 三个赋值运算符
为什么需要:a = a || b 这类写法重复了左操作数,而且左操作数如果是复杂表达式会被求值两次。
怎么想:表中“左边操作数是变量的形式”这一条最关键——它不是随便一个表达式,而是一个可赋值的引用。值是“左边重新赋值之后的值”,所以整个表达式可以继续参与运算。副作用为“有”,因为它改写了变量。
怎么做:
js
let a = null;
a ??= '默认值'; // a 变成 '默认值'
let b = 1;
b &&= 2; // b 变成 2
let c = 0;
c ||= 5; // c 变成 5易错点:三者都是“需要赋值时才求值右侧”,具备短路特性:a &&= fn() 在 a 为假值时根本不会调用 fn()。这一点常被忽略,导致以为副作用一定发生。另外 ??= 同样只认 nullish,0 不会被覆盖。
副作用一列怎么读
前三个运算符“无副作用”,意味着你可以放心把它们塞进表达式里做判断;后三个“有副作用”,意味着它们本质是赋值语句的简写,出现在条件里会悄悄改变量。转写稿里反复强调的那套方法——由已有结论推出新结论,而不是死记——在这里同样适用:知道了 ?? 只认 nullish,就能自己推出 ??= 也只认 nullish,不必单独背。
先纠正与保留笔记
转写稿里的“森宝、生宝、申保、分宝、simble”都应写作 Symbol;“sybol”也是笔误。先原样保留本节笔记:
1. symbol 使用原始类型,使用 typeof 判断返回 symbol
2. 使用 Symbol() 函数创建 sybol 类型的数据,Symbol() 只能调用不能实例化
3. 每使用 Symbol() 创建一个数据,都是独一无二(具有对象特性)
3. symbol 类型的数据可以用作属性名 (属性名可以是字符串或者symbol数据)下面按这四条展开。
1. Symbol 是原始类型,typeof 返回 symbol
为什么需要: 前面学过的原始类型有 number、string、boolean、undefined、null 等。Symbol 也是原始类型,所以判断类型时要有稳定的结果。
怎么想: 先创建值,再用 typeof 看类型。实际中就是先写 const s1 = Symbol();,然后输出 typeof s1,浏览器返回 "symbol"。
怎么做:
js
const s1 = Symbol();
console.log(typeof s1); // "symbol"易错点: typeof 返回的是小写字符串 "symbol",不是 "Symbol";也不要因为 Symbol 有“唯一性”就误认为它是对象,它的类型仍是原始类型。
2. 用 Symbol() 函数创建,只能调用不能实例化
为什么需要: 需要一个专门创建 symbol 值的入口,同时要和普通构造函数区分开。
怎么想: 转写稿里特意试了 new Symbol(),结果报错,说明它不是构造函数。ES6 有意把“函数”和“构造函数”区分得更清楚。
怎么做:
js
const s1 = Symbol();
const s2 = Symbol('id'); // 参数只是标记,方便开发者看易错点: 不能写 new Symbol();Symbol 只能当函数调用。参数名字一样也不代表值一样,名字只是标记。
3. 每次 Symbol() 创建的数据都是独一无二的
为什么需要: 需要一个“不会和别的值撞名”的标识。转写稿先创建 s1、s2,看长相似乎一样,但比较结果是不等。
怎么想: 对象、数组、函数每次创建也都不相等,但 Symbol 的特殊点在于:它是原始类型,却具有“每次创建都唯一”的效果。可以把它理解成“具有对象特征的原始类型数据”。
怎么做:
js
const s1 = Symbol('a');
const s2 = Symbol('a');
console.log(s1 === s2); // false易错点: 不要以为 Symbol('a') 和另一个 Symbol('a') 相等;也不要以为不保存也能找回来。转写稿强调:如果创建后不用变量存下来,后面就永远找不到它了。
4. Symbol 数据可以用作属性名
为什么需要: 对象属性名过去主要是字符串,容易重名。Symbol 唯一,可以做一个外部很难碰到的属性名。
怎么想: 对象字面量里直接写 s1: 123,浏览器会把 s1 当成字符串属性名;要把变量 s1 的值当属性名,必须用中括号 [s1]。读取时也要用同一个 symbol。
怎么做:
js
const s1 = Symbol('id');
const obj = {
name: '华为',
[s1]: 123
};
console.log(obj[s1]); // 123
console.log(obj[Symbol('id')]); // undefined转写稿还演示了:如果属性名变量是数组,会被自动转成字符串;但 symbol 不会这样自动转换。读取 symbol 属性必须用 obj[s1],不能写成 obj.s1。
易错点: 定义对象时创建了一个 Symbol,读取时又写 Symbol('id'),这是两个不同的值,读不到;必须把创建出来的 symbol 存到变量里,定义和读取都用同一个变量。对象属性名可以是字符串,也可以是 symbol 数据。
小结
Symbol 这一节记住四件事:它是原始类型,typeof 返回 "symbol";用 Symbol() 调用创建,不能 new;每次创建都独一无二;它可以作为对象属性名,但定义和读取都要用中括号和同一个 symbol 变量。转写稿还提到内置 Symbol.iterator 被部署在字符串原型上作为可遍历接口,这属于后续分类用法,本节先建立“symbol 可作属性名”的基本认识即可。
从构造函数到 class:本质仍是函数
为什么需要:ES6 之前定义“类”只能写构造函数,再手动往 prototype 上挂方法,写法零散;class 让定义类型更像 Java 等面向对象语言,语法更集中。
怎么想:先别把 class 神秘化。转写稿里反复验证:typeof Product 返回 "function",说明它归根结底还是函数,只是不能像普通函数那样直接调用。
怎么做:
js
class Product {
name;
price;
buy() {
console.log(`购买 ${this.name}`);
}
}
console.log(typeof Product); // "function"
// Product(); // 报错:类不能直接调用
const p = new Product();易错点:class 不是 CSS 的 class;类名仍是变量名,推荐首字母大写;类只能用 new 实例化,不能直接 Product() 调用。类体内只能定义属性和方法,不能随便写 console.log() 这类语句。
类字段与实例属性:把写死改成可传参
为什么需要:如果属性值都写死在类里,每个实例都一样,不合理。实例化时应能传参,再给属性赋初始值。
怎么想:属性属于实例,方法通常属于原型。转写稿中特别强调:简写形式的方法会自动加到实例原型上;直接写 buy = function(){} 这类形式则更接近实例自身属性。
怎么做:
js
class Product {
name;
price;
#stock = 10;
buy(count) {
console.log(`购买 ${count} 件 ${this.name}`);
}
}易错点:类字段写法是较新的语法,ES2022 之前不能这样直接在类体里写属性;方法推荐用简写形式,不要写成箭头函数,因为箭头函数没有自己的 this。
构造器方法 constructor:实例化时自动执行
为什么需要:需要在 new 的时候接收参数、做判断、赋初始值。
怎么想:constructor 是固定名字,不需要手动调用;new 的那一刻自动执行,里面的 this 指向实例。
怎么做:
js
class Product {
name;
price;
constructor(name, price) {
this.name = name;
this.price = Math.max(price, 100);
}
buy(count) {
console.log(`购买了 ${count} 件 ${this.name},单价 ${this.price}`);
}
}
const p1 = new Product("华为P40", 199);
const p2 = new Product("手机", 99);
console.log(p1.price, p2.price); // 199 100易错点:一个类不写 constructor 也会默认有一个空构造器;构造器里常见操作是赋初始值,也可做参数校验;this 指向实例,不要误以为指向类本身。
私有属性 #:只能在类内部使用
为什么需要:有些属性只供类内部方法使用,不希望外部随便读写。
怎么想:属性前加 # 变成私有字段;外部访问会报错,类内部方法可通过 this.#name 使用。
怎么做:
js
class Product {
#name;
price;
constructor(name, price) {
this.#name = name;
this.price = price;
}
buy(count) {
console.log(`购买 ${count} 件 ${this.#name}`);
}
}
const p = new Product("华为P40", 199);
p.buy(2);
// console.log(p.#name); // 报错易错点:私有属性必须先在类体中声明,不能直接在某处突然写 this.#name = ...;私有属性不能被子类直接修改,子类只能通过父类提供的方法间接操作;私有方法也可用 #,但实际较少用。
静态方法 static:属于类本身
为什么需要:有些方法不属于某个实例,而属于类本身,比如工具方法。
怎么想:静态方法挂在构造函数/类上,不需要 new 实例即可调用。
怎么做:
js
class Product {
static getInfo() {
console.log("Product 类中的静态方法");
}
}
Product.getInfo();易错点:静态方法不能通过实例调用;转写稿提到静态属性当时仍处于提案阶段,浏览器可能提前支持,但不要把它和正式标准混为一谈。
extends 与 super:继承自动完成原型设置
为什么需要:以前实现继承要手动改子类原型、修正 constructor、再重复父类赋值代码,很繁琐。
怎么想:extends 自动完成原型链设置;子类重写 constructor 时,必须先调用 super(...) 执行父类构造器。
怎么做:
js
class Phone extends Product {
color;
constructor(name, price, color) {
super(name, price);
this.color = color;
}
buy(count) {
console.log(`购买 ${count} 件 ${this.name},颜色 ${this.color}`);
}
}
const phone = new Phone("华为P50", 189.9, "黑色");
phone.buy(12);易错点:子类重写构造器时 super 必须放在最前面;super 在这里是函数调用,不是上一节对象方法里的 super;父类私有属性子类不能直接改;子类方法与父类同名会覆盖父类方法。调试时先看 typeof、再 new 看实例属性,最后看原型上的方法,就能把 class 的“语法糖”与底层构造函数机制对应起来。
课堂画面






代码示例
示例一:Symbol 类型与新增运算符
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Symbol 与新增运算符</title>
</head>
<body>
<script>
// ========== 1. Symbol:独一无二的原始类型 ==========
const s1 = Symbol('标记');
const s2 = Symbol('标记');
console.log(typeof s1); // symbol
console.log(s1 === s2); // false —— 名字一样也不相等
// Symbol 只能调用,不能实例化
// const s3 = new Symbol(); // 报错:Symbol is not a constructor
// ========== 2. Symbol 作为属性名 ==========
const key = Symbol('key');
const user = {
userName: '小明',
[key]: '上海', // 变量当属性名必须用中括号
[Symbol('key')]: '深圳' // 每次 Symbol() 都是全新的,取不回来
};
console.log(user[key]); // 上海
console.log(user['key']); // undefined
console.log(user[Symbol('key')]); // undefined —— 又创建了一个新的
// ========== 3. 新增运算符 ==========
console.log(2 ** 10); // 1024 指数运算符
const obj = { a: { b: 1 } };
console.log(obj?.a?.b); // 1
console.log(obj?.x?.b); // undefined,不会报错
const val = null;
console.log(val ?? '默认值'); // 默认值
console.log(0 ?? '默认值'); // 0 —— ?? 只认 null / undefined
let x = 1, y = 0, z = null;
x &&= 5; // x 为真,重新赋值为 5
y ||= 5; // y 为假,重新赋值为 5
z ??= 5; // z 为 null,重新赋值为 5
console.log(x, y, z); // 5 5 5
</script>
</body>
</html>要点解释与易错点
typeof s1返回'symbol',Symbol 是原始类型,但具有对象“每创建一个都独一无二”的特性。Symbol()只能调用,不能new;括号里的字符串只是开发者看的标记,不参与比较。- 用变量 / Symbol 作属性名必须写
[key];直接写key会被当成字符串'key'。 - 定义对象时
[Symbol('key')]创建的那个 Symbol 没有变量保存,永远取不回来——想用就先用变量存下来。 ??只判断null / undefined,不像||会把0、''也当成假值;&&=、||=、??=都是有副作用的赋值表达式。
示例二:Class 基础、构造器方法、私有属性、静态方法
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Class 基础</title>
</head>
<body>
<script>
class Product {
// 私有属性必须提前声明,名字前加 #
#name;
#price;
#stock = Math.floor(Math.random() * 100);
// 公有属性直接写(ES2022 起)
color = '白色';
// 构造器方法:名字固定,实例化时自动执行
constructor(name, price) {
this.#name = name;
this.#price = Math.max(price, 100); // 价格不能低于 100
}
// 简写方法 —— 自动挂到实例的原型上
buy(count) {
console.log(`购买了${count}件 ${this.#name},单价${this.#price}`);
}
addShoppingCart() {
console.log(`将 ${this.#name} 加入购物车`);
}
// 私有方法:类外部调用不到
#log() {
console.log('内部日志');
}
// 静态方法:属于类本身,无需实例化
static getInfo() {
console.log('Product 类中的静态方法');
}
}
const p1 = new Product('华为P40', 199);
p1.buy(12); // 购买了12件 华为P40,单价199
p1.addShoppingCart(); // 将 华为P40 加入购物车
// console.log(p1.#name); // 报错:私有属性类外部不可访问
// p1.#log(); // 报错:私有方法类外部不可访问
console.log(typeof Product); // function —— 本质仍是构造函数
// Product(); // 报错:类只能用 new 调用
Product.getInfo(); // 静态方法用类名直接调用
</script>
</body>
</html>要点解释与易错点
class本质还是构造函数,typeof Product === 'function',但不能用Product()直接调用,只能new。- 只有简写形式的方法才会挂到原型上;写成
buy = function () {}是属性,会挂到实例自身。 - 构造器方法
constructor名字固定,实例化时自动执行,无需手动调用;不写也会默认加一个空的。 - 私有属性
#name必须在类体中提前声明,否则赋值时报错;类外部通过p1.#name访问不到。 - 私有属性在子类中也改不了,只能在定义它的类内部修改。
static方法是构造函数(类)自身的方法,不需要实例化就能调用,如Product.getInfo();静态属性语法还处于提案阶段。
示例三:Class 实现继承(extends + super)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Class 继承</title>
</head>
<body>
<script>
class Product {
// 子类要改,就不加 # 私有
name;
price;
constructor(name, price) {
this.name = name;
this.price = Math.max(price, 100);
}
buy(count) {
console.log(`购买了${count}件 ${this.name},单价${this.price}`);
}
}
class Phone extends Product {
color = '白色';
// 重写构造器方法,必须先 super
constructor(name, price, color) {
super(name, price); // 调用父类构造器,必须写在最前面
this.color = color;
}
// 与父类同名 —— 重写(覆盖)
buy(count) {
console.log(`购买了${count}件手机 ${this.name},单价${this.price}`);
}
discount(rate) {
this.price = this.price * rate;
}
}
const p = new Phone('华为P50', 89.9, '红色');
p.buy(12); // 购买了12件手机 华为P50,单价100(89.9 被抬到 100)
console.log(p.color); // 红色
console.log(p instanceof Product); // true
console.log(Object.getPrototypeOf(Phone.prototype) === Product.prototype); // true
p.discount(0.8);
p.buy(1); // 购买了1件手机 华为P50,单价80
</script>
</body>
</html>要点解释与易错点
extends自动完成了以前手写的两件事:子类实例原型指向父类实例、原型上 constructor 指回子类。- 子类重写构造器时必须调用
super(),且必须放在构造器第一行,否则报错;作用是“把父类构造器执行一次”。 - 不重写构造器时,父类构造器会自动执行,可以省略
super。 super作为函数只能在子类构造器中使用;写在普通方法里会报错(它和对象方法里的super不是一回事)。- 子类中定义同名属性 / 方法会覆盖父类继承来的;父类的私有属性
#xxx子类只能继承、不能修改,需要子类改的属性就别加#。 - 一个子类只能继承一个父类,一个父类可以被多个子类继承。
常见错误
| 现象 | 原因 | 修法 |
|---|---|---|
new Symbol('id') 报 TypeError | Symbol 只能调用,不能实例化 | 改为 Symbol('id') |
obj.s1 = 1 后以为写入了 Symbol 键,实际得到字符串键 "s1" | 点语法后的名字按静态字符串处理,不取变量值 | 用计算属性:obj[s1] = 1 |
用新的 Symbol('id') 读取已有 Symbol 属性得到 undefined | 每次 Symbol() 都创建唯一值 | 保存首次创建的 Symbol 引用,并用同一引用读写 |
class Product {} 后直接 Product() 报错 | class 定义的类只能 new,不能当普通函数调用 | 改为 new Product() |
在 class 体内写 console.log(...) 报语法错误 | 类体只能定义属性和方法,不能写普通语句 | 把语句放进构造器或方法中 |
子类重写 constructor 未先调用 super() 报错 | 子类构造器必须先执行父类构造器 | 在子类构造器首行调用 super(...) |
用实例调用静态方法报 not a function | 静态方法属于类本身,不属于实例 | 用 类名.方法() 调用 |
把 ?? 当 ` | 用,导致0、''` 被默认值替换 |
练习
创建两个
Symbol('id'),比较是否相等;再用其中一个作为对象属性名并读取,说明为什么用新的Symbol('id')读不到。- 要点:唯一性、保存引用、计算属性。
用
class Product定义name和私有#price,构造器中保证#price >= 100,方法buy(n)返回总价;外部直接读取#price会怎样?- 要点:私有字段声明位置、类内访问、外部访问边界。
定义
class Phone extends Product,增加color,重写constructor并正确调用super,再重写buy覆盖父类方法;说明子类能否直接修改父类#price。- 要点:
super位置、方法覆盖、私有字段修改权。
- 要点:
比较
config?.timeout ?? 3000与config.timeout || 3000在config = { timeout: 0 }时的结果,并说明应选哪一个。- 要点:
??与||的触发条件不同。
- 要点:
改错:
class A { console.log('x'); static s(){} } new A(); A.s(); A();- 要点:类体语句位置、静态方法调用、类不能直接调用。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 新增原始类型-Symbol | 19:43 |
| 2 | class语法定义类 | 15:31 |
| 3 | 构造器方法 | 10:31 |
| 4 | 私有属性 | 07:12 |
| 5 | 静态方法 | 06:18 |
| 6 | class实现继承 | 22:38 |
小结
- Symbol 的唯一性只有在保存并复用同一引用时才有实际读写价值。
- 对象属性名的动态写入必须用计算属性,点语法和字面量裸名不会取变量值。
class是构造函数的语法层封装,调用限制和原型布置由语法自动完成。- 子类重写构造器时,
super是前置条件,不是可选优化。 - 私有字段的边界是声明类;继承不自动授予子类修改权。
- 静态成员属于类对象,实例不是其调用入口。
- 新增运算符要按“空值判断、短路、赋值目标”区分,不能凭感觉互换。