Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

《你不知道的javascript(中)》1.4 强制类型转换 #29

Open
qunzi0214 opened this issue Feb 19, 2021 · 0 comments
Open

《你不知道的javascript(中)》1.4 强制类型转换 #29

qunzi0214 opened this issue Feb 19, 2021 · 0 comments
Labels
read book 读书笔记

Comments

@qunzi0214
Copy link
Owner

qunzi0214 commented Feb 19, 2021

值类型转换

应该将JavaScript中的类型转换区分为:显式强制类型转换(发生在编译阶段)、隐式强制类型转换(发生在运行时)

强制类型转换总是返回基本类型值,不会返回对象或函数。为基本类型值(除 object )封装一个相应类型的对象,并非严格意义上的强制类型转换

var a = 42
var b = a + '' // 隐式强制类型转换
var c = String(a) // 显式强制类型转换

除了字面意思意外,两种转换在行为特征上也有一定差别

抽象值操作

ToString

基本类型值的字符串化规则为:

  • null 转化为 'null'

  • undefined 转化为 'undefined'

  • true 转化为 'true'

  • 数字遵循通用规则,不过极大或极小的数字会使用指数形式

    var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000
    a.toString() // '1.07e+21'
  • 对于普通对象来说,除非自行定义,否则会调用 Object.prototype.toString() 返回内部属性 [[Class]] 的值

  • 数组的 toString() 方法重新定义过

    [1, 2, 3].toString() // '1,2,3'

工具函数 JSON.stringify() 在将JSON对象序列化为字符串时也会调用 toString() ,但是它并非是严格意义上的强制类型转换。对于大多数简单值来说,JSON.stringify()toString() 效果相同

JSON.stringify('aa') // ""aa""
JSON.stringify(42) // "42"
JSON.stringify(null) // "null"
JSON.stringify(true) // "true"

所有安全的JSON值(JSON-safe)都可以用 JSON.stringify() 字符串化。对于不安全的JSON值( undefined function symbol 循环引用的对象 ):

  • 在对象中遇到 undefined function symbol 会自动忽略,在数组中遇到则返回 null

    JSON.stringify(undefined) // undefined
    JSON.stringify(() => {}) // undefined
    JSON.stringify([1, undefined, () => {}, 4]) // "[1,null,null,4]"
    JSON.stringify({ a: 1, b: () => {}, c: undefined }) // "{"a":1}"
  • 对包含循环引用的对象,则会报错

  • 如果要对含有非法JSON值的对象做字符串化,或者对象中的某些值无法被序列化时,需要定义 toJSON() 方法来返回一个安全的JSON值,JSON.stringify() 会先调用该方法并用它的返回值做序列化操作(toJSON() 应该返回一个可以被字符串化的安全的JSON值,而不是返回一个字符串)

    var o = {}
    var a = {
      b: 42,
      c: o,
      d: () => {}
    }
    
    // 创建一个循环引用
    o.e = a
    
    JSON.stringify(a) // TypeError: Converting circular structure to JSON
    
    a.toJSON = function() {
      return { b: this.b }
    }
    
    JSON.stringify(a) // "{"b":42}"

JSON.stringify() 可以传递两个可选参数

  • replacer :如果是数组,必须是字符串数组,包含需要序列化的属性名,其余属性会被忽略;如果是函数,会对对象本身调用一次,然后对对象中的每个属性各调用一次。如果要忽略某个键就返回 undefined ,否则返回指定的值。同时,如果遇到键对应的值也是对象,则递归调用

    var  a = {
      b: 42,
      c: '42',
      d: [1, 2, 3],
      e: {
        b: 1,
        c: 2
      }
    }
    
    JSON.stringify(a, ['b', 'c']) // "{"b":42,"c":"42"}"
    JSON.stringify(a, function (k, v) {
      if (k !== 'c') return v
    }) // "{"b":42,"d":[1,2,3],"e":{"b":1}}"
  • space :用来指定缩进格式,为整数时指定每一级缩进的字符数,为字符串时前十个字符被用于每一级的缩进

    var  a = {
      b: 42,
      c: '42',
      d: [1, 2, 3],
    }
    
    JSON.stringify(a, null, 3)
    
    // "{
    //    "b": 42,
    //    "c": "42",
    //    "d": [
    //       1,
    //       2,
    //       3
    //    ]
    // }"
    
    JSON.stringify(a, null, '-----')
    
    // "{
    // -----"b": 42,
    // -----"c": "42",
    // -----"d": [
    // ----------1,
    // ----------2,
    // ----------3
    // -----]
    // }"

