JS 数据类型浅析之 String、Symbol、Object

1,288 阅读12分钟

String 类型

字符串转换

  • toString() 方法可见于数值、布尔值、对象和字符串值,这个方法的唯一用途就是返回当前值的字符串等价物
    • null 和 undefined 值没有 toString() 方法
    • 数值调用这个方法时,可以传入一个 进制数 作为参数
let num = 10;
console.log(num.toString()); // "10"  默认十进制
console.log(num.toString(2)); // "1010"  2进制
console.log(num.toString(8)); // "12"  8进制
console.log(num.toString(10)); // "10" 10进制
console.log(num.toString(16)); // "a"  16进制
  • String() 转型函数,规则如下:
    • 如果值有 toString() 方法,则调用该方法(不传参数)并返回结果
    • 如果值是 null,返回 "null"。
    • 如果值是 undefined,返回 "undefined"。

模板字面量

  • ECMAScript 6 新增了使用模板字面量定义字符串的能力
  • 与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串,且模板字符串支持嵌套无须转义
  • 模板字面量最常用的一个特性是支持字符串插值,技术上讲,模板字面量不是字符串,而是一种特殊的 JavaScript 句法表达式,求值后得到字符串。
  • 所有插入的值都会使用 toString() 强制转型为字符串,而且任何 JavaScript 表达式都可以用于插值
console.log(`Hello, ${ `World` }!`);		// Hello, World!

let foo = { toString: () => 'World' };
console.log(`Hello, ${ foo }!`);        // Hello, World!

模板字面量标签函数

  • 模板字面量也支持定义标签函数(tag function),通过标签函数可以自定义插值行为。
  • 标签函数会接收被插值记号分隔后的字符串数组和对每个表达式求值的结果
let a = 6;
let b = 9;

function simpleTag(strings, aValExpression, bValExpression, sumExpression) {
 console.log(strings);
 console.log(aValExpression);
 console.log(bValExpression);
 console.log(sumExpression);
 return 'foobar';
}

let untaggedResult = `${ a } + ${ b } = ${ a + b }`;
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", "", raw: Array(4)]
// 6
// 9
// 15

console.log(untaggedResult); // "6 + 9 = 15"
console.log(taggedResult);   // "foobar"

原始字符串

  • 默认的 String.raw 标签函数可以直接获取原始的模板字面量内容(如换行符或 Unicode 字符),而不是被转换后的字符表示。
// Unicode 示例
// \u00A9 是版权符号
console.log(`\u00A9`); // ©
console.log(String.raw`\u00A9`); // \u00A9

// 换行符示例
console.log(`first line\nsecond line`);
// first line
// second line
console.log(String.raw`first line\nsecond line`); // "first line\nsecond line" 

Symbol 类型

基本用法

  • Symbol(符号)是 ECMAScript 6 新增的数据类型
  • 符号是原始值,且符号实例是唯一、不可变的
  • 符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险
  • 调用 Symbol() 函数时,可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码。字符串参数与符号定义或标识完全无关
let genericSymbol = Symbol();		//  Symbol()
let otherGenericSymbol = Symbol();

let fooSymbol = Symbol('foo');  //  Symbol(foo)
let otherFooSymbol = Symbol('foo');

console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false
  • 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
  • 想要使用符号包装对象,也可以这样玩
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);  // Symbol {Symbol()}

console.log(typeof myWrappedSymbol); // "object"

全局符号注册表

  • 如果运行时的不同部分需要共享和重用 symbol 实例,那么可以用一个字符串作为键,通过 Symbol.for() 在全局符号注册表中创建并重用符号
  • Symbol.for() 对每个字符串键都执行幂等操作
    • 传入的字符串,该方法会查找全局运行时的注册表,若不存在对应的 symbol 实例则会创建一个新的 symbol 并返回,若已存在对应 symbol 实例则直接返回该实例
  • 即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol() 定义的符号也不相同
let fooGlobalSymbol = Symbol.for('foo');
console.log(typeof fooGlobalSymbol); // symbol

let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true 

let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false
  • 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

使用符号作为属性

  • 凡是可以使用字符串或数值作为属性的地方,都可以使用符号
let s1 = Symbol('foo'),
    s2 = Symbol('bar'),
    s3 = Symbol('baz'),
    s4 = Symbol('qux');

let 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}
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)]

