JavaScript程序设计
温馨提示,Symbol类型这段较长,不必追求一次性看懂
2.3.7 Symbol 类型
Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的(尤其是因为Object API 提供了方法,可以更方便地发现符号属性)。相反,符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。
-
符号的基本用法
符号需要使用
Symbol()函数初始化。因为符号本身是原始类型,所以typeof操作符对符号返回symbol。let sym = Symbol(); console.log(typeof sym); // symbol // 调用 Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description) // 将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关 let genericSymbol = Symbol(); let otherGenericSymbol = Symbol(); let fooSymbol = Symbol('foo'); let otherFooSymbol = Symbol('foo'); console.log(genericSymbol == otherGenericSymbol); // false console.log(fooSymbol == otherFooSymbol); // false符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建 Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。
let genericSymbol = Symbol(); console.log(genericSymbol); // Symbol() let fooSymbol = Symbol('foo'); console.log(fooSymbol); // Symbol(foo);最重要的是,Symbol()函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象,像使用 Boolean、String 或 Number 那样,它们都支持构造函数且可用于初始化包含原始值的包装对象:
let myBoolean = new Boolean(); console.log(typeof myBoolean); // "object" let myString = new String(); console.log(typeof myString); // "object" let myNumber = new Number(); console.log(typeof myNumber); // "object" let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor如果你确实想使用符号包装对象,可以借用 Object()函数:
let mySymbol = Symbol(); let myWrappedSymbol = Object(mySymbol); console.log(typeof myWrappedSymbol); // "object" -
使用全局符号注册表
如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。为此,需要使用
Symbol.for()方法。let fooGlobalSymbol = Symbol.for('foo'); console.log(typeof fooGlobalSymbol); // symbolSymbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号 let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号 console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol()定义的符号也并不等同:
let localSymbol = Symbol('foo'); let globalSymbol = Symbol.for('foo'); console.log(localSymbol === globalSymbol); // false全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。
let emptyGlobalSymbol = Symbol.for(); console.log(emptyGlobalSymbol); // Symbol(undefined)还可以使用
Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回 undefined。// 创建全局符号 let s = Symbol.for('foo'); console.log(Symbol.keyFor(s)); // foo // 创建普通符号 let s2 = Symbol('bar'); console.log(Symbol.keyFor(s2)); // undefined如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError:
Symbol.keyFor(123); // TypeError: 123 is not a symbol -
使用符号作为属性
凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性
Object.defineProperty()或Object.defineProperties()定义的属性。对象字面量只能在计算属性语法中使用符号作为属性。
let s1 = Symbol('foo'), s2 = Symbol('bar'), s3 = Symbol('baz'), s4 = Symbol('qux'); let o = { // 此处使用的语法为计算属性名(Computed Property Name) // 在ES6中,对象字面量允许使用方括号来包裹表达式,计算结果作为属性名。 // 这里[s1]表示使用变量s1的值(一个Symbol)作为属性名,对应的值为'foo val'。 [s1]: 'foo val' }; // 这样也可以:o[s1] = 'foo val'; console.log(o); // {Symbol(foo): foo val} Object.defineProperty(o, s2, {value: 'bar val'}); console.log(o); // {Symbol(foo): foo val, Symbol(bar): bar val} Object.defineProperties(o, { [s3]: {value: 'baz val'}, [s4]: {value: 'qux val'} }); console.log(o); // {Symbol(foo): foo val, Symbol(bar): bar val, // Symbol(baz): baz val, Symbol(qux): qux val}这里来讲讲
Object.defineProperty和Object.defineProperties两个方法:-
Object.defineProperty// 基本语法: Object.defineProperty(obj, property, descriptor) // 对于上例中的代码 Object.defineProperty(o, s2, { value: 'bar val' }); // 这行代码的意思是: // 给对象 o // 添加一个属性,属性名为 Symbol s2 // 属性描述符为 { value: 'bar val' }属性描述符:
属性描述符有两种类型:数据描述符 和 存取描述符
-
数据描述符
// 数据描述符(常用) Object.defineProperty(o, s2, { value: 'bar val', // 属性值 writable: true, // 是否可修改(默认false) enumerable: true, // 是否可枚举(默认false) configurable: true // 是否可删除、可修改描述符(默认false) }); -
存取描述符(getter/setter)
// 这里的get(), set() Object.defineProperty(o, s2, { get() { return this._internalValue; }, set(newValue) { this._internalValue = newValue; }, enumerable: true, configurable: true });
-
-
Object.defineProperties// 基本语法 Object.defineProperties(obj, propertiesObject) // 对于上例中的代码 Object.defineProperties(o, { [s3]: { value: 'baz val' }, [s4]: { value: 'qux val' } }); // 实际上就是一次性添加多个属性 -
与普通属性赋值的区别
普通赋值(宽松)
// 属性默认:可写、可枚举、可配置 o.regularProp = 'value'; o[s1] = 'value';defineProperty(精确控制)
// 属性默认:不可写、不可枚举、不可配置(除非显式指定) Object.defineProperty(o, s1, { value: 'value' });
类似于
Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnPropertySymbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键:let s1 = Symbol('foo'), s2 = Symbol('bar'); let o = { [s1]: 'foo val', [s2]: 'bar val', baz: 'baz val', qux: 'qux val' }; console.log(Object.getOwnPropertySymbols(o)); // [Symbol(foo), Symbol(bar)] console.log(Object.getOwnPropertyNames(o)); // ["baz", "qux"] console.log(Object.getOwnPropertyDescriptors(o)); // {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}} console.log(Reflect.ownKeys(o)); // ["baz", "qux", Symbol(foo), Symbol(bar)]因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:
let o = { [Symbol('foo')]: 'foo val', [Symbol('bar')]: 'bar val' }; console.log(o); // {Symbol(foo): "foo val", Symbol(bar): "bar val"} let barSymbol = Object.getOwnPropertySymbols(o) .find((symbol) => symbol.toString().match(/bar/)); console.log(barSymbol); // Symbol(bar) -
-
常用内置符号
ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。
这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道
for-of循环会在相关对象上使用Symbol.iterator属性,那么就可以通过在自定义对象上重新定义Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。// 假设我们有一个对象,我们希望它能够被 for...of 循环遍历,我们可以这样做: // 定义一个对象,并实现 Symbol.iterator 方法。 // 该方法必须返回一个迭代器对象,这个迭代器对象有一个 next 方法。 // next 方法返回一个包含 value 和 done 属性的对象。 let myIterableObject = { data: [1, 2, 3, 4, 5], // 实现 Symbol.iterator 方法 [Symbol.iterator]: function() { let index = 0; let data = this.data; // 返回一个迭代器对象 return { next: function() { if (index < data.length) { return { value: data[index++], done: false }; } else { return { value: undefined, done: true }; } } }; } }; // 现在可以用 for...of 循环遍历这个对象了 for (let item of myIterableObject) { console.log(item); } // 输出:1, 2, 3, 4, 5这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。
Note: 在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator 指的就是 Symbol.iterator。
-
Symbol.asyncIterator根据 ECMAScript 规范,这个符号作为一个属性表示“**一个方法,该方法返回对象默认的
AsyncIterator。**由for-await-of语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数。for-await-of循环会利用这个函数执行异步迭代操作。循环时,它们会调用以Symbol.asyncIterator为键的函数,并期望这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API的AsyncGenerator:class Foo { // async - 异步函数标识,表示这个函数是异步的,可以在函数内部使用 await,函数总是返回一个 Promise // * - 生成器函数标识,表示这是一个生成器函数,可以在函数内部使用 yield,函数返回一个 Generator 对象 // [Symbol.asyncIterator] - 计算属性名,使用 Symbol.asyncIterator 作为方法名 // () {} - 方法定义,普通的函数参数列表和方法体 async *[Symbol.asyncIterator]() {} } let f = new Foo(); console.log(f[Symbol.asyncIterator]()); // AsyncGenerator {<suspended>} // 关于生成器函数:生成器函数是一种特殊的函数,它可以暂停执行并在之后恢复执行。与普通函数一旦调用就必须执行到底不同,生成器函数可以在执行过程中多次暂停和继续。 function* demoGenerator() { console.log("第1步"); const result1 = yield "暂停点1"; console.log("第2步,收到:", result1); const result2 = yield "暂停点2"; console.log("第3步,收到:", result2); return "完成"; } const generator = demoGenerator(); // 第一次调用 next() console.log("调用第1次 next():"); let step1 = generator.next(); console.log("返回:", step1); // 输出: // 第1步 // 返回值: { value: "暂停点1", done: false } // 第二次调用 next(),并传入值 console.log("\n调用第2次 next('值1'):"); let step2 = generator.next("值1"); console.log("返回:", step2); // 输出: // 第2步,收到: 值1 // 返回值: { value: "暂停点2", done: false } // 第三次调用 next(),并传入值 console.log("\n调用第3次 next('值2'):"); let step3 = generator.next("值2"); console.log("返回:", step3); // 输出: // 第3步,收到: 值2 // 返回值: { value: "完成", done: true } /* 生成器函数的特点:使用 function* 声明,可以多次暂停和恢复执行,返回一个生成器对象(也是迭代器) yield 关键字的作用: 1.暂停执行:函数在执行到 yield 时暂停 2.返回值:向调用者返回 yield 后面的值 3.接收值:当恢复执行时,接收通过 next() 传入的值 4.双向通信:既是数据的生产者,也是数据的消费者 */技术上,这个由
Symbol.asyncIterator函数生成的对象应该通过其next()方法陆续返回Promise实例。可以通过显式地调用next()方法返回,也可以隐式地通过异步生成器函数返回:class Emitter { constructor(max) { this.max = max; this.asyncIdx = 0; } async *[Symbol.asyncIterator]() { while(this.asyncIdx < this.max) { yield new Promise((resolve) => resolve(this.asyncIdx++)); } } } async function asyncCount() { let emitter = new Emitter(5); for await(const x of emitter) { console.log(x); } } asyncCount(); // 0 // 1 // 2 // 3 // 4Note: Symbol.asyncIterator 是 ES2018 规范定义的,因此只有版本非常新的浏览器支持它。
-
Symbol.hasInstance根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由
instanceof操作符使用”。instanceof操作符可以用来确定一个对象实例的原型链上是否有原型。instanceof的典型使用场景如下:function Foo() {} let f = new Foo(); console.log(f instanceof Foo); // true class Bar {} let b = new Bar(); console.log(b instanceof Bar); // true在 ES6 中,
instanceof操作符会使用Symbol.hasInstance函数来确定关系。以Symbol.hasInstance为键的函数会执行同样的操作,只是操作数对调了一下:function Foo() {} let f = new Foo(); console.log(Foo[Symbol.hasInstance](f)); // true class Bar {} let b = new Bar(); console.log(Bar[Symbol.hasInstance](b)); // true这个属性定义在
Function的原型上,因此默认在所有函数和类上都可以调用。由于 instanceof操作符会在**原型链(此处将会在后面详细解释,对这里有个印象即可)**上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通过静态方法重新定义这个函数:class Bar {} class Baz extends Bar { static [Symbol.hasInstance]() { return false; } } let b = new Baz(); console.log(Bar[Symbol.hasInstance](b)); // true console.log(b instanceof Bar); // true console.log(Baz[Symbol.hasInstance](b)); // false console.log(b instanceof Baz); // false -
Symbol.isConcatSpreadable根据 ECMAScript 规范,这个符号作为一个属性表示“一个布尔值,如果是
true,则意味着对象应该用Array.prototype.concat()打平其数组元素("打平"指的是将嵌套的数组结构展开成一维数组的过程。)”。ES6 中的Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖Symbol.isConcatSpreadable的值可以修改这个行为。数组对象默认情况下会被打平到已有的数组,false 或假值会导致整个对象被追加到数组末尾。类数组对象默认情况下会被追加到数组末尾,true 或真值会导致这个类数组对象被打平到数组实例。其他不是类数组对象的对象在 Symbol.isConcatSpreadable 被设置为 true 的情况下将被忽略!!!。
let arr1 = [1, 2]; let arr2 = [3, 4]; // 1.数组默认会被"打平" let result = arr1.concat(arr2); console.log(result); // [1, 2, 3, 4] - arr2 被展开 // 查看数组的 Symbol.isConcatSpreadable console.log(arr2[Symbol.isConcatSpreadable]); // undefined,但数组默认行为相当于 true // 2.阻止数组被"打平" arr2[Symbol.isConcatSpreadable] = false; let result = arr1.concat(arr2); console.log(result); // [1, 2, [3, 4]] - arr2 作为整体添加 // 3.类数组对象的处理 // 创建一个类数组对象 let arrayLike = { 0: 'a', 1: 'b', 2: 'c', length: 3 }; let arr = [1, 2]; // 类数组对象默认不会被展开 let result1 = arr.concat(arrayLike); console.log(result1); // [1, 2, {0: 'a', 1: 'b', 2: 'c', length: 3}] // 设置 Symbol.isConcatSpreadable 为 true arrayLike[Symbol.isConcatSpreadable] = true; let result2 = arr.concat(arrayLike); console.log(result2); // [1, 2, 'a', 'b', 'c'] - 类数组被展开了! // 4.普通对象 let initial = ["foo"]; let obj = {age: 24}; // 普通对象 let set = new Set(["x", "y", "z"]); // 普通对象 console.log(initial.concat(obj)); // ['foo', {...}] console.log(initial.concat(set)); // ['foo', Set(3)] // 对普通对象设置Symbol.isConcatSpreadable为true obj[Symbol.isConcatSpreadable] = true; set[Symbol.isConcatSpreadable] = true; console.log(initial.concat(obj)); // ['foo'] - obj被忽略! console.log(initial.concat(set)); // ['foo'] - set被忽略! // 再比如下面这个例子: let config = { debug: true, version: '1.0' }; let user = { name: 'John', age: 30 }; let uniqueItems = new Set([1, 2, 3]); config[Symbol.isConcatSpreadable] = true; user[Symbol.isConcatSpreadable] = true; uniqueItems[Symbol.isConcatSpreadable] = true; let result = [].concat(config, user, uniqueItems); console.log(result); // 输出:[] - config,user,uniqueItems都被忽略 -
Symbol.iterator根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由
for-of语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。for-of循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以Symbol.iterator为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API的 Generator:// 类似于Symbol.asyncIterator,只不过Symbol.iterator是同步的 class Foo { *[Symbol.iterator]() {} } let f = new Foo(); console.log(f[Symbol.iterator]()); // Generator {<suspended>}技术上,这个由 Symbol.iterator 函数生成的对象应该通过其 next()方法陆续返回值。可以通过显式地调用 next()方法返回,也可以隐式地通过生成器函数返回:
class Emitter { constructor(max) { this.max = max; this.idx = 0; } *[Symbol.iterator]() { while(this.idx < this.max) { yield this.idx++; } } } function count() { let emitter = new Emitter(5); for (const x of emitter) { console.log(x); } } count(); // 0 // 1 // 2 // 3 // 4 -
Symbol.match根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由
String.prototype.match()方法使用”。String.prototype.match()方法会使用以Symbol.match为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:console.log(RegExp.prototype[Symbol.match]); // ƒ [Symbol.match]() { [native code] },即正则表达式的原型上默认有这个函数的定义 // 正常用法:用正则表达式匹配字符串 let result = 'hello world'.match(/world/); console.log(result); // ["world", index: 6, input: "hello world", groups: undefined]给这个方法传入非正则表达式值会导致该值被转换为
RegExp对象。如果想改变这种行为,让方法直接使用参数,则可以重新定义 Symbol.match 函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。Symbol.match 函数接收一个参数,就是调用 match()方法的字符串实例。返回的值没有限制:class StringMatcher { constructor(str) { this.str = str; } [Symbol.match](target) { return target.includes(this.str); } } console.log('foobar'.match(new StringMatcher('foo'))); // true console.log('barbaz'.match(new StringMatcher('qux'))); // false // 简单的邮箱验证 class EmailValidator { [Symbol.match](target) { // 简单的邮箱验证 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(target); } } const validator = new EmailValidator(); console.log("test@example.com".match(validator)); // true console.log("invalid-email".match(validator)); // false -
Symbol.replace根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由
String.prototype.replace()方法使用”。String.prototype.replace()方法会使用以Symbol.replace为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:console.log(RegExp.prototype[Symbol.replace]); // ƒ [Symbol.replace]() { [native code] } ,即正则表达式的原型上默认有这个函数的定义 console.log('foobarbaz'.replace(/bar/, 'qux')); // 'fooquxbaz'给这个方法传入非正则表达式值会导致该值被转换为
RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.replace函数以取代默认对正则表达式求值的行为,从而让replace()方法使用非正则表达式实例。Symbol.replace函数接收两个参数,即调用replace()方法的字符串实例和替换字符串。返回的值没有限制:class StringReplacer { constructor(str) { this.str = str; } [Symbol.replace](target, replacement) { return target.split(this.str).join(replacement); } } console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux')); // "barquxbaz" -
Symbol.search根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由
String.prototype.search()方法使用”。String.prototype.search()方法会使用以Symbol.search为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:console.log(RegExp.prototype[Symbol.search]); // ƒ [Symbol.search]() { [native code] } ,即正则表达式的原型上默认有这个函数的定义 console.log('foobar'.search(/bar/)); // 3给这个方法传入非正则表达式值会导致该值被转换为
RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.search函数以取代默认对正则表达式求值的行为,从而让search()方法使用非正则表达式实例。Symbol.search函数接收一个参数,就是调用match()方法的字符串实例。返回的值没有限制:class StringSearcher { constructor(str) { this.str = str; } [Symbol.search](target) { return target.indexOf(this.str); } } console.log('foobar'.search(new StringSearcher('foo'))); // 0 console.log('barfoo'.search(new StringSearcher('foo'))); // 3 console.log('barbaz'.search(new StringSearcher('qux'))); // -1 -
Symbol.species根据 ECMAScript 规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象(派生对象指的是通过某个方法从现有对象创建的新对象)的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义:
// 派生对象:通过某个方法从现有对象创建的新对象 let arr1 = [1, 2, 3]; let arr2 = arr1.concat(4, 5); // arr2 是 arr1 的派生对象 let arr3 = arr1.map(x => x * 2); // arr3 也是派生对象 // 当你继承内置类(如 Array)时,派生对象的类型可能会出问题: class MyArray extends Array {} let myArr = new MyArray(1, 2, 3); let result = myArr.concat(4, 5); // 应该返回 MyArray 还是普通 Array? // Symbol.species 就是用来指定创建派生对象时应该使用哪个构造函数。 // 如果没有定义 Symbol.species,默认返回 this(当前构造函数) class Bar extends Array {} // 不使用Symbol.species class Baz extends Array { static get [Symbol.species]() { return Array; // 使用Symbol.species,明确指定创建派生对象时使用 Array,而不是 Baz } } let bar = new Bar(); console.log(bar instanceof Array); // true - Bar 继承自 Array console.log(bar instanceof Bar); // true - bar 是 Bar 的实例 bar = bar.concat('bar'); // 调用 concat 创建新数组 console.log(bar instanceof Array); // true - 新数组是 Array console.log(bar instanceof Bar); // true - 同时新数组也是 Bar 的实例 let baz = new Baz(); console.log(baz instanceof Array); // true - Baz 继承自 Array console.log(baz instanceof Baz); // true - baz 是 Baz 的实例 baz = baz.concat('baz'); console.log(baz instanceof Array); // true - 新数组是 Array console.log(baz instanceof Baz); // false - 新数组不是 Baz 的实例! -
Symbol.split根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由
String.prototype.split()方法使用”。String.prototype. split()方法会使用以Symbol.split为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个String方法的有效参数:console.log(RegExp.prototype[Symbol.split]); // ƒ [Symbol.split]() { [native code] } ,即正则表达式的原型上默认有这个函数的定义 console.log('foobarbaz'.split(/bar/)); // ['foo', 'baz']给这个方法传入非正则表达式值会导致该值被转换为
RegExp对象。如果想改变这种行为,让方法直接使用参数,可以重新定义Symbol.split函数以取代默认对正则表达式求值的行为,从而让split()方法使用非正则表达式实例。Symbol.split函数接收一个参数,就是调用match()方法的字符串实例。返回的值没有限制:class StringSplitter { constructor(str) { this.str = str; } [Symbol.split](target) { return target.split(this.str); } } console.log('barfoobaz'.split(new StringSplitter('foo'))); // ["bar", "baz"] -
Symbol.toPrimitive根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由
ToPrimitive抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的Symbol.toPrimitive属性上定义一个函数可以改变默认行为。根据提供给这个函数的参数(string、number 或 default),可以控制返回的原始值:
class Foo {} let foo = new Foo(); console.log(3 + foo); // "3[object Object]" console.log(3 - foo); // NaN console.log(String(foo)); // "[object Object]" class Bar { constructor() { this[Symbol.toPrimitive] = function(hint) { switch (hint) { case 'number': return 3; case 'string': return 'string bar'; case 'default': default: return 'default bar'; } } } } let bar = new Bar(); console.log(3 + bar); // "3default bar" console.log(3 - bar); // 0 console.log(String(bar)); // "string bar" -
Symbol.toStringTag根据 ECMAScript 规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法
Object.prototype.toString()使用”。通过toString()方法获取对象标识时,会检索由Symbol.toStringTag指定的实例标识符,默认为"Object"。内置类型已经指定了这个值,但自定义类实例还需要明确定义:// 内置类型 let s = new Set(); console.log(s); // Set(0) {} console.log(s.toString()); // [object Set] console.log(s[Symbol.toStringTag]); // Set // 未定义Symbol.toStringTag class Foo {} let foo = new Foo(); console.log(foo); // Foo {} console.log(foo.toString()); // [object Object] console.log(foo[Symbol.toStringTag]); // undefined // 明确定义Symbol.toStringTag class Bar { constructor() { this[Symbol.toStringTag] = 'Bar'; } } let bar = new Bar(); console.log(bar); // Bar {} console.log(bar.toString()); // [object Bar] console.log(bar[Symbol.toStringTag]); // Bar -
Symbol.unscopables根据 ECMAScript 规范,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除”。设置这个符号并让其映射对应属性的键值为 true,就可以阻止该属性出现在
with环境绑定中,如下例所示:let o = { foo: 'bar' }; with (o) { console.log(foo); // bar } o[Symbol.unscopables] = { foo: true }; with (o) { console.log(foo); // ReferenceError }Note: 不推荐使用 with,因此也不推荐使用 Symbol.unscopables。
1万+

被折叠的 条评论
为什么被折叠?