ToNumber

如果需要将非数字值当做数字使用,遵循以下规则:

  • true 转化为 1
  • false 转化为 0
  • undefined 转化为 NaN
  • null 转化为 0
  • 对于字符串,遵循数字常量的相关规则/语法,处理失败返回 NaN (报错?)
  • 对象会首先被转化为基本类型值,如果非数字,则按照以上规则转化

ToPrimitive

为了将值转化为相应的基本类型值,首先会去检查该值是否有 valueOf() 方法,如果有且返回基本类型值,会使用该基本类型值进行强制类型转换。如果没有则使用 toString() 的返回值来进行强制类型转换。如果这两种方式都不返回基本类型值,会产生 TypeError 错误

使用 Object.create(null) 创建的对象 [[Prototype]] 属性为 null ,不存在 valueOf()toString() 方法,因此无法进行强制类型转换

var a = {
  valueOf: () => '42',
}

var b = {
  toString: () => '42',
}

var c = [4, 2]
c.toString = function () {
  return this.join('')
}

Number(a) // 42
Number(b) // 42
Number(c) // 42
Number('') // 0
Number([]) // 0
Number(['abc']) // NaN

ToBoolean

Javascript规范具体定义了一小撮可以被强制类型转换为 false 的值,其余的则都是 true

假值列表:

  • undefined
  • null
  • false
  • +0 -0 NaN
  • ''

显式强制类型转换

字符串和数字

字符串和数字的互相转换是通过 String()Number() 两个内建函数来实现的

var a = 42
var b = String(a) // '42'

var c = '3.14'
var d = Number(c) // 3.14

// 注意这里没有new关键字,不会创建封装对象

一些其他的方式:

  • +

    var a = 42
    var b = a.toString() // '42'
    
    var c = '3.14'
    var d = +c // 3.14
    
    // 不推荐用一元运算符来显式强制类型转换,会导致代码难以理解
    // 同时 + 还可以将日期对象转换为时间戳,同样不推荐
    
    var e = new Date('Mon, 18 Aug 2014 08:53:06 CDT')
    +e // 1408369986000
  • ~:位操作符会通过抽象操作 ToInt32 强制操作数使用32位格式,位非 ~x 基本等于 -(x+1) 。对于正数,位反后拿到的是补码,需要转换为原码。对于负数,先取补码再位反。(MDN例子)

    补码的存在是为了正确计算负数的加法(借助溢出),以8位有符号位数字举例

    正数的补码是其本身,负数的补码符号位不变,其余按位取反并+1

    补码转换为原码同样是符号位不变,其余按位取反并+1

    原码 补码
    1 + (-2) 00000001
    +
    10000010
    1000011 = -3
    00000001
    +
    11111110
    11111111 = -1
    1 + (-1) 00000001
    +
    10000001
    1000010 = -2
    00000001
    +
    11111111
    00000000 = 0(溢出8位,从0开始)

    -(x+1) 中唯一能得到 0x 值是 -1,这是个哨位值,通常被赋予特殊含义,例如字符串 indexOf() 方法在没有找到相应字符串时返回 -1

    var a = 'Hello World'
    
    if (a.indexOf('lo') >= 0) {
      // 写法不好,抽象渗漏(暴露底层实现细节)
    }
    
    if (a.indexOf('lo') != -1) {
      // 写法不好,抽象渗漏(暴露底层实现细节)
    }
    
    if (~a.indexOf('lo')) {
      // 将结果强制类型转换为真假值!
    }