常用的内置符号

  • ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为
  • 内置符号都以 Symbol 工厂函数 字符串属性的形式存在,也就是全局函数 Symbol 的普通字符串属性,指向一个符号的实例。
    • 所有内置符号属性都是不可写、不可枚举、不可配置的
  • 这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为

Symbol.iterator

  • 这个符号作为一个属性表示 “一个方法,该方法返回对象默认的迭代器。由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。

  • for-of 循环时,会调用以 Symbol.iterator 为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象,通常返回的对象是实现该 API 的 Generator

class Foo {
  *[Symbol.iterator]() {}
}
let f = new Foo();
console.log(f[Symbol.iterator]());		// Generator {<suspended>} 
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.asyncIterator

  • 这个符号作为一个属性表示 “一个方法,该方法返回对象默认的 AsyncIterator。由 for-await-of 语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数
class Emitter {
  constructor(max) {
    this.max = max;
    this.asyncIdx = 0;
  }

  async *[Symbol.asyncIterator]() {
    while (this.asyncIdx < this.max) {
      yield new Promise((resolve) => {
        setTimeout(() => {
          resolve(this.asyncIdx++)
        }, 1000)
      });
    }
  }
}

function asyncCount() {
  let emitter = new Emitter(5);
  (async function () {
    for await (const x of emitter) {
      console.log(x);
    }
  })()

}

asyncCount(); // 估计大家都能猜到结果是串行打印的
// 0
// 1
// 2
// 3
// 4

Symbol.hasInstance

  • 这个符号作为一个属性表示 “一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 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 
  • 也可以这样玩,在 instanceof 执行时,会调用以 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;  // 不管是啥都返回 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

  • 这个符号作为一个属性表示 “一个布尔值,如果是 true,则意味着对象应该用 Array.prototype.concat() 打平其数组元素”

  • ES6 中的 Array.prototype.concat() 方法会根据接收到的对象的类型来选择,如何将一个 类数组对象 拼接成数组实例。覆盖 Symbol.isConcatSpreadable 的值可以修改这个行为

    • 数组对象 默认情况下会被打平到 已有的数组false 或假值 会导致 整个对象 被追加到 数组末尾
    • 类数组对象 默认情况下会被追加到 数组末尾true 或真值 会导致这个 类数组对象 被打平到 数组实例
    • 其他不是 类数组对象 的对象,如集合 Set,在 Symbol.isConcatSpreadable 被设置为 true 的情况下将被忽略
let initial = ['foo'];
let array = ['bar'];

console.log(array[Symbol.isConcatSpreadable]); // 默认是 undefined
console.log(initial.concat(array)); // ['foo', 'bar']

array[Symbol.isConcatSpreadable] = false;
console.log(initial.concat(array)); // ['foo', Array(1)]

let arrayLikeObject = { length: 1, 0: 'baz' }; // 类数组对象
console.log(arrayLikeObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(arrayLikeObject)); // ['foo', {...}]  默认被整个添加到数组末尾

arrayLikeObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(arrayLikeObject)); // ['foo', 'baz']

let otherObject = new Set().add('qux');
console.log(otherObject[Symbol.isConcatSpreadable]); // undefined
console.log(initial.concat(otherObject)); // ['foo', Set(1)]

otherObject[Symbol.isConcatSpreadable] = true;
console.log(initial.concat(otherObject)); // ['foo']

Symbol.match

  • 这个符号作为一个属性表示 “一个正则表达式方法,该方法用正则表达式去匹配字符串。由 String.prototype.match() 方法使用”

  • 给 match 方法传入非正则表达式值会导致该值被转换为 RegExp 对象

    • 如果想改变这种行为,让方法直接使用参数,则可以重新定义 Symbol.match 函数以取代默认对正则表达式求值的行为,从而让 match() 方法使用非正则表达式实例
    • Symbol.match 函数接收一个参数,就是调用 match()方法的字符串实例
class FooMatcher {
  static [Symbol.match](target) {
    return target.includes('foo');  // target 就是调用 match 方法的字符串实例
  }
}

console.log('foobar'.match(FooMatcher)); // true
console.log('barbaz'.match(FooMatcher)); // false


class StringMatcher {
  constructor(str) {
    this.str = str;
  }
  
  [Symbol.match](target) {
    return target.includes(this.str);		// target 就是调用 match 方法的字符串实例
  }
}
console.log('foobar'.match(new StringMatcher('foo'))); // true
console.log('barbaz'.match(new StringMatcher('qux'))); // false

