阅读 467

【ES6基础】Symbol介绍:独一无二的值

ES6之前我们都清楚JS有六种数据类型:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object),今天笔者讲的Symbol类型是ES6才加入的,它最大的特点就如标题所说“独一无二”。

本篇文章笔者将从以下几个方面进行介绍:

  • 值类型和引用类型介绍
  • 如何声明一个Symbol?
  • 为什么要有Symbol?
  • Symbol的常用用法
  • 内置常用Symbol值的用法

本篇文章阅读时间预计15分钟。

值类型和引用类型介绍

在了解Symbol之前,我们需要了解下JS的数据类型,在JS中数据类型分为两类:值类型和引用类型。

  • 值类型:数值型(Number),字符类型(String),布尔值型(Boolean),null 和 underfined
  • 引用类型:对象(Object)

所谓的值类型可以这样理解:变量之间的互相赋值,是指开辟一块新的内存空间,将变量值赋给新变量保存到新开辟的内存里面;之后两个变量的值变动互不影响。 如下段代码所示:

let weChatName ="前端达人"; 
//开辟一块内存空间保存变量 weChatName 的值“前端达人”;
let touTiao =weChatName; 
//给变量 touTiao 开辟一块新的内存空间,将 weChatName 的值 “前端达人” 赋值一份保存到新的内存里;
//weChatName 和 touTiao 的值以后无论如何变化,都不会影响到对方的值;复制代码

一些语言,比如 C,有引用传递和值传递的概念。JS 也有类似的概念,它是根据传递的数据类型推断的。如果将值传递给函数,则重新分配该值不会修改调用位置中的值。但是,如果你修改的是引用类型,那么修改后的值也将在调用它的地方被修改。

所谓的引用类型可以这样理解:变量之间的互相赋值,只是指针的交换,而并非将对象复制一份给新的变量,对象依然还是只有一个,只是多了一个指引~~; 如下段代码所示:

let weChat = { name: "前端达人", regYear:"2014" }; 
//需要开辟内存空间保存对象,变量 weChat 的值是一个地址,这个地址指向保存对象的空间;
let touTiao= weChat; 
// 将 weChat 的指引地址赋值给 touTiao,而并非复制一给对象且新开一块内存空间来保存;
weChat.regYear="2018";
console.log(touTiao);
//output:{ name: '前端达人', regYear: '2018' }
// 这个时候通过 weChat 来修改对象的属性,则通过 touTiao 来查看属性时对象属性已经发生改变;复制代码

那Symbol是什么数据类型呢?这里笔者先告诉大家是值类型,下面会有详细的介绍。

如何声明一个Symbol?

Symbol最大的特点就如本篇文章的标题一样:独一无二。这个独一无二怎么解释呢?就好比双胞胎,外表看不出差别,但是相对个体比如性格爱好还是有差异的,每个人都是独一无二。Symbol表示独一无二的值,是一种互不等价标识,声明Symbol十分简单,如下段代码所示:

const s = Symbol();复制代码

Symbol([description]) 声明方式,支持一个可选参数,只是用于描述,方便我们开发调试而已。每次执行Symbol()都会生成一个独一无二的Symbol值,如下段代码所示:

let s1 = Symbol("My Symbol");
let s2 = Symbol("My Symbol");
console.log(s1 === s2); // Outputs false”复制代码

由此可见,即使Symbol的描述值参数相同,它们的值也不相同,描述值仅仅是起描述的作用,不会对Symbol值本身起到任何的改变。关于描述值需要注意的一点:接受除Symbol值以外所有的值,怎么理解呢,请看下段代码所示:

const symbol = Symbol();
const symbolWithString=Symbol('前端达人');
//Symbol(前端达人)
const SymbolWithNum=Symbol(3.14); 
//Symbol(3.14)
const SymbolWithObj=Symbol({foo:'bar'});
//Symbol([object Object])
const anotherSymbol=Symbol(symbol); 
//TypeError: Cannot convert a Symbol value to a string复制代码

接下来笔者来详细解释下,为什么Symbol是值类型,而不是引用类型。Symbol函数并不是构造函数,因此不能使用new方法来生成一个Symbol对象,否则编译器会抛出异常,如执行下段代码所示:

new Symbol();
//TypeError: Symbol is not a constructor复制代码

由此可见,Symbol是一种值类型而非引用类型,这就意味着如果将Symbol作为参数传值的话,将会是值传值而非引用传值,如下段代码所示(值的改变没有互相影响):