显式解析数字字符串

解析字符串中的数字,和将字符串强制类型转换为数字是有区别的:解析允许字符串中含有非数字字符,而转换不允许

var a = '42'
var b = '42px'

Number(a) // 42
parseInt(a) // 42
Number(b) // NaN
parseInt(b) // 42

ES5之前,parseInt() 如果不传递第二个参数指定进制,会自行根据字符串的第一个字符来决定进制。除此之外,不要传递非字符串的其他参数,如果传入参数非字符串,会先隐式转换为字符串

parseInt(1 / 0, 19) // parseInt('Infinity', 19) 第一个字符为'I',以19位基数时值为18,第二个字符不是有效数字,解析结束
parseInt(0.000008) // 0
parseInt(0.0000008) // 8 '8e-7'
parseInt(false, 16) // 250 'fa' = 250
parseInt(parseInt, 16) // 15 'f' 来自于function () {...}
parseInt('0x10') // 16
parseInit('103', 2) // 2 到3停止,因为3不是有效的二进制数字

显式转换为布尔值

Boolean() (不带new)是显式的 ToBoolean 强制类型转换

var a = '0'
var b = []
var c = {}

var d = ''
var e = 0
var f = null
var g

Boolean(a) // true
Boolean(b) // true
Boolean(c) // true

Boolean(d) // false
Boolean(e) // false
Boolean(f) // false
Boolean(g) // false

但这种方式并不常用,一元运算符 ! 也可以将值显式强制类型转换为布尔值,因为 ! 还会反转真假值,因此最常用的方式是 !! ,在 if 语句中,会自动隐式的进行 ToBoolean 转换,建议用显式的方式让代码更易理解

隐式强制类型转换

因人而异,对于自己来说不够明显的强制类型转换都可以称作隐式强制类型转换,会导致代码晦涩难懂。但从另一个角度来看,也可以减少代码的冗余,让代码更简洁

字符串和数字之间的隐式强制类型转换

根据ES5规范,如果 + 的某个操作数是字符串或者能够通过以下步骤转换为字符串,则进行拼接操作:

  • 对其进行 ToPrimitive 抽象操作
  • 调用 [[DefaultValue]]

否则执行数字加法

var a = '42'
var b = '0'

a + b // '420'

var c = 42
var d = 0

c + d // 42

var e = [1, 2]
var f = [3, 4]

e + f // '1,23,4' 对数组进行ToPrimitive抽象操作,发现valueOf()方法不能返回基本类型值,转而使用toString()

通过 + 空字符串来隐式强制类型转换数字为字符串的场景非常多见,但和显式强制类型转换略有不同:

  • a + '' 依据 ToPrimitive 抽象操作规则,先对 a 调用 valueOf() 方法,然后通过 ToString 抽象操作将返回值转换为字符串
  • String(a) 则是直接调用 ToString 抽象操作
var a = {
  valueOf: () => 42,
  toString: () => 4
}

a + '' // '42'
String(a) // '4'

相应的,- * / 都是数字运算符,因此会将操作数强制类型转化为数字。

var a = 1
var b = '2'

b - a // 1
a * b // 2
b / a // 2

var c = [3]
var d = [1]

c - d // 2 c和d首先被转化为字符串,然后再转化为数字

布尔值到数字的隐式强制类型转换

将某些复杂的布尔逻辑转化为数字加法的时候可以派上大用场,但是情况非常罕见

function onlyOne() {
  var sum = 0
  for (var i = 0; i < arguments.length; i++) {
    if (arguments[i]) {
      sum += arguments[i]
    }
  }
  return sum == 1
}

var a = true
var b = false

onlyOne(b, a) // true
onlyOne(b, a, b, b, b, b) // true

