一文总结:内置 Symbols、defineProperty、对象隐式类型转换
1.派生类的构造方法对于 filter 方法的影响:引出 Symbol.species
题目:
jsclass 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
构造函数。
根据这段代码的执行,以下是预期的结果:
arr.constructor === Array
将返回true
,因为arr
是由Array
构造函数创建的。- 但是,
arr.func()
将抛出一个错误,因为arr
是由Array
构造函数创建的,而Array
构造函数没有名为func
的方法。这会导致类似于 "TypeError: arr.func is not a function" 的错误。
注意:代码中的 cls
类继承自 Array
并定义了一个静态 [Symbol.species]
getter 方法,这是用来配置当派生类的实例进行类似 map
、filter
等操作时应返回的构造函数。在这里,它被设置为返回 Array
构造函数,所以 filter
操作返回的是一个普通的 Array
。
如果没有[Symbol.species]方法呢?
派生类的构造函数是指从一个基类(或父类)继承属性和方法的子类(或派生类)所使用的构造函数。在面向对象编程中,基类是一个通用的类,而派生类则通过继承基类的特性来扩展或修改其行为。
如果没有定义 [Symbol.species]
方法,那么默认情况下派生类的实例方法(如 filter
、map
、slice
等)将返回与派生类相同的类。也就是说,如果派生类是通过继承 Array
创建的,那么这些实例方法将返回新的派生类的实例。
在你的代码示例中,cls
类继承自 Array
,但没有定义 [Symbol.species]
方法。因此,filter
方法返回的将是 cls
类的实例,而不是 Array
实例。
这意味着以下两点:
arr.constructor === cls
将返回true
,因为arr
是由cls
构造函数创建的。arr.func()
将成功调用,并在控制台上打印出1
,因为arr
是cls
的实例,而cls
类定义了一个名为func
的方法。
所以,如果没有定义 [Symbol.species]
方法,代码的输出将是:true 1
2.[Symbol.species]简介?这一系列的 Symbol 方法都有什么?
[Symbol.species]
是 JavaScript 中的一个内置的 Symbol(符号)属性。它用于定义派生类(子类)在执行诸如 map
、filter
、slice
等操作时返回的构造函数。这个属性可以被用来改变派生类默认返回的构造函数,从而影响操作结果的类型。
当在派生类中实现了 [Symbol.species]
方法时,该方法将被用于确定执行操作后返回的对象类型。如果没有定义 [Symbol.species]
,则会默认使用派生类的构造函数来创建新的对象。
在 ES6 之后,JavaScript 引入了一系列 Symbol 属性,它们用于在对象中定义一些特殊行为。以下是一些常见的 Symbol 属性和其作用:
[Symbol.iterator]
: 用于定义对象的默认迭代器方法,使对象可以通过for...of
循环进行迭代。[Symbol.toPrimitive]
: 定义对象转换为原始值时的行为,通过调用对象的valueOf()
和toString()
方法。[Symbol.toStringTag]
: 用于自定义对象在调用Object.prototype.toString()
方法时返回的字符串标签。[Symbol.hasInstance]
: 用于定义一个对象作为一个构造函数的实例的判定规则,即确定对象是否为某构造函数的实例。[Symbol.isConcatSpreadable]
: 定义一个对象在使用Array.prototype.concat()
方法时是否可以被展开拼接。[Symbol.species]
: 定义派生类在执行类似map
、filter
、slice
操作时返回的构造函数。[Symbol.match]
,[Symbol.replace]
,[Symbol.search]
,[Symbol.split]
: 用于定制字符串的正则匹配、替换、搜索和拆分行为。[Symbol.unscopables]
: 用于定义哪些属性在使用 with 语句时应该被排除在作用域之外。
这些 Symbol 属性可以用来扩展 JavaScript 对象的行为,从而更好地适应特定的用途和场景。每个 Symbol 属性都具有独特的目的和用法。
3.解决让x==1&&x==2
为 true:使用 Symbol.toPrimitive
1.因为==会隐式类型转换,所以 x=true 的时候就可以!
2.给对象定义 Symbol.toPrimitive(ES6)
toPrimitive
操作在 JavaScript 中用于隐式类型转换的过程
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 对象这个作用域里面,对象的属性随便用!
// 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)
需要注意以下几点:
- 访问器属性不具备可写性(即不能直接赋值修改)。如果你想同时定义读取和写入操作,你需要使用
get
和set
两个访问器。 - 访问器属性的值是通过访问属性时计算得出的,而不是存储在对象中的。
- 访问器属性通常用于创建虚拟属性,也就是那些不能从对象内部存储的属性。
- 访问器属性和数据属性(普通属性)可以共存在一个对象中。
2.globalThis + 元编程
这个在严格模式下也可以使用!
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
的示例:
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
的示例:
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.");
}
}
注意以下几点区别:
Reflect.defineProperty
的返回值在成功定义属性时是true
,而Object.defineProperty
没有直接返回值。- 当试图修改
'prop'
属性的值时,Object.defineProperty
抛出了一个TypeError
错误,而Reflect.defineProperty
返回了false
。 - 使用
Reflect.defineProperty
时,你可以通过检查返回值来处理属性定义是否成功,从而避免抛出错误。
总之,Reflect.defineProperty
提供了更加一致且易于处理错误的属性定义方法。在现代 JavaScript 中,它是推荐的选择。
5.对象的隐式类型转换
哪些情况下对象会触发默认转换,并被转化为'[object Object]'这样的字符串呢?——> 这里算是一个大总结
加法操作: 当对象与字符串或数字相加时,会触发对象到字符串的转换。
js0 + {}; //'0[object Object]'
比较操作: 在使用相等运算符或严格相等运算符(===)比较对象和其他类型时,会触发对象到原始值的转换。
js{} == 2 //false,{}被转化为'[object Object]'
**作为对象属性:**把对象用作属性名的时候,也会进行隐式类型转换。
jsconst x = { a:1; } const y = {}; y[x] = 10; //x被转化为'[object Object]'
默认情况下,对象的默认转换会调用 valueOf()
方法,然后再调用 toString()
方法。如果对象的 valueOf()
方法并没有返回我们期望的类型的值,那么 JavaScript 会调用对象的 toString()
方法,将对象转换为字符串'[object Object]',然后再尝试将字符串转换为数字。