Skip to content

一文总结:内置 Symbols、defineProperty、对象隐式类型转换

1.派生类的构造方法对于 filter 方法的影响:引出 Symbol.species

题目:

js
class cls extends Array {
  func() {
    console.log(1);
  }
  static get [Symbol.species]() {
    return Array;
  }
}

const son = new cls(1, 2, 3);
const arr = son.filter((item) => item >= 2);
console.log(arr.constructor === Array);
arr.func();

结果是啥?

这段代码是 JavaScript 代码,它创建了一个名为 cls 的类,该类继承自 Array。这个类有一个名为 func 的方法,以及一个静态 getter 方法 [Symbol.species],用于配置派生类的构造函数。在这个特定的代码中,[Symbol.species] 被配置为返回 Array 构造函数。

根据这段代码的执行,以下是预期的结果:

  1. arr.constructor === Array 将返回 true,因为 arr 是由 Array 构造函数创建的。
  2. 但是,arr.func() 将抛出一个错误,因为 arr 是由 Array 构造函数创建的,而 Array 构造函数没有名为 func 的方法。这会导致类似于 "TypeError: arr.func is not a function" 的错误。

注意:代码中的 cls 类继承自 Array 并定义了一个静态 [Symbol.species] getter 方法,这是用来配置当派生类的实例进行类似 mapfilter 等操作时应返回的构造函数。在这里,它被设置为返回 Array 构造函数,所以 filter 操作返回的是一个普通的 Array

如果没有[Symbol.species]方法呢?

派生类的构造函数是指从一个基类(或父类)继承属性和方法的子类(或派生类)所使用的构造函数。在面向对象编程中,基类是一个通用的类,而派生类则通过继承基类的特性来扩展或修改其行为。

如果没有定义 [Symbol.species] 方法,那么默认情况下派生类的实例方法(如 filtermapslice 等)将返回与派生类相同的类。也就是说,如果派生类是通过继承 Array 创建的,那么这些实例方法将返回新的派生类的实例。

在你的代码示例中,cls 类继承自 Array,但没有定义 [Symbol.species] 方法。因此,filter 方法返回的将是 cls 类的实例,而不是 Array 实例。

这意味着以下两点:

  1. arr.constructor === cls 将返回 true,因为 arr 是由 cls 构造函数创建的。
  2. arr.func() 将成功调用,并在控制台上打印出 1,因为 arrcls 的实例,而 cls 类定义了一个名为 func 的方法。

所以,如果没有定义 [Symbol.species] 方法,代码的输出将是:true 1

2.[Symbol.species]简介?这一系列的 Symbol 方法都有什么?

[Symbol.species] 是 JavaScript 中的一个内置的 Symbol(符号)属性。它用于定义派生类(子类)在执行诸如 mapfilterslice 等操作时返回的构造函数。这个属性可以被用来改变派生类默认返回的构造函数,从而影响操作结果的类型。

当在派生类中实现了 [Symbol.species] 方法时,该方法将被用于确定执行操作后返回的对象类型。如果没有定义 [Symbol.species],则会默认使用派生类的构造函数来创建新的对象。

在 ES6 之后,JavaScript 引入了一系列 Symbol 属性,它们用于在对象中定义一些特殊行为。以下是一些常见的 Symbol 属性和其作用:

  1. [Symbol.iterator]: 用于定义对象的默认迭代器方法,使对象可以通过 for...of 循环进行迭代。
  2. [Symbol.toPrimitive]: 定义对象转换为原始值时的行为,通过调用对象的 valueOf()toString() 方法。
  3. [Symbol.toStringTag]: 用于自定义对象在调用 Object.prototype.toString() 方法时返回的字符串标签。
  4. [Symbol.hasInstance]: 用于定义一个对象作为一个构造函数的实例的判定规则,即确定对象是否为某构造函数的实例。
  5. [Symbol.isConcatSpreadable]: 定义一个对象在使用 Array.prototype.concat() 方法时是否可以被展开拼接。
  6. [Symbol.species]: 定义派生类在执行类似 mapfilterslice 操作时返回的构造函数。
  7. [Symbol.match], [Symbol.replace], [Symbol.search], [Symbol.split]: 用于定制字符串的正则匹配、替换、搜索和拆分行为。
  8. [Symbol.unscopables]: 用于定义哪些属性在使用 with 语句时应该被排除在作用域之外。

这些 Symbol 属性可以用来扩展 JavaScript 对象的行为,从而更好地适应特定的用途和场景。每个 Symbol 属性都具有独特的目的和用法。

3.解决让x==1&&x==2为 true:使用 Symbol.toPrimitive

1.因为==会隐式类型转换,所以 x=true 的时候就可以!

2.给对象定义 Symbol.toPrimitive(ES6)

toPrimitive 操作在 JavaScript 中用于隐式类型转换的过程

js
const x = {
  _x: 1,
  [Symbol.toPrimitive]() {
    //因为对象的隐式类型转换是通过valueOf(变成一个值)和toString(变成一个字符串)这两个方法决定的,而toPrimitive是元编程的实现,会决定这两个方法(优先级更高)
    //如果不指定的话对象到数字的隐式类型转换将是 NaN
    return this._x++; //返回我们自定义的值
  },
};
console.log(x == 1 && x == 2); //第一次x为1,第二次x为2

