Skip to content

新增的运算符与symbol 类型(Day3 2/4) ​

本页对应 Day03_ES6,共 6 段视频,约 1 小时 21 分。

学习目标 ​

  1. 能创建 Symbol 类型数据,使用 typeof 判断其类型,并说明其唯一性及作为对象属性名时的读写方式。
  2. 能使用 class 关键字定义类,说明其本质仍是构造函数,并验证该类只能实例化、不能直接调用。
  3. 能在 class 中定义属性和方法,区分直接赋值属性添加到实例、简写方法添加到原型上的行为。
  4. 能编写 constructor 构造器方法,在实例化时自动执行并完成属性初始化、参数处理和初始值计算。
  5. 能声明并使用私有属性(#),说明私有属性需提前声明、类外不可访问,并能定义和调用静态方法。
  6. 能使用 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 的“语法糖”与底层构造函数机制对应起来。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例一: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') 报 TypeErrorSymbol 只能调用,不能实例化改为 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、''` 被默认值替换

练习 ​

  1. 创建两个 Symbol('id'),比较是否相等;再用其中一个作为对象属性名并读取,说明为什么用新的 Symbol('id') 读不到。

    • 要点:唯一性、保存引用、计算属性。
  2. 用 class Product 定义 name 和私有 #price,构造器中保证 #price >= 100,方法 buy(n) 返回总价;外部直接读取 #price 会怎样?

    • 要点:私有字段声明位置、类内访问、外部访问边界。
  3. 定义 class Phone extends Product,增加 color,重写 constructor 并正确调用 super,再重写 buy 覆盖父类方法;说明子类能否直接修改父类 #price。

    • 要点:super 位置、方法覆盖、私有字段修改权。
  4. 比较 config?.timeout ?? 3000 与 config.timeout || 3000 在 config = { timeout: 0 } 时的结果,并说明应选哪一个。

    • 要点:?? 与 || 的触发条件不同。
  5. 改错:class A { console.log('x'); static s(){} } new A(); A.s(); A();

    • 要点:类体语句位置、静态方法调用、类不能直接调用。

原视频对照 ​

#视频时长
1新增原始类型-Symbol19:43
2class语法定义类15:31
3构造器方法10:31
4私有属性07:12
5静态方法06:18
6class实现继承22:38

小结 ​

  1. Symbol 的唯一性只有在保存并复用同一引用时才有实际读写价值。
  2. 对象属性名的动态写入必须用计算属性,点语法和字面量裸名不会取变量值。
  3. class 是构造函数的语法层封装,调用限制和原型布置由语法自动完成。
  4. 子类重写构造器时,super 是前置条件,不是可选优化。
  5. 私有字段的边界是声明类;继承不自动授予子类修改权。
  6. 静态成员属于类对象,实例不是其调用入口。
  7. 新增运算符要按“空值判断、短路、赋值目标”区分,不能凭感觉互换。