const symbol=Symbol('前端达人');
function fn1(_symbol) {
    return _symbol==symbol;
}
console.log(fn1(symbol));
//output:true;
function fn2(_symbol) {
    _symbol=null;
    console.log(_symbol);
}
fn2(symbol);
//output:null;
console.log(symbol);
//Symbol(前端达人)复制代码

为什么要有Symbol?

介绍了这么多,Symbol存在的意义是什么?笔者先举个简单的业务场景:

在前端的JavaScript应用开发中,需要先通过渲染引擎所提供的API来获取一个DOM元素对象,并保留在JavaScript运行时中。因为业务需要,需要通过一个第三方库对这个DOM元素对象进行一些修饰和调整,即对该DOM元素对象进行一些新属性的插入。

而后来因为新需求的出现,需要再次利用另外一个第三方库对同一个DOM元素对象进行修饰。但非常不巧的是这个第三方库同样需要对该DOM元素对象进行属性插入,而恰好这个库所需要操作的属性与前一个第三方库所操作的属性相同。这种情况下就很有可能会出现两个第三方库都无法正常运行的现象,而使用这些第三方库的开发者却难以进行定位和修复。

针对上述问题, Symbol可以提供一种良好的解决方案。这是因为Symbol的实例值带有互不等价的特性,即任意两个Symbol值都不相等。在ES2015标准中,字面量对象除了可以使用字符串、数字作为属性键以外,还可以使用Symbol作为属性键,因此便可以利用Symbol值的互不等价特性来实现属性操作的互不干扰了。

Symbol的常用用法

1、判断是否是Symbol

如何判断一个变量是不是Symbol类型呢?目前唯一的方法就是使用typeof,如下段代码所示:

const s = Symbol();
console.log(typeof s); 
//Outputs "symbol”复制代码

2、用作对象的属性

通常我们使用字符串定义对象的属性(Key),有了Symbol类型后,我们当然可以使用Symbol作为对象的属性,唯一不同的地方,我们需要使用[]语法定义属性,如下段代码所示:

const WECHAR_NAME = Symbol();
const WECHAR_REG = Symbol();

let obj = {
  [WECHAR_NAME]: "前端达人";
}
obj[WECHAR_REG] = 2014;
console.log(obj[WECHAR_NAME]) //output: 前端达人
console.log(obj[WECHAR_REG]) //output:2014复制代码

还有一点需要强调的是,使用Symbol作为对象的Key值时,具有私有性,我们无法通过枚举获取Key值,如下段代码所示:

let obj = {
    weChatName:'前端达人',
    regYear: 2014,
    [Symbol('pwd')]: 'wjqw@$#sndk9012',
}

console.log(Object.keys(obj));   
// ['weChatName', 'regYear']

for (let p in obj) {
    console.log(p)   
    // 分别会输出:'weChatName' 和 'regYear'
}
console.log(Object.getOwnPropertyNames(obj));  
// [ 'weChatName', 'regYear' ]复制代码

从上述代码中,可以看出Symbol类型的key是不能通过Object.keys()或者for...in来枚举的,它未被包含在对象自身的属性名集合(property names)之中。利用该特性,我们可以把一些不需要对外操作和访问的属性可以使用Symbol来定义。由于这一特性的存在,我们使用JSON.stringify()将对象转换成JSON字符串的时候,Symbol属性也会被排除在输出内容之外,在上述代码中执行下段代码:

console.log(JSON.stringify(obj));
//output:{"weChatName":"前端达人","regYear":2014}复制代码

基于这一特性,我们可以更好的去设计我们的数据对象,让“对内操作”和“对外选择性输出”变得更加灵活。

我们难道就没有办法获取Symbol方式定义的对象属性了么?私有并不是绝对的,我们可以通过一些API函数进行获取,在上述代码中执行下段代码:

// 使用Object的API
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(pwd)]

// 使用新增的反射API
console.log(Reflect.ownKeys(obj));// [Symbol(pwd), 'age', 'title']复制代码

3、定义类的私有属性/方法

我们都清楚在JS中,是没有如Java等面向对象语言的访问控制关键字private的,类上所有定义的属性或方法都是可公开访问的。上面笔者讲到作为对象属性具有私有性的特点,我们定义类的私有属性和方法才能实现,如下段代码所示:

我们先建立一个a.js的文件,如下所示:

const PASSWORD = Symbol();
class Login {
  constructor(username, password) {
    this.username = username;
    this[PASSWORD] = password;
  }
  checkPassword(pwd) {
      return this[PASSWORD] === pwd;
  }
}
export default Login;复制代码

我们在建立一个文件b.js,引入a.js文件,如下所示:

import  Login from './a.js';
const login = new Login('admin', '123456');
console.log(login.checkPassword('123456'));  // true
console.log(login.PASSWORD);  // undefined
console.log(login[PASSWORD]);// PASSWORD is not defined
console.log(login["PASSWORD"]); // undefined复制代码

由于Symbol常量PASSWORD被定义在a.js所在的模块中,外面的模块获取不到这个Symbol,也不可能再创建一个一模一样的Symbol出来(因为Symbol是独一无二的),因此这个PASSWORD的Symbol只能被限制在a.js内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,从而实现了私有化的效果。

4、创建共享Symbol

虽然Symbol是独一无二的,但是有些业务场景,我们需要共享一个Symbol,我们如何实现呢?这种情况下,我们就需要使用另一个API来创建或获取Symbol,那就是Symbol.for(),它可以注册或获取一个全局的Symbol实例,如下段代码所示:

let obj = {};
(function(){
 let s1 = Symbol("name");
 obj[s1] = "Eden";
})();
console.log(obj[s1]);
//SyntaxError: Unexpected identifier cannot be accessed here
(function(){
 let s2 = Symbol.for("age");
 obj[s2] = 27;
})();
console.log(obj[Symbol.for("age")]); //Output "27”复制代码

从上述代码可以看出,Symbol.for()会注册一个全局作用域的Symbol值,如果这个Key值从未使用则会进行创建注册,如果已被注册,则会返回一个与第一次使用创建的Symbol值等价的Symbol,如下段代码所示:

const symbol=Symbol.for('foo');
const obj={};
obj[symbol]='bar';
const anotherSymbol=Symbol.for('foo');
console.log(symbol===anotherSymbol);
//output:true
console.log(obj[anotherSymbol]);
//output:bar复制代码

常用Symbol值及意义

我们除了可以自行创建Symbol值以外,ES6还将其应用到了ECMAScript引擎的各个角落,我们可以运用这些常用值对底层代码的实现逻辑进行修改,以实现更高级的定制化的需求。

以下表格进行了常用Symbol值的总结

定义项

描述

含义

@@iterator

"Symbol.iterator"

用于为对象定义一个方法并返回一个属于所对应对象的迭代器。该迭代器会被for-of循环使用。

@@hasInstance

"Symbol.hasInStance"

用于为类定义一个方法。该方法会因为instanceof语句的使用而被调用,来检查一个对象是否是某一个类的实例。

@@match

"Symobol.match"

用于正则表达式定义一个可被String.prototype.match()方法使用的方法,检查对应字符串与当前正则表达式是否匹配

@@replace

"Symbol.replace"

用于正则表达式会对象定义一个方法。该方法会因为String.prototype.replace()方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象作为替换标志时的内部处理逻辑

@@search

"Symbol.search"

用于正则表达式会对象定义一个方法。该方法会因为String.prototype.search()方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象作为位置检索标志时的内部处理逻辑

@@split

"Symbol.split"

用于正则表达式会对象定义一个方法。该方法会因为String.prototype.split()方法的使用而被调用,用于处理当前字符串使用该正则表达式或对象作为分割标志时的内部处理逻辑

@@unscopables

"Symbol.unscopables"

用于为对象定义一个属性。该属性用于描述该对象中哪些属性是可以被with语句所使用的。

@@isConcatSpreadable

"Symbol.isConcatSpreadable"

用于为对象定义一个属性。该属性用于决定该对象作为Array.prototype.concat()方法参数时,是否被展开。

@@species

"Symbol.species"

用于为类定义一个静态属性,该属性用于决定该类的默认构建函数。

@@toPrimitive

"Symbol.toPrimitive"

用于为对象定义一个方法。该方法会在该对象需要转换为值类型的时候被调用,可以根据程序的行为决定该对象需要被转换成的值。

@@toStringTag

"Symbol.toStringTag"

用于为类定义一个属性。该属性可以决定这个类的实例在调用toString()方法时,其中标签的内容。

由于常用Symbol值比较多,笔者只对其中最常用的几个进行解释。

1、Symbol.iterator