注意:toPrimitive 只适用于隐式类型转换的过程,所以在x===1&&x===2的时候是不会走这个方法的!

3.给对象定义 valueOf(变成一个值)和 toString(变成一个字符串)方法(ES5)

4.如果是让x===1&&x===2为 true 怎么做呢?

1.OOP + with

with 语句的作用:就是不用使用对象 obj 的实例去.就可以访问里面的属性

类似于对象的解构,也就是说在 with 对象这个作用域里面,对象的属性随便用!

js
// OOP + with
let o = {
  _x: 1,
  get x() {
    //存取器属性,前面加了get修饰符也就是说在访问x的时候会走这里!
    return this._x++;
  },
};
with (o) {
  console.log(x === 1 && x === 2);
}

注意:with 语句在严格模式下是不可以使用的!eslint 会报错!

注意:get 是一种对象里面的属性修饰符,用于定义一个对象的属性访问器(getter)

需要注意以下几点:

  1. 访问器属性不具备可写性(即不能直接赋值修改)。如果你想同时定义读取和写入操作,你需要使用 getset 两个访问器。
  2. 访问器属性的值是通过访问属性时计算得出的,而不是存储在对象中的。
  3. 访问器属性通常用于创建虚拟属性,也就是那些不能从对象内部存储的属性。
  4. 访问器属性和数据属性(普通属性)可以共存在一个对象中。

2.globalThis + 元编程

这个在严格模式下也可以使用!

js
let _x = 1;
Reflect.defineProperty(globalThis, "x", {
  //在全局作用域定义一个变量x,通过get去拦截读取的行为
  get: () => _x++,
});
console.log(x === 1 && x === 2);

globalThis?

globalThis 是一个全局对象,它在不同的 JavaScript 环境中可以用来访问全局范围内的属性和方法。然而,globalThis 通常用于浏览器环境、Node.js 环境和其他 JavaScript 运行时环境中,而不是用于定义对象属性。

Reflect.defineProperty 和 Object.defineProperty 有啥区别?

  • Reflect.defineProperty 在 ES6 中引入,它的设计旨在提供一种标准的、更易于操作的方式来定义属性。
  • Object.defineProperty 是从 ES5 开始就存在的方法,在 ES6 之后引入的 Reflect.defineProperty 更加符合 ECMAScript 规范,并且是推荐的属性定义方法。
  • 两者定义时的参数是一样的:参数顺序是 (obj, prop, descriptor),分别是要定义属性的对象、属性名和属性描述符。

使用 Object.defineProperty 的示例:

js
const obj = {};

Object.defineProperty(obj, "prop", {
  value: 42,
  writable: false,
  configurable: false,
});

console.log(obj.prop); // 输出:42

// 试图修改属性值会引发错误
obj.prop = 100; // TypeError: Cannot assign to read only property 'prop' of object

使用 Reflect.defineProperty 的示例:

js
const obj = {};

if (
  Reflect.defineProperty(obj, "prop", {
    value: 42,
    writable: false,
    configurable: false,
  })
) {
  console.log(obj.prop); // 输出:42

  // 试图修改属性值会返回 false
  if (
    !Reflect.defineProperty(obj, "prop", {
      value: 100,
    })
  ) {
    console.log("Property 'prop' could not be modified.");
  }
}

注意以下几点区别:

  1. Reflect.defineProperty 的返回值在成功定义属性时是 true,而 Object.defineProperty 没有直接返回值。
  2. 当试图修改 'prop' 属性的值时,Object.defineProperty 抛出了一个 TypeError 错误,而 Reflect.defineProperty 返回了 false
  3. 使用 Reflect.defineProperty 时,你可以通过检查返回值来处理属性定义是否成功,从而避免抛出错误。

总之,Reflect.defineProperty 提供了更加一致且易于处理错误的属性定义方法。在现代 JavaScript 中,它是推荐的选择。

5.对象的隐式类型转换

哪些情况下对象会触发默认转换,并被转化为'[object Object]'这样的字符串呢?——> 这里算是一个大总结

  1. 加法操作: 当对象与字符串或数字相加时,会触发对象到字符串的转换

    js
    0 + {}; //'0[object Object]'
  2. 比较操作: 在使用相等运算符或严格相等运算符(===)比较对象和其他类型时,会触发对象到原始值的转换。

    js
    {} == 2 //false,{}被转化为'[object Object]'
  3. **作为对象属性:**把对象用作属性名的时候,也会进行隐式类型转换。

    js
    const x = {
        a:1;
    }
    const y = {};
    y[x] = 10; //x被转化为'[object Object]'

默认情况下,对象的默认转换会调用 valueOf() 方法,然后再调用 toString() 方法。如果对象的 valueOf() 方法并没有返回我们期望的类型的值,那么 JavaScript 会调用对象的 toString() 方法,将对象转换为字符串'[object Object]',然后再尝试将字符串转换为数字。