谈 JavaScript 中的强制类型转换 (1. 基础篇)

1. 从基本包装类型讲起

讨论基本包装类型的前提是了解基本数据类型(也可以称为原始类型, 简单数据类型等)。然后通过基本数据类型调用方法的行为, 引出基本包装类型的概念和作用.

1.1 基本数据类型

已经知道 JavaScript 中共有6种基本数据类型,分别是:string,number,boolean,null,undefined,symbol.

基本类型既不是对象, 也没有方法可供调用. 然而经常见到基本类型的变量调用方法的情况, 类如:

let index = 'ABCDE'.indexOf('CD');
console.log(index); // 2

上例中 'ABCDE' 是一个字符串的基本类型, 这个字符串直接调用了indexOf 方法, 将该方法的返回值保存在变量 index 中, 然后在第二行输出. 可以看到这个例子能够运行并且结果正确.

既然上面说原始类型没有方法可供调用, 那么字符串 'ABCDE' 在调用 indexOf 方法时为什么没有出错呢? 而且从运行结果来看, indexOf 这个方法确实被调用了, 那么是谁调用了这个方法并且返回了正确的结果呢? 答案就是基本包装类型. 下面引入基本包装类型的相关内容.

1.2 基本包装类型

关键词: 包装, 基本包装类型, 基本包装类型的对象

上一节讨论到其实调用方法的并不是字符串本身 , 而是基本包装类型. 下面就应该具体讨论基本包装类型的相关内容了.

为了基本类型可以正常调用方法, 后台会为这个基本类型的值自动创建一个对应的对象, 这个对象的类型就称为基本包装类型, 然后用这个对象去调用方法, 在调用完方法之后, 这个对象就会被销毁( 用完就销毁 )了. 这个从基本数据类型生成基本包装类型对象的过程称为包装( box ).

简单来说, 在基本类型调用方法的时候, 方法的真正调用者其实不是我们直接定义的基本类型, 而是后台给我们创建的基本包装类型的 对象 . 而且这个对象是临时的、不会一直存在的、用完就会被销毁的.

这个对象还有一个特点, 他是与基本类型的值相对应的. 然而:

并不是每一种基本类型都有对应的包装类型

上节中提到的六种基本类型中的四种有其对应的包装类型, 分别是:

string(字符串) -> String, number(数值) -> Number,boolean(布尔) -> Boolean,symbol(符号) -> Symbol

其中符号 -> 表示对应. 同时: 注意首字母的大写( 对象名的首字母习俗默认大写 ).

注意: 本文只讨论 string,number,boolean 三种基本类型及其对应的包装类型.

还需要了解的是, 不仅仅是系统可以隐式的创建包装对象, 用户也可以手动、显式的创建一个基本包装类型的对象, 方法为: 用关键字 new 加上对应的包装类型的构造函数, 参数传入基本类型的值即可, 例如:

let n = new Number(22);             // n 是 数值22对应的包装对象
let str = new String('example');    // str 是字符串 'example' 对应的包装对象
let flag = new Boolean(false);      // flag 是布尔值 false 对应的包装对象

至此, 再看第一节的这个例子, 可以再次想象系统创建包装对象的大致过程:

let index = 'ABCDE'.indexOf('CD');
console.log(index); // 2

可以发现第一行代码中后台发生了包装行为: 后台根据字符串类型的 'ABCDE' 包装出了一个对象. 即: 在调用 indexOf 方法时, 后台发现想调用这个方法的是一个基本类型的值, 但是这个值没有这个方法可供调用, 于是为它生成了对应的包装对象( 操作 1 ), 然后通过这个包装对象来调用了 indexOf 方法( 操作 2 ), 而后将方法的返回值赋给了变量 index. 最后, 把这个包装对象销毁( 操作 3 ).

根据以上思路,可以大致模拟出上述过程的对应的代码:

// step 1. 创建 'ABCDE' 对应的基本包装类型的对象: 
let temp = new String('ABCDE');

// step 2. 用包装类型的对象 temp 调用 indexOf 方法, 并将返回值赋给 index 变量: 
let index = temp.indexOf('CD');

// step 3. 将 temp 对象销毁
temp = null;

1.3 总结

当一个基本数据类型想要调用方法时, 后台会为它生成一个临时的包装对象, 利用这个对象去调用方法, 再将方法执行的结果返回, 随后这个临时对象被销毁.

1.4 包装对象的"拆包装" box <-> unbox

从上面的内容了解到 包装(box) 是根据一个基本类型的值生成一个对应类型的对象的过程,
与这个过程大致相反, 存在一种根据包装对象生成基本类型值的过程, 可称为 拆包装 (unbox). 这个过程同样即可以由后台隐式的完成, 也可以手动的调用方法 valueOf 来做.

下一节开始讨论 valueOf 这个方法, 同时引出另外一个同样重要的方法 toString.

2. 对象的两个重要方法 valueOftoString

2.1 基本包装类型的拆包装( unbox ) 用到的 valueOf 方法

基本包装类型的拆包装操作用到了包装对象中的 valueOf 函数, 这个函数可以将一个对象转换成一个基本类型的值.

对于 Boolean, Number 和 String 三者的基本包装对象来说, 调用 valueOf 的返回值是各自对应的基本数据类型的值:

let n = new Number(22);     // 包装基本数值数据 22
// 拆包装出来的结果是对应的基本数据类型的值
console.log(n.valueOf() === 22);    // true

let str = new String('example');    // 包装基本字符串数据 'example'
// 拆包装出来的结果是对应的基本数据类型的值
console.log(str.valueOf() === 'example');   // true, 

