)
教程前端【免费下载链接】es6tutorial《ECMAScript 6入门》是一本开源的 JavaScript 语言教程全面介绍 ECMAScript 6 新增的语法特性。项目地址https://gitcode.com/gh_mirrors/es/es6tutorial点击查看免费下载本文基于开源教程《ECMAScript 6 入门》中的 docs/symbol.md 展开系统讲解 ES6 引入的第七种原始数据类型 Symbol从基本语法、属性名应用、遍历规则、全局注册机制到Symbol.hasInstance、Symbol.iterator、Symbol.toPrimitive等 11 个内置 Symbol 值的作用与底层调用原理。读完本文你将掌握用 Symbol 消除属性名冲突、构造不可枚举的内部方法、实现跨模块 Singleton以及通过自定义内置 Symbol 方法改写instanceof、concat、match等语言内置行为。概述为什么需要 Symbol在 ES5 中对象的属性名都是字符串。字符串属性名带来的直接问题是属性名冲突当你基于他人提供的对象扩展新方法例如 mixin 模式时新方法的名字可能与已有方法重名导致覆盖。如果有一种机制能保证每个属性名独一无二就能从根源上防止这种冲突——这就是 ES6 引入Symbol的原因。ES6 新增了一种原始数据类型Symbol表示独一无二的值。加上它之后JavaScript 一共有 7 种数据类型undefined、null、布尔值Boolean、字符串String、数值Number、Symbol以及 ES2015 之后的BigInt大整数和Object对象。也就是说对象的属性名现在可以有两种类型原本的字符串和新增的 Symbol 类型。凡是用 Symbol 类型作属性名的都独一无二不会与其他属性名冲突。let s Symbol(); typeof s // symboltypeof运算符的结果说明变量s是独立的symbol数据类型而不是字符串等类型。不能用 new 调用注意Symbol()函数前不能使用new命令否则会报错。因为生成的 Symbol 是一个原始类型的值不是对象所以不能调用new也不能给它添加属性。本质上它是一种类似于字符串的数据类型。new Symbol(); // TypeError: Symbol is not a constructor参数对 Symbol 的描述Symbol()函数可以接受一个字符串作为参数表示对 Symbol 实例的描述主要用于在控制台显示或转成字符串时区分不同的值。let s1 Symbol(foo); let s2 Symbol(bar); s1 // Symbol(foo) s2 // Symbol(bar) s1.toString() // Symbol(foo) s2.toString() // Symbol(bar)不加参数时所有 Symbol 在控制台输出都是Symbol()难以区分加了参数就如同打上了标签。如果参数是一个对象会先调用该对象的toString()方法将其转为字符串再生成 Symbol 值const obj { toString() { return abc; } }; const sym Symbol(obj); sym // Symbol(abc)关键点描述只是说明性的不影响唯一性。参数相同返回值也不相等——调用 100 次Symbol()会得到 100 个互不相等的值。let s1 Symbol(); let s2 Symbol(); s1 s2 // false let s3 Symbol(foo); let s4 Symbol(foo); s3 s4 // false类型转换规则Symbol 值不能与其他类型的值进行运算否则报错let sym Symbol(My symbol); your symbol is sym // TypeError: cant convert symbol to string your symbol is ${sym} // TypeError: cant convert symbol to string但可以显式转为字符串String(sym) // Symbol(My symbol) sym.toString() // Symbol(My symbol)也可以转为布尔值Symbol 永远是 truthy但不能转为数值Boolean(sym) // true !sym // false if (sym) { // 该分支会执行 } Number(sym) // TypeError sym 2 // TypeErrorSymbol.prototype.description读取 Symbol 的描述传统写法需要显式转成字符串const sym Symbol(foo); String(sym) // Symbol(foo) sym.toString() // Symbol(foo)这种写法很不方便。ES2019提供了实例属性description直接返回描述字符串该属性源自 TC39 的 Symbol-description 提案const sym Symbol(foo); sym.description // foo作为属性名的 Symbol由于每个 Symbol 值都不相等用 Symbol 作对象属性名可以保证不会出现同名属性。这对由多个模块共同构成的对象尤其有用能防止某个键被不小心改写或覆盖。let mySymbol Symbol(); // 第一种写法 let a {}; a[mySymbol] Hello!; // 第二种写法 let a { [mySymbol]: Hello! }; // 第三种写法 let a {}; Object.defineProperty(a, mySymbol, { value: Hello! }); // 以上写法都得到同样结果 a[mySymbol] // Hello!注意Symbol 值作属性名时不能用点运算符因为点运算符后面总是字符串const mySymbol Symbol(); const a {}; a.mySymbol Hello!; a[mySymbol] // undefined a[mySymbol] // Hello!同理在对象字面量内部用 Symbol 定义属性必须放在方括号中否则键名就是字符串s而不是s代表的 Symbol 值。配合 ES6 增强的对象写法方法定义可以更简洁let s Symbol(); let obj { [s]: function (arg) { /* ... */ } }; objs; // 更简洁的写法 let obj { s { /* ... */ } };用 Symbol 定义常量Symbol 类型很适合定义一组常量保证这组常量值互不相等从而让switch等逻辑按设计工作const log {}; log.levels { DEBUG: Symbol(debug), INFO: Symbol(info), WARN: Symbol(warn) }; console.log(log.levels.DEBUG, debug message); console.log(log.levels.INFO, info message);另一个经典例子——互补色const COLOR_RED Symbol(); const COLOR_GREEN Symbol(); function getComplement(color) { switch (color) { case COLOR_RED: return COLOR_GREEN; case COLOR_GREEN: return COLOR_RED; default: throw new Error(Undefined color); } }常量使用 Symbol 的最大好处是其他任何值都不可能与其相等因此switch语句的匹配结果绝对可靠。需要说明的是Symbol 值作为属性名时该属性仍是公开属性不是私有属性——只是常规 API 难以发现它而已。实例消除魔术字符串魔术字符串指在代码中多次出现、与代码形成强耦合的字符串或数值。风格良好的代码应尽量用含义清晰的变量代替它们。function getArea(shape, options) { let area 0; switch (shape) { case Triangle: // 魔术字符串 area .5 * options.width * options.height; break; /* ... more code ... */ } return area; } getArea(Triangle, { width: 100, height: 100 }); // 魔术字符串消除魔术字符串的常规做法是把它写成变量const shapeType { triangle: Triangle }; function getArea(shape, options) { let area 0; switch (shape) { case shapeType.triangle: area .5 * options.width * options.height; break; } return area; } getArea(shapeType.triangle, { width: 100, height: 100 });进一步分析shapeType.triangle具体等于哪个值并不重要只要保证不与shapeType的其他属性冲突即可。因此这里非常适合改用 Symbol 值——只需改一行const shapeType { triangle: Symbol() };其他代码完全不用修改就同时获得了语义清晰与值绝对唯一两个优点。属性名的遍历Symbol 值作为属性名时它不会出现在for...in、for...of循环中也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但有一个专门的 API 可以拿到它们Object.getOwnPropertySymbols()它返回当前对象所有用作属性名的 Symbol 值组成的数组。const obj {}; let a Symbol(a); let b Symbol(b); obj[a] Hello; obj[b] World; const objectSymbols Object.getOwnPropertySymbols(obj); objectSymbols // [Symbol(a), Symbol(b)]与常规遍历方法对比const obj {}; const foo Symbol(foo); obj[foo] bar; for (let i in obj) { console.log(i); // 无输出 } Object.getOwnPropertyNames(obj) // [] Object.getOwnPropertySymbols(obj) // [Symbol(foo)]另一个新 APIReflect.ownKeys()可以返回所有类型的键名包括常规键名和 Symbol 键名let obj { [Symbol(my_key)]: 1, enum: 2, nonEnum: 3 }; Reflect.ownKeys(obj) // [enum, nonEnum, Symbol(my_key)]关于遍历次序docs/object.md 中给出了明确的规则Reflect.ownKeys返回的数组先排数值属性升序再排字符串属性最后按加入时间升序排列所有 Symbol 键。例如Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 }) // [2, 10, b, a, Symbol()]另外docs/object.md 还提到如果对象的方法是 Symbol 值那么方法的name属性返回该 Symbol 的描述。利用遍历特性实现非私有的内部方法既然 Symbol 键名不会被常规方法遍历到就可以借此为对象定义一些非私有、但只希望内部使用的方法let size Symbol(size); class Collection { constructor() { this[size] 0; } add(item) { this[this[size]] item; this[size]; } static sizeOf(instance) { return instance[size]; } } let x new Collection(); Collection.sizeOf(x) // 0 x.add(foo); Collection.sizeOf(x) // 1 Object.keys(x) // [0] Object.getOwnPropertyNames(x) // [0] Object.getOwnPropertySymbols(x) // [Symbol(size)]对象x的size属性是 Symbol 值所以Object.keys(x)、Object.getOwnPropertyNames(x)都获取不到它造成一种非私有的内部方法效果。docs/class.md 中也有类似用法利用 Symbol 值的唯一性给类定义私有方法但要注意Reflect.ownKeys()依然可以拿到这些 Symbol 键名所以它不是绝对私有。Symbol.for() 与 Symbol.keyFor()有时我们希望复用同一个 Symbol 值这就要用到Symbol.for()。它接受一个字符串参数先搜索全局有没有以该参数为名称的 Symbol 值有则直接返回没有则新建一个并注册到全局。let s1 Symbol.for(foo); let s2 Symbol.for(foo); s1 s2 // trueSymbol.for()与Symbol()都会生成 Symbol区别在于前者会登记在全局环境中供搜索后者不会。调用Symbol.for(cat)30 次每次返回同一个值调用Symbol(cat)30 次返回 30 个不同的值。Symbol.for(bar) Symbol.for(bar) // true Symbol(bar) Symbol(bar) // falseSymbol.keyFor()方法返回一个已登记Symbol 值的key未登记的 Symbol 返回undefinedlet s1 Symbol.for(foo); Symbol.keyFor(s1) // foo let s2 Symbol(foo); Symbol.keyFor(s2) // undefined注意Symbol.for()登记的名字属于全局环境不管是否在全局环境运行。函数内部Symbol.for(bar)生成的 Symbol 同样登记在全局第二次在别处运行Symbol.for(bar)可以取到同一个值function foo() { return Symbol.for(bar); } const x foo(); const y Symbol.for(bar); console.log(x y); // true这个全局登记特性甚至可以跨 iframe 或 service worker 取到同一个值iframe document.createElement(iframe); iframe.src String(window.location); document.body.appendChild(iframe); iframe.contentWindow.Symbol.for(foo) Symbol.for(foo) // true实例模块的 Singleton 模式Singleton 模式指调用一个类任何时候返回的都是同一个实例。对 Node 来说模块文件可以看作一个类。最直接的想法是把实例放到顶层对象global// mod.js function A() { this.foo hello; } if (!global._foo) { global._foo new A(); } module.exports global._foo;const a require(./mod.js); console.log(a.foo);问题在于global._foo是可写的任何文件都可以修改global._foo { foo: world }; const a require(./mod.js); console.log(a.foo);这样会导致加载mod.js的脚本全部失真。改用 Symbol 可以防止无意覆盖// mod.js const FOO_KEY Symbol.for(foo); function A() { this.foo hello; } if (!global[FOO_KEY]) { global[FOO_KEY] new A(); } module.exports global[FOO_KEY];global[FOO_KEY]不会再被无意间覆盖但仍可被改写——只要对方也知道Symbol.for(foo)这个键global[Symbol.for(foo)] { foo: world }; const a require(./mod.js);如果希望外部完全无法引用就用Symbol(foo)而不是Symbol.for(foo)// mod.js const FOO_KEY Symbol(foo); // 后面代码相同 ……这样其他脚本都拿不到FOO_KEY自然无法改写。但代价是多次执行脚本时每次FOO_KEY都不同。虽然 Node 默认会缓存脚本执行结果但用户可手动清除缓存所以这种方案也不是绝对可靠。两种方案的取舍Symbol.for()可跨模块共享但可被改写Symbol()绝对不可引用但无法跨执行复用。内置的 Symbol 值除了自定义 SymbolES6 还提供了11 个内置的 Symbol 值指向语言内部使用的方法均作为Symbol的静态属性存在。Symbol.hasInstanceSymbol.hasInstance指向一个内部方法。当其他对象使用instanceof运算符判断是否为该对象实例时会调用这个方法。即foo instanceof Foo在语言内部实际调用的是FooSymbol.hasInstance。class MyClass { Symbol.hasInstance { return foo instanceof Array; } } [1, 2, 3] instanceof new MyClass() // true也可以定义成静态方法控制谁是该类的实例class Even { static Symbol.hasInstance { return Number(obj) % 2 0; } } // 等同于 const Even { Symbol.hasInstance { return Number(obj) % 2 0; } }; 1 instanceof Even // false 2 instanceof Even // true 12345 instanceof Even // falseSymbol.isConcatSpreadableSymbol.isConcatSpreadable等于一个布尔值表示该对象用于Array.prototype.concat()时是否可以展开。数组默认可以展开属性值为undefined等同展开设为false则不展开let arr1 [c, d]; [a, b].concat(arr1, e) // [a, b, c, d, e] arr1[Symbol.isConcatSpreadable] // undefined let arr2 [c, d]; arr2[Symbol.isConcatSpreadable] false; [a, b].concat(arr2, e) // [a, b, [c,d], e]类似数组的对象正好相反默认不展开设为true才展开let obj {length: 2, 0: c, 1: d}; [a, b].concat(obj, e) // [a, b, obj, e] obj[Symbol.isConcatSpreadable] true; [a, b].concat(obj, e) // [a, b, c, d, e]该属性也可以定义在类里。定义在实例上A1与定义在类本身A2的 getter效果相同class A1 extends Array { constructor(args) { super(args); this[Symbol.isConcatSpreadable] true; } } class A2 extends Array { constructor(args) { super(args); } get [Symbol.isConcatSpreadable] () { return false; } } let a1 new A1(); a1[0] 3; a1[1] 4; let a2 new A2(); a2[0] 5; a2[1] 6; [1, 2].concat(a1).concat(a2) // [1, 2, 3, 4, [5, 6]]Symbol.speciesSymbol.species指向一个构造函数创建衍生对象时使用。默认情况下子类调用继承自父类的方法生成的衍生对象仍是子类的实例class MyArray extends Array { } const a new MyArray(1, 2, 3); const b a.map(x x); const c a.filter(x x 1); b instanceof MyArray // true c instanceof MyArray // true你可能希望b、c是普通数组而不是MyArray的实例。这时可以通过Symbol.species指定衍生对象使用哪个构造函数且定义该属性要采用get取值器class MyArray extends Array { static get [Symbol.species]() { return Array; } } const a new MyArray(); const b a.map(x x); b instanceof MyArray // false b instanceof Array // true默认的Symbol.species属性等价于static get [Symbol.species]() { return this; }Promise 同理class T1 extends Promise { } class T2 extends Promise { static get [Symbol.species]() { return Promise; } } new T1(r r()).then(v v) instanceof T1 // true new T2(r r()).then(v v) instanceof T2 // false总结Symbol.species的作用是——实例在运行中需要再次调用自身构造函数时使用该属性指定的构造函数。主要用途在于类库子类使用继承的方法时作者可能希望返回基类的实例而不是子类的实例。Symbol.matchSymbol.match指向一个函数。执行str.match(myObject)时如果该属性存在会调用它并返回其返回值。String.prototype.match(regexp)等同于regexpSymbol.matchclass MyMatcher { Symbol.match { return hello world.indexOf(string); } } e.match(new MyMatcher()) // 1Symbol.replaceSymbol.replace指向一个方法。当该对象被String.prototype.replace调用时返回该方法的返回值。String.prototype.replace(searchValue, replaceValue)等同于searchValueSymbol.replaceconst x {}; x[Symbol.replace] (...s) console.log(s); Hello.replace(x, World) // [Hello, World]Symbol.replace方法收到两个参数第一个是replace正在作用的对象上面例子中的Hello第二个是替换值World。Symbol.searchSymbol.search指向一个方法。当该对象被String.prototype.search调用时返回该方法的返回值。String.prototype.search(regexp)等同于regexpSymbol.searchclass MySearch { constructor(value) { this.value value; } Symbol.search { return string.indexOf(this.value); } } foobar.search(new MySearch(foo)) // 0Symbol.splitSymbol.split指向一个方法。当该对象被String.prototype.split调用时返回该方法的返回值。String.prototype.split(separator, limit)等同于separatorSymbol.splitclass MySplitter { constructor(value) { this.value value; } Symbol.split { let index string.indexOf(this.value); if (index -1) { return string; } return [ string.substr(0, index), string.substr(index this.value.length) ]; } } foobar.split(new MySplitter(foo)) // [, bar] foobar.split(new MySplitter(bar)) // [foo, ] foobar.split(new MySplitter(baz)) // foobarSymbol.iteratorSymbol.iterator指向该对象的默认遍历器方法。对象进行for...of循环时会调用Symbol.iterator方法返回默认遍历器。用 Generator 函数实现最简洁const myIterable {}; myIterable[Symbol.iterator] function* () { yield 1; yield 2; yield 3; }; [...myIterable] // [1, 2, 3]类中部署class Collection { *[Symbol.iterator]() { let i 0; while(this[i] ! undefined) { yield this[i]; i; } } } let myCollection new Collection(); myCollection[0] 1; myCollection[1] 2; for(let value of myCollection) { console.log(value); } // 1 // 2深入源码佐证在 docs/iterator.md 中明确写道ES6 规定默认的 Iterator 接口部署在数据结构的Symbol.iterator属性上——一个数据结构只要具有Symbol.iterator属性就认为是可遍历的iterablefor...of循环内部调用的就是这个方法。数组等原生数据结构默认部署了该属性arr[Symbol.iterator]即可拿到遍历器普通对象则需自行部署也可以直接复用数组的遍历器NodeList.prototype[Symbol.iterator] Array.prototype[Symbol.iterator]。若该属性对应的不是遍历器生成函数不返回遍历器对象解释引擎会报错详见 docs/iterator.md。Map、Set的默认遍历器也与此相关例如map[Symbol.iterator] map.entries见 docs/set-map.md。有关 Iterator 与for...of的完整介绍可阅读 docs/iterator.md。Symbol.toPrimitiveSymbol.toPrimitive指向一个方法。对象被转为原始类型的值时会调用该方法并返回对应的原始类型值。调用时接受一个字符串参数表示运算模式共三种number该场合需要转成数值string该场合需要转成字符串default该场合可以转成数值也可以转成字符串let obj { Symbol.toPrimitive { switch (hint) { case number: return 123; case string: return str; case default: return default; default: throw new Error(); } } }; 2 * obj // 246 3 obj // 3default obj default // true String(obj) // str注意3 obj得到3default加法运算符默认采用default模式而default分支返回了字符串default于是触发字符串拼接。Symbol.toStringTagSymbol.toStringTag用来设定一个字符串设为其他类型的值无效但不报错。在目标对象上调用Object.prototype.toString()时该属性设定的字符串会出现在返回值中用来定制[object Object]、[object Array]中object后面的大写字符串// 例一 ({[Symbol.toStringTag]: Foo}.toString()) // [object Foo] // 例二 class Collection { get [Symbol.toStringTag]() { return xxx; } } let x new Collection(); Object.prototype.toString.call(x) // [object xxx]ES6 新增内置对象的Symbol.toStringTag属性值如下JSON[Symbol.toStringTag]JSONMath[Symbol.toStringTag]MathModule 对象M[Symbol.toStringTag]ModuleArrayBuffer.prototype[Symbol.toStringTag]ArrayBufferDataView.prototype[Symbol.toStringTag]DataViewMap.prototype[Symbol.toStringTag]MapPromise.prototype[Symbol.toStringTag]PromiseSet.prototype[Symbol.toStringTag]Set%TypedArray%.prototype[Symbol.toStringTag]Uint8Array 等WeakMap.prototype[Symbol.toStringTag]WeakMapWeakSet.prototype[Symbol.toStringTag]WeakSet%MapIteratorPrototype%[Symbol.toStringTag]Map Iterator%SetIteratorPrototype%[Symbol.toStringTag]Set Iterator%StringIteratorPrototype%[Symbol.toStringTag]String IteratorSymbol.prototype[Symbol.toStringTag]SymbolGenerator.prototype[Symbol.toStringTag]GeneratorGeneratorFunction.prototype[Symbol.toStringTag]GeneratorFunctionSymbol.unscopablesSymbol.unscopables指向一个对象指定使用with关键字时哪些属性会被with环境排除。以数组为例Array.prototype[Symbol.unscopables] // { // copyWithin: true, // entries: true, // fill: true, // find: true, // findIndex: true, // includes: true, // keys: true // } Object.keys(Array.prototype[Symbol.unscopables]) // [copyWithin, entries, fill, find, findIndex, includes, keys]数组有 7 个属性会被with命令排除。对比有无 unscopables 的行为差异// 没有 unscopables 时 class MyClass { foo() { return 1; } } var foo function () { return 2; }; with (MyClass.prototype) { foo(); // 1 优先取到原型上的 foo } // 有 unscopables 时 class MyClass { foo() { return 1; } get [Symbol.unscopables]() { return { foo: true }; } } var foo function () { return 2; }; with (MyClass.prototype) { foo(); // 2 foo 被排除指向外层作用域的变量 }总结与进一步阅读Symbol 是 ES6 中一个小而关键的原始类型其核心价值可以归纳为四点唯一性每个 Symbol 值都不相等从根本上解决属性名冲突是安全常量、mixin 扩展、内部方法命名的基础遍历不可见性Symbol 键名不会出现在for...in、Object.keys()、JSON.stringify()等常规操作中但Object.getOwnPropertySymbols()与Reflect.ownKeys()可以获取全局登记机制Symbol.for()提供跨模块、跨 iframe/service worker 共享同一 Symbol 值的能力配合Symbol.keyFor()可反查登记名协议定制能力11 个内置 Symbol 值作为语言内部的钩子允许开发者改写instanceof、concat、map/filter衍生对象的构造、match/replace/search/split、for...of遍历、类型转换、toString()标签与with作用域等语言级行为。本仓库的 docs/symbol.md 是全书第 10 章见 SUMMARY.md与下列章节关联紧密可进一步阅读docs/iterator.mdSymbol.iterator与 Iterator 接口、for...of循环的完整机制docs/reflect.mdReflect.ownKeys()的语义与属性遍历次序docs/object.md对象属性遍历的完整规则含 Symbol 键的排序docs/set-map.mdWeakMap/WeakSet 允许以 Symbol 值作为键的扩展docs/class.md用 Symbol 模拟私有方法的实践与局限docs/let.mdlet、const等 ES6 基础语法。赞分享教程前端【免费下载链接】es6tutorial《ECMAScript 6入门》是一本开源的 JavaScript 语言教程全面介绍 ECMAScript 6 新增的语法特性。项目地址https://gitcode.com/gh_mirrors/es/es6tutorial点击查看免费下载相关推荐ES6教程深入理解Symbol类型ES6教程深入理解Symbol类型 什么是Symbol Symbol是ES6引入的一种全新的原始数据类型用于表示独一无二的值。在JavaScript中除了教程前端TypeScript符号类型详解如何使用Symbol创建唯一标识符TypeScript符号类型详解如何使用Symbol创建唯一标识符 TypeScript符号类型是ECMAScript 2015引入的 全新原始数据类型 它文档教程100行代码读懂U-Net实现brain-segmentation-pytorch核心代码逐行讲解100行代码读懂U Net实现brain segmentation pytorch核心代码逐行讲解 brain segmentation pytorch是一个上一篇openpilot system/ui基于 raylib 的图形界面框架、调试环境变量与 Widget 设计规范下一篇30亿参数重塑企业智能边界IBM Granite-4.0-H-Micro混合架构技术突破与行业变革创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考