我们可以使用Symbol.iterator来自定义一个可以迭代的对象,我们可以使用Symbol.iterator作为方法名的方法属性,该方法返回一个迭代器(Iterator)。虽然JS中没有协议(Protocal)的概念,我们可以将迭代器看做成一个协议,即迭代器协议(Iterator Protocal),该协议定义了一个方法next(),含义是进入下一次迭代的迭代状态,第一次执行即返回第一次的迭代状态,该迭代状态有两个属性,如表格所示:

定义项

描述

含义

done

Boolean

该迭代器是否已经迭代结束

value

Any

当前迭代状态值

以下是我们使用Symbol.iterator带迭代的方法,如下段代码所示:

let obj = {
    array: [1, 2, 3, 4, 5],
    nextIndex: 0,
    [Symbol.iterator]: function(){
        return {
         array: this.array,
         nextIndex: this.nextIndex,
         next: function(){
             return this.nextIndex < this.array.length ?
                    {value: this.array[this.nextIndex++], done: false} :
                    {done: true};
         }
        }
    }
};
let iterable = obj[Symbol.iterator]();
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().value);
console.log(iterable.next().done);复制代码

以上代码将会输出:

1
2
3
4
5
true复制代码

除了可以自定义迭代的逻辑,我们也可以使用引擎默认的迭代,从而节省了我们的代码量,如下段代码所示:

const arr = [1, 2];
const iterator = arr[Symbol.iterator](); // returns you an iterator
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());复制代码

以上代码将会输出

{ value: 1, done: false }
{ value: 2, done: false }
{ value: undefined, done: true }复制代码

2、Symbol.hasInstance

用于为类定义一个方法。该方法会因为instanceof语句的使用而被调用,来检查一个对象是否是某一个类的实例, 用于扩展instanceof的内部逻辑,我们可以用于为一个类定一个静态方法,该方法的第一个形参便是被检测的对象,而自定义的方法内容决定了instanceof语句的返回结果,代码如下:

class Foo{
    static [Symbol.hasInstance](obj){
        console.log(obj);
        return true;
    }
}
console.log( {} instanceof  Foo);复制代码

以上代码将会输出

{}
true复制代码

3、Symbol.match

Symbol.match 在字符串使用match()方法时,为其实现自定义的逻辑。如下段代码所示:

没自定义前:

const re=/foo/
console.log('bar'.match(re));//null
console.log('foo'.match(re));
//[ 'foo', index: 0, input: 'foo', groups: undefined ]复制代码

使用Symbol.match后:

const re=/foo/
re[Symbol.match]=function (str) {
    const regexp=this;
    console.log(str);
    return true;
}
console.log('bar'.match(re));
console.log('foo'.match(re));复制代码

上端代码将会输出:

bar
true
foo
true复制代码

4、Symbol.toPrimitive

在JS开发中,我们会利用其中的隐式转换规则,其中就包括将引用类型转换成值类型,然而有时隐式转换的结果并不是我们所期望的。虽然我们可以重写toString()方法来自定义对象在隐式转换成字符串的处理,但是如果出现需要转换成数字时变得无从入手。我们可以使用Symbol.toPrimitive来定义更灵活处理方式,如下段代码所示(仅为演示,可结合自己的业务自行修改):

const obj={};
console.log(+obj);
console.log(`${obj}`);
console.log(obj+"");
//output:
//NaN
//[object Object]
//[object Object]
const transTen={
    [Symbol.toPrimitive](hint){
        switch (hint) {
            case 'number':
                return 10;
            case 'string':
                 return 'Ten';
            default:
                 return true;
        }
    }
}

console.log(+transTen);
console.log(`${transTen}`);
console.log(transTen+"");
//output:
//10
//Ten
//true复制代码

5、Symbol.toStringTag

前面的表格提到过,Symbol.toStringTag的作用就是自定义这个类的实例在调用toString()时的标签内容。 比如我们在开发中定义的类,就可以通过Symbol.toStringTag来修改toString()中的内容,利用它做为属性键为类型定一个Getter。

class Foo{
    get [Symbol.toStringTag](){return 'Bar'}
}
const obj=new Foo();
console.log(obj.toString());
//output:[object Bar]复制代码

小节

今天的内容有些多,需要慢慢理解,我们清楚了Symbol值是独一无二的,Symbol的一些使用场景,以及使用Symbol常用值改写更底层的方法,让我们写出更灵活的处理逻辑。Symbol虽然强大,但是用好它还需要在实践中结合业务场景进行掌握。




关注下面的标签,发现更多相似文章
评论