Javascript原始值

JavaScript原始值是类型转换中的一个重要的概念,涉及到对象的类型转换时,会先转成原始值再进行其他转换。

建议先看最后面的例子,尝试在浏览器里运行后,再详细阅读原理

一、ToPrimitive ( input [ , PreferredType ] )
转原始值方法:

  1. 确保input是一个JavaScript的合法值
  2. 如果input不是一个Object类型的值返回input本身,否则继续
  3. 如果PreferredType没有传,则令hint为“default”,跳到步骤6
  4. 如果PreferredType是字符串,则令hint为“string”,跳到步骤6
  5. 如果PreferredType是数字,则令hint为“number”
  6. 令exoticToPrim为GetMethod(input, @@toPrimitive)返回值,其中@@toPrimitive是一个用于将对象转成原始值的方法,见后面的例子
  7. 如果exoticToPrim为undefined,继续步骤8,否则跳到步骤10
  8. 如果hint为“default”则将其改为“number”
  9. 返回OrdinaryToPrimitive(input, hint)的值,结束
  10. 令result为Call(exoticToPrim, input, « hint »)的返回值
  11. 如果result的类型不是Object,则返回result,结束流程,否则抛出TypeError异常

二、IsPropertyKey ( argument )
如果入参是字符串或Symbol类型,返回true,否则返回false

三、GetMethod ( V, P )

  1. 确保IsPropertyKey(P)是true
  2. 令func为GetV(V, P)的返回值
  3. 如果func为null或undefined,返回undefined,结束
  4. 如果func是一个可以被调用的方法,返回func,否则抛出一个TypeError异常

四、GetV ( V, P )
GetV用来得到属性的值,如果该值不是对象,则使用适合于该值类型的包装器对象来执行属性查找。 使用参数V和P调用操作,其中V是值,P是属性键。
第一部分中通过GetMethod(input, @@toPrimitive)调用GetV实际就是看input的@@toPrimitive是不是一个可以调用的方法,如果是,就返回这个方法,否则抛出异常

五、OrdinaryToPrimitive ( O, hint )

  1. 确保O是一个Object,hint是一个字符串,且只能是“string”或“number”
  2. 如果hint是“string”,令methodNames为[“toString”,”valueOf”](是一个有序的list),否则就是[“valueOf”,”toString”](顺序返回来)
  3. 遍历methodNames,并看每一项是不是可调用的方法,如果是则调用这个方法,然后看返回值,如果返回值不是个Object,OrdinaryToPrimitive函数直接返回这个返回值
  4. 如果上面遍历都没有符合条件,则抛出TypeError异常

六、示例
下面以Object转Number为例,观察如何调用初始值转换
1. 已知Object转Number的步骤如下:
令primValue为ToPrimitive(argument, hint Number)返回值
返回ToNumber(primValue)
2. +{}会将Object转为Number类型
来看第一段代码

const test1 = {
  valueOf: () => 2,
  toString: () => 3
};
console.log(+test1);

以上代码打印结果为2,转换过程如下:
1. 待转换的为Object(第一部分中的input),待转换类型为Number
2. 取test1的@@toPrimitive方法,这里没有,则要调用OrdinaryToPrimitive
3. 由于这里第二个参数为number,所以按顺序调用test1的valueOf和toString方法
4. 先调用valueOf,首先valueOf是一个函数,可以被调用,且返回值不是一个Object类型(这里返回值是2,是Number类型),那么OrdinaryToPrimitive返回的就是2
5. 再次调用ToNumber(2)最终结果还是2

如果没有valueOf:

const test2 = {
  toString: () => 3
};
console.log(+test2);

打印toString返回的值3

如果valueOf不是个方法,或者valueOf返回值是个Object:

const test3 = {
  valueOf: 2,
  toString: () => 3
};
console.log(+test3);

const test4 = {
  valueOf: () => ({}),
  toString: () => 3
};
console.log(+test4);

均打印toString的返回值3

valueOf返回值为非Object类型的其他值

const test5 = {
  valueOf: () => '0xa',
  toString: () => 3
};
console.log(+test5);

const test6 = {
  valueOf: () => 'a',
  toString: () => 3
};
console.log(+test6);

const test7 = {
  valueOf: () => null,
  toString: () => 3
};
console.log(+test7);

分别打印10,NaN,0
首先三个例子中均返回了valueOf的值作为原始值,根据ToNumber的规则,对原始值再进行一次ToNumber
也就是分别对’0xa’,’a’,null进行了一次转换,等同于+’0xa’,+’a’,+null

如果valueOf和toString返回值都是Object

const test8 = {
  valueOf: () => ({}),
  toString: () => ({})
};
console.log(+test8);

那么根据上面的算法,应该抛出TypeError异常,Chrome控制台运行结果为Uncaught TypeError: Cannot convert object to primitive value,符合预期

上面的例子都没有@@toPrimitive方法,我们看看@@toPrimitive方法如何使用:

const test9 = {
  [Symbol.toPrimitive]: (hint) => {
    console.log(hint);
    return 2;
  }
};
console.log(+test9);

结果输出number和2,由于这里只演示转数字,所以hint就是“number”,实际可以根据不同的类型返回不同的值,例如:

const test10 = {
  [Symbol.toPrimitive]: (hint) => {
    switch (hint) {
      case 'number':
        return 2;
      case 'string': 
        return 'Hello world';
      default: 
        throw new TypeError('转换不了')
    }
  }
};
console.log(+test10, `${test10}`);

以上返回的是2和Hello world

七、应用
一个简易的Money对象,转为数字时,返回数字金额,当转为字符串时自动添加人民币¥前缀

const moneyIn = {
  value: 10,
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return this.value;
      case 'string': 
        return `¥${this.value}`;
      default: 
        throw new TypeError('出错了');
    }
  }
}
const moneyOut = {
  value: 5,
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return this.value;
      case 'string': 
        return `¥${this.value}`;
      default: 
        throw new TypeError('出错了');
    }
  }
}
console.log(`收入:${moneyIn},支出:${moneyOut},余额¥${moneyIn - moneyOut}`);

注意这里只能相减,根据隐式类型转换规则,两个对象相减,会先将对象转为Number,而相加则并非如此,具体转换规则会在后续文章中分析讲解

《Javascript原始值》有3个想法

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

请开启浏览器JavaScript