Symbol.replace

  • 这个符号作为一个属性表示 “一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace() 方法使用”

  • 给 replace() 方法传入非正则表达式值会导致该值被转换为 RegExp 对象

    • 如果想改变这种行为,让方法直接使用参数,可以重新定义 Symbol.replace 函数以取代默认对正则表达式求值的行为,从而让 replace() 方法使用非正则表达式实例
    • Symbol.replace 函数接收两个参数,即调用 replace() 方法的 字符串实例替换字符串
class FooReplacer {
  static [Symbol.replace](target, replacement) {
    return target.split('foo').join(replacement);
  }
}
console.log('barfoobaz'.replace(FooReplacer, 'qux'));		// "barquxbaz"


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

  • 这个符号作为一个属性表示 “一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由 String.prototype.search() 方法使用”

  • 给 search() 方法传入非正则表达式值会导致该值被转换为 RegExp 对象

    • 如果想改变这种行为,让方法直接使用参数,可以重新定义 Symbol.search 函数以取代默认对正则表达式求值的行为,从而让 search() 方法使用非正则表达式实例
    • Symbol.search 函数接收一个参数,就是调用 search() 方法的字符串实例
class FooSearcher {
  static [Symbol.search](target) {
    return target.indexOf('foo');
  }
}
console.log('foobar'.search(FooSearcher)); // 0
console.log('barfoo'.search(FooSearcher)); // 3
console.log('barbaz'.search(FooSearcher)); // -1


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

  • 这个符号作为一个属性表示 “一个函数值,该函数作为创建派生对象的构造函数”
  • 用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义
class Bar extends Array {}

let bar = new Bar();
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true

bar = bar.concat('bar');
console.log(bar instanceof Array); // true
console.log(bar instanceof Bar); // true


class Baz extends Array {
  static get [Symbol.species]() {
    return Array;
  }
}

let baz = new Baz();
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // true

baz = baz.concat('baz');
console.log(baz instanceof Array); // true
console.log(baz instanceof Baz); // false 

Symbol.split

  • 这个符号作为一个属性表示 “一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由 String.prototype.split() 方法使用”

  • 给 split() 传入非正则表达式值会导致该值被转换为 RegExp 对象

    • 如果想改变这种行为,让方法直接使用参数,可以重新定义 Symbol.split 函数以取代默认对正则表达式求值的行为,从而让 split() 方法使用非正则表达式实例
    • Symbol.split 函数接收一个参数,就是调用 split() 方法的字符串实例
class FooSplitter {
  static [Symbol.split](target) {		// target 就是调用 split 方法的字符串实例
    return target.split('foo');
  }
}
console.log('barfoobaz'.split(FooSplitter));		// ["bar", "baz"]

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

  • 这个符号作为一个属性表示 “一个方法,该方法将对象转换为相应的原始值。由 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

  • 这个符号作为一个属性表示 “一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString() 使用”

  • 通过 toString() 方法获取对象标识时,会检索由 Symbol.toStringTag 指定的实例标识符,默认为 "Object"。

    • 内置类型已经指定了这个值,但自定义类实例还需要明确定义
let s = new Set();
console.log(s); // Set(0) {size: 0}
console.log(s.toString()); // [object Set]
console.log(s[Symbol.toStringTag]); // Set

class Foo {}
let foo = new Foo();
console.log(foo); // Foo {}
console.log(foo.toString()); // [object Object]
console.log(foo[Symbol.toStringTag]); // undefined

class Bar {
  constructor() {
    this[Symbol.toStringTag] = 'Bar';
  }
}
let bar = new Bar();
console.log(bar); // Bar {Symbol(Symbol.toStringTag): 'Bar'}
console.log(bar.toString()); // [object Bar]
console.log(bar[Symbol.toStringTag]); // Bar 

Object 类型

  • ECMAScript 中的对象其实就是一组数据和功能的集合
  • ECMAScript 中的 Object 也是派生其他对象的基类
  • Object 类型的所有属性和方法在派生的对象上同样存在,每个 Object 实例都有如下属性和方法:
    • constructor:用于创建当前对象的函数。这个属性的值就是 Object() 函数
    • hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属 性,要检查的属性名必须是字符串或符号
    • isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型
    • propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用 for-in 语句枚举,属性名必须是字符串
    • toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境
    • toString():返回对象的字符串表示
    • valueOf():返回对象对应的字符串、数值或布尔值表示,通常与 toString() 的返回值相同