let flag = new Boolean(false);      // 包装基本布尔数据 false
// 拆包装出来的结果是对应的基本数据类型的值
console.log(flag.valueOf() === false);  // true

有时会发生后台隐式拆包装的情况, 包装类型的对象会在后台调用 valueOf 方法, 例如:

let a = new Number(1);
let b = a + 1;  //---> 这一行发生了隐式拆包装操作: let b = a.valueOf() + 1;

console.log(b);  // b 为 2

console.log(typeof a);  // object
console.log(typeof b);  // number, b 的类型为 基本数据类型, 而不是包装类型

甚至一行代码中会发生包装和解包装两种操作, 例如:

let num = 3.14159;
console.log(num.valueOf());   // 3.14159

在上面代码块的第二行代码中变量 num 要调用函数 valueOf , 此时 num 会被先包装为 基本包装类型的对象,而这个对象在调用 valueOf 方法时就发生了解包装的操作.

2.2 其他对象的 valueOf 方法

不仅仅是基本包装类型有 valueOf 方法, 许多 JavaScript 内建(build-in)对象都有该函数, 为使执行的结果与对象本身相符合, 大多对象都重写了这个方法. 下面看一些其他对象的 valueOf 方法的行为有什么特点.

以下列出常用内置对象的 valueOf 方法的返回值:

对象 返回值
Boolean, Number 和 String 三者 各自相对应的基本类型的值
Array,Function,Object 三者 其本身
Date 当前时间距 1970.01.01 午夜的毫秒数
Math 和 Error 没有 valueOf 方法

下面是实验结果:

// 数组调用 valueOf, 返回数组本身
let array = [1, 'hello', false];
console.log(array.valueOf() === array);   // true

// 函数调用 valueOf, 返回函数本身
function foo(){}
console.log(foo.valueOf() === foo);   // true

// 对象调用 valueOf, 返回对象本身
let obj = {
    name: 'doug',
    age : 22
};
console.log(obj.valueOf() === obj);   // true

// 当前时间距1970年1月1日午夜的毫秒数
console.log(new Date().valueOf());   // 1551684737052

总结: valueOf 方法可以将一个对象转换为基本数据类型, 并不是每个对象都有此方法(例如: Math 和 Error 对象).
对于布尔、数值和字符串三者的基本包装类型来说,调用此函数返回其对应的基本类型的值;
对象调用此函数的返回值是其本身 ( 由于数组和函数本质上也是对象, 所以也返回其自身 ) .

提到 valueOf 方法就不得不想起另外一个对于类型转换十分重要的方法 toString, 下节将会讨论它.

2.3 可以将对象表示为字符串的方法 toString()

每个内置的对象都有此方法,是从 Object 对象继承而来的. 为使执行的结果与对象本身相符合, 大多数内置对象都重写了该函数. 常见的对象调用 toString 方法的返回值如下:

  • 对于用户创建的对象, 返回'[object object]'. (存在一个例外, 见最后部分)

  • 对于 Math 对象, 返回 "[object Math]":

// 自定义的对象
console.log({name: 'doug'}.toString());  // '[object Object]'

// Math 对象
console.log(Math.toString());  // '[object Math]'
  • 对于 第一部分中提到的 3 个基本包装类型的对象:
  1. 对于布尔对象, 返回字符串 "true" 或 "false", 根据其对应的基本数据类型的值而定.
  2. 对于数值对象, 返回在指定基数下该数的字符串形式, 默认基数是 10, 即默认返回 十进制 的数用引号包裹而成的字符串.
  3. 对于字符串对象, 返回对应基本数据类型的字符串(和调用 valueOf 方法得到的结果相同) .
// 布尔值的包装类型的对象
console.log(new Boolean(false).toString());   // 'false'

// 数值包装类型对象的对象
console.log(new Number(3.14159).toString());  // '3.14159'

// 字符串包装类型对象的对象
console.log(new String('str').toString());    // 'str'
  • 对于数组,返回所有项组成的字符串, 各项之间用 "," 连接.
// 数组
console.log([1, 'hello', false].toString());  // '1,hello,false'
  • 对于 函数,toString方法返回一个字符串,其中包含用于定义函数的源文本段.
// 函数
function foo(){console.log('hello foo');}
console.log(foo.toString());  
// 'function foo(){console.log('hello foo');}'
  • 其他对象
  1. 对于 RegExp 对象,返回该正则表达式的字符串.
  2. 对于 Date 对象, 返回表示特定时间的字符串.
  3. 对于 Error 对象,返回包含错误内容的字符串.
// 正则对象
console.log(new RegExp("a+b+c").toString());      // "/a+b+c/"

// 日期对象
console.log(new Date().toString()); 
// Mon Mar 04 2019 17:07:54 GMT+0800 (中国标准时间)


// Error 对象
console.log(new Error('fatal error').toString()); 
// 'Error: fatal error'

并非每个对象都有 toString() 方法, 例如通过 Object.create 函数传入null 为参数创建出来的对象, 由于它的 prototypenull, 所以没有 toStringvalueOf 方法

2.4 总结

夲节讨论了两个重要的函数 valueOftoString , 前者返回调用者的基本类型的值, 后者可以将一个对象转化为字符串.

这两个函数将在强制类型转换过程中起到重要的作用.

注: 包装和拆包装过程也有其他名称, 例如封装(wrap)和解封(unwrap), 只是同一个过程的不同说法.

posted @ 2019-03-02 22:16  Shikaka  阅读(391)  评论(0编辑  收藏  举报