隐式强制类型转换为布尔值

  1. if() 语句中的条件判断表达式
  2. for(..; ..; ..) 语句中的条件判断表达式(第二个)
  3. while()do..while() 循环中的条件判断表达式
  4. ?: 语句中的条件判断表达式
  5. 逻辑运算符 ||&& 的左操作数

对于javascript中的逻辑运算符(实际上更像是操作数选择器运算符),并不是返回一个布尔值,而是返回两个操作数中的一个。 ||&& 首先对第一个操作数进行 ToBoolean 抽象操作,然后再执行条件判断。对于 || ,如果第一个操作数判断结果为 true 则返回第一个操作数的值,为 false 则返回第二个操作数的值。而对于 && ,如果第一个操作数判断结果为 true 就返回第二个操作数的值,为 false 则返回第一个操作数的值

之所以我们在 if 或其他条件判断语句中可以用 &&||,是因为这些语句本身会做隐式强制类型转换

宽松相等和严格相等

常见的误区:“ == 检查值是否相等,=== 检查值和类型是否相等”。正确的解释是:“ == 允许在相等比较中进行强制类型转换,而 === 不允许。”延伸出来的性能结论:实际上 ===== 在比较过程中做的事情更多(相比误区说法),所以性能上确实会慢上一点点(可以忽略不计)

抽象相等

ES5规范如下:

  • 如果两个值类型相同,就仅比较他们是否相等(NaN 不等于 NaN+0 等于 -0
  • 对象指向同一个值时,视为相等,不发生强制类型转换
  • == 在比较两个不同类型的值时,会发生隐式强制类型转换,将其中之一或两者都转换为相同类型再比较,转换规则如下:
    • 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果,反之亦然
    • 如果 Type(x) 是布尔值,则返回 ToNumber(x) == yType(y) 亦然),因此不要使用布尔值的宽松相等,会和预期完全不符
    • nullundefined 在比较中可以相互强制类型转换,两者等价
    • 如果 Type(x) 是字符串或数字,Type(y) 对象,则返回 x == ToPrimitive(y) 的结果,反之亦然

比较少见的情况

更改对象原型

Number.prototype.valueOf = () => 3
new Number(2) == 3 // true
// 因为数字对象拆封涉及到ToPrimitive操作

假值的相等比较

false == 0 // true
// false优先转换为0,即 0 == 0
false == '0' // true
// false优先转换为0,即 0 == '0',两个操作数分别是数字和字符串,转换为 0 == Number('0')
false == '' // true
// false优先转换为0,即 0 == '',两个操作数分别是数字和字符串,转换为 0 == Number('')
false == [] // true
// false优先转换为0,[]通过ToPrimitive转换为''
[] == ![] // true
// 右侧进行了布尔值的显式强制类型转换,即 [] == false

总结:

  • 如果比较的两个值存在布尔值,千万不要使用 ==
  • 如果比较的两个值存在 0 '' [] ,尽量不要使用 ==
  • 干脆就不要使用 == (个人理解)

抽象关系比较

根据ES5规范,首先对比较双方调用 ToPrimitive 操作

  • 如果结果出现非字符串,就根据 ToNumber 将比较双方强制类型转换为数字比较
  • 如果比较双方都是字符串,则按照字母顺序来比较

这里原本书中的例子有问题

var a = 42 
var b = '43'

a < b // true (42 < 43)

var a = ['42']
var b = ['043']

a < b // false ('42' < '043',在字母顺序上,0小于4)

需要注意,根据规范,a <= b 会被处理成 b < a 再将结果反转。所以实际上JavaScript中 <= 是不大于的意思(而不是通常理解的小于等于),>= 同理。这会产生一些悖论:

var a = {}
var b = {}

a < b // false 都是 '[object object]'
a == b // false 指向地址不是一致的!
a > b // false

a <= b // true
a >= b // true

相等比较有严格相等,但是关系比较却没有严格关系比较,想要避免在关系比较中出现隐式强制类型转换,只能确保比较两者类型相同,别无他法

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
read book 读书笔记
Projects
None yet
Development

No branches or pull requests

1 participant