前端温故知新之数据类型

309 阅读12分钟

I2J6bj.png

1、类型分类

1.1 基本类型(原始类型)

基本类型共有七种,分别是 stringnumberbooleannullundefinedsymbolbigint

1.1.1 精度丢失

当对小数和大整数进行处理时,会出现计算精度不准确问题

console.log(0.1 + 0.2)  // 0.30000000000000004
console.log(0.3 - 0.2)  // 0.09999999999999998
console.log(0.8 * 3)    // 2.4000000000000004
console.log(0.3 / 0.1)  // 2.9999999999999996
console.log(66666666666666999)  // 66666666666667000

小数精度丢失产生原因:计算机数据是以二进制存储的,JavaScript使用的是双精度浮点数编码,存储时位数限制为 64 位(符号位 1 位 + 指数位 11 位 + 小数位 52 位)。当某些十进制转换成二进制时会出现无限循环,如 0.1 的二进制是 0.0001100110011001100...(1100 循环),会造成二进制的舍入操作(0 舍 1 入,第一次精度丢失),当进行运算之后(第二次精度丢失)把结果再转换为十进制后就造成了计算误差,精度丢失。

//二进制
console.logo(0.1.toString(2)) // 0.0001100110011001100110011001100110011001100110011001101(57位,第58位为1,第57位由0变1)
//科学计数法(为了节省存储空间)
1.1001100110011001100110011001100110011001100110011001*2^-4 // 符号位为0(表示正数),指数位为-4,小数位52位,第53位0 舍 1 入
//更高精度查看 0.1 值
console.log(0.1.toPrecision(20)) // 0.10000000000000000555

解决方法一:

对结果进行精度缩小(因为实际业务一般只需要保留的小数位是 2 位或 3 位,所以可以精度缩小来截取小数点后几位),开始准备使用toFixed来保留小数四舍五入,但是因为会有精度误差导致对于小数最后一位为5时进位不正确问题,所以使用Math.round代替toFixed来处理四舍五入

// 3.335 实际对应的数字是 3.334999999999999964,所以在四舍五入时会出现进位不正确

console.log(3.335.toFixed(18)) // '3.334999999999999964'
console.log(3.335.toFixed(2)) // '3.33'

// number-待处理数字,decimal-需要保留的小数位
function toFixedHandle(number, decimal) {
  const sign = (number < 0) ? -1 : 1
  const pow = Math.pow(10, decimal)
  let numResult = sign * Math.round(sign * number * pow) / pow
  //位数不足补零
  const arrayNum = numResult.toString().split(".")
  if(arrayNum.length > 1 && arrayNum[1].length < decimal){
    numResult += new Array(decimal - arrayNum[1].length + 1).join('0')
  }
  return numResult
}
console.log(toFixedHandle(3.335, 2)) // '3.34'
console.log(toFixedHandle(3.335, 18)) // '3.335000000000000000'
console.log(toFixedHandle(0.1 + 0.2, 2)) // '0.30'

解决方法二:

因为整数十进制转二进制时, 是除以二去余数, 这是可以除尽的,所以整数存储和计算时不会出现精度丢失,可以根据小数点后几位乘以倍数变成整数再除以倍数还原(进行运算的值不能超过js的数字最大安全值)

function add(num1, num2) {
 const num1Digits = (num1.toString().split('.')[1] || '').length
 const num2Digits = (num2.toString().split('.')[1] || '').length
 const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits))
 return (num1 * baseNum + num2 * baseNum) / baseNum
}
console.log(add(0.1, 0.2)) // 0.3

解决方法三:

为浮点数计算设置一个误差范围,如果误差能够小于 Number.EPSILON,我们也可以认为结果是可靠的

function numberepsilon(arg1,arg2){
  return Math.abs(arg1 - arg2) < Number.EPSILON
}

console.log(numberepsilon(0.1 + 0.2, 0.3)) // true

解决方法四:(推荐)

使用js库可以轻松解决浮动运算问题,避免小数点后产生多位数和计算精度损失,如number-precisionexact-mathMath.jsdecimal.js

import NP from 'number-precision'
NP.plus(0.1, 0.2)  // 0.3, not 0.30000000000000004
NP.times(3, 0.8) // 2.4, not 2.4000000000000004

大整数精度丢失产生原因:双精度浮点数有效数字有 53 个(符号位+小数位),如果超出了小数点后面 52 位以外的话,就遵从二进制舍 0 进 1 的原则。Number.MAX_SAFE_INTEGER表示最大安全数字,计算结果是9007199254740991,即在这个数范围内不会出现精度丢失(⼩数除外)

console.log(Math.pow(2, 53) - 1) // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER.toString(2)) // 11111111111111111111111111111111111111111111111111111

处理大整数方法:如果后台有传超过最大安全数字的大整数,需要后台将数字修改成字符串或使用第三方库处理。

1.1.2 bigint

为了可以操作超过最大安全数字的数字,ES10引入了新的基本类型bigint,在一个整数字面量后面加n或者调用BigInt函数

console.log(66666666666666999)  // 66666666666667000
console.log(66666666666666999n)  // 66666666666666999n
// BigInt(),注意如果超过安全数需要使用字符串
console.log(BigInt('66666666666666999'))  // 66666666666666999n
console.log(BigInt(66666666666666999))  // 66666666666667000n

1.1.3 symbol

ES6引入了新的基本类型symbolsymbol作为表示独一无二的值,我们可以使用Symbol来作为对象属性名(防止属性名的冲突)或定义类的私有属性/方法

//对象属性名,不能用点运算符
const mySymbol = Symbol('mySymbol')
const mySymbol2 = Symbol('mySymbol2')
let obj = {
  [mySymbol]: 'Hello'
}
obj[mySymbol2] = 'World'
console.log(obj[mySymbol]) // Hello
console.log(obj[mySymbol2]) // World

//类的私有属性,symbol值作为键名,不会被常规方法遍历得到
const Person = (function () {
  const name = Symbol('name')
  class PersonClass {
    constructor(n) {
      this.age = 18
      this[name] = n
    }
    getName() {
      return this[name]
    }
  }
  return PersonClass
})()

const person = new Person('张三')
console.log(person.age)  // 18
console.log(person[Symbol('name')])  // undefined
console.log(person.getName()) // '张三'

1.1.4 NaN

NaN表示某个值不是数字,用于指出数字类型中的错误情况,但是它的类型是number类型,并且它和自身不相等,可以使用isNaNNumber.isNaN()来判断

//本身不相等
console.log(NaN === NaN) // false

//isNaN 在判断传入的值是不是 NaN 是会先将其转换成数字,再判断结果是不是 NaN
console.log(isNaN(123)) // false
console.log(isNaN(NaN)) // true
console.log(isNaN('a123')) // true

//Number.isNaN(ES6引入) 先判断一个值类型是否是数字,在判断值是否值等于NaN
console.log(Number.isNaN('a123')) // false

1.2 引用类型(对象类型)

引用类型只有一种为objectArrayFunctionDateRegExp等都属于原生内置对象,是object的子类型

2、类型存储

  • 基本类型的值直接存储到栈中,存储的是值本身

  • 引用类型的值存储在堆中,存储的仅仅是空间地址(地址是存储在栈中),空间指向堆中存储着对象、数组、函数等值

//基本类型
//复制值
const name = 'name'
let nameCopy = name
nameCopy = 'nameCopy'
console.log(name) // 'name'
console.log(nameCopy) // 'nameCopy'

//函数参数按值传递
let nameValue = 'Hello'
function changeName(value){
  value = 'World'
}
changeName(nameValue)
console.log(nameValue) // 'Hello'

//引用类型
//复制空间地址
const obj = {
  name: 'name'
}
const objCopy = obj
objCopy.name = 'nameCopy'
console.log(obj.name)   // 'nameCopy'
console.log(objCopy.name) // 'nameCopy'

//函数参数按值传递(空间地址)
let objValue = {
  name: 'Hello'
}
function changeName(value){
  value.name = 'World'
}
changeName(objValue)
console.log(objValue.name) // 'World'

3、类型检测

3.1 typeof

  • typeof可以准确的检测基本类型的类型(除了null外),除函数外所有的引用类型都会被判定为object

  • 检测null的类型为objet的原因:js在底层存储变量的时候,会在变量的机器码的低位1~3位存储其类型信息,因为判断object类型标准是前三位为0,而null的所有机器码都为0,所以被当做了object来看待

console.log(typeof 1)               // number
console.log(typeof NaN)             // number
console.log(typeof true)            // boolean
console.log(typeof 'str')           // string
console.log(typeof 555n)           // bigint
console.log(typeof Symbol('a'))    // symbol
console.log(typeof undefined)      // undefined
console.log(typeof null)            // object
console.log(typeof [])             // object
console.log(typeof {})              // object
console.log(typeof function(){})    // function

3.2 instanceof

instanceof可以检测引用类型的类型,判断依据是根据其原型链中是否可以找到该类型,但是无法判断基本类型并且引用类型的原型链都存在Object

console.log([] instanceof Array)  // true
console.log([] instanceof Object) // true
console.log(function() {} instanceof Function)  // true
console.log(function() {} instanceof Object)  // true
console.log(1 instanceof Number)  // false
console.log(new Number(1) instanceof Number)  // true
console.log(new Number(1) instanceof Object)  // true

3.3 constructor

constructor通过判断该变量的构造函数来确认类型(undefinednull没有constructor属性),但是一旦改变了它的原型,constructor判断类型就不准确了

console.log('str'.constructor === String)  // true
console.log(false.constructor === Boolean) // true
console.log((1).constructor === Number)     // true
console.log(([]).constructor === Array)  // true
console.log(([]).constructor === Object)  // false
console.log((function() {}).constructor === Function) // true
console.log(({}).constructor === Object) // true

function Fn(){}
Fn.prototype = new Object()
const f = new Fn()
console.log(f.constructor === Fn)     // false
console.log(f.constructor === Function)  // false
console.log(f.constructor === Object)  // true

3.4 Object.prototype.toString.call(推荐)

  • 当调用Object.prototype.toString时会获取对象的[[Class]][[Class]]是一个字符串,规范定义的对象分类的一个字符串值,它是一个内部属性,且不能被修改)。ES6引入了Symbol.toStringTag允许我们自定义该对象的类型,可通过重定义数据的Symbol.toStringTag来改写Object.prototype.toString.call()返回的数据类型

  • 使用call是为了改变toString函数内部的this指向,从而指向我们需要检测的目标,获取内部属性[[Class]]Object.prototype.toString调用后,若参数为nullundefined,直接返回结果,对于基本类型,会把基本类型用它们相应的引用类型包装起来(装箱),转为对象后,取得该对象的 [[Class]] 属性值

  • 使用Object.prototype.toString而不是obj.toString,是因为ArrayFunction等类型都重写了toString方法

const checkType = Object.prototype.toString
console.log(checkType.call(1))             // [object Number]
console.log(checkType.call(new String(1))) // [object Number]
console.log(checkType.call(new Object(1))) // [object Number]
console.log(checkType.call(true))          // [object Boolean]
console.log(checkType.call('str'))         // [object String]
console.log(checkType.call(undefined))     // [object Undefined]
console.log(checkType.call(null))          // [object Null]
console.log(checkType.call(5n))            // [object BigInt]
console.log(checkType.call(Symbol()))      // [object Symbol]
console.log(checkType.call(new Map()))     // [object Map]
console.log(checkType.call([]))            // [object Array]
console.log(checkType.call(new Date()))    // [object Date]
console.log(checkType.call(function(){}))  // [object Function]
console.log(checkType.call({}))            // [object Object]

//Symbol.toStringTag重定义类型
let myObj = {
  [Symbol.toStringTag]: 'Sdj'
}
console.log(Object.prototype.toString.call(myObj))  // [object Sdj]

//不同对象重写了toString方法
console.log(function () {}.toString())  //'function () {}'
console.log([1, 2, 3].toString())       //'1,2,3'
console.log({}.toString())              //'function () {}'

Object.prototype.toString.call不足之处

  • 使用Object.prototype.toString.call会把基本类型用它们相应的引用类型包装起来,产生很多临时对象(用完之后会销毁),所以可以先使用typeof来提前检测基本类型(注意null的特殊性)

  • 无法区分自定义对象类型,返回的都是object,可以使用instanceof来区分自定义对象类型

const checkType = function (obj) {
  if (obj === null) return 'null'
  if (typeof obj !== 'object') return typeof obj
  return Object.prototype.toString.call(obj).slice(8, -1).toLocaleLowerCase()
}
console.log(checkType(1))            // 'number'
console.log(checkType(true))         // 'boolean'
console.log(checkType('str'))        // 'string'
console.log(checkType(undefined))    // 'undefined'
console.log(checkType(null))         // 'null'
console.log(checkType(5n))           // 'bigint'
console.log(checkType(Symbol()))     // 'symbol'
console.log(checkType(new Map()))    // 'map'
console.log(checkType([]))           // 'array'
console.log(checkType(new Date()))   // 'date'
console.log(checkType(function(){})) // 'function'
console.log(checkType({}))           // 'object'

4、类型转换

4.1 基本类型转引用类型(装箱操作)

4.1.1 基本包装类型

  • 数值、字符串和布尔值都有自己对应的包装对象(如NumberStringBoolean构造函数),通过使用new来调用它们各自的构造函数将基本类型转化为对象的数据可以称为基本包装类型

  • 当我们使用基本类型的属性或方法时,js会在后台隐式的自动创建一个包装类型的对象实例,然后调用属性或方法,最后销毁实例

const str = "abc"
console.log(str.toUpperCase())    // ABC,相当于new String(str).toUpperCase()
  • Object构造函数它可以接收一个任意类型的变量,然后进行不同的转换,其它基本类型也可以传入进去
console.log(new Object('1'))      // String{'1'}
console.log(new Object(1))        // Number{1}
console.log(new Object(true))     // Boolean{true}
console.log(new Object(Symbol())) // Symbol{Symbol()}
console.log(new Object(1n))       // BigInt{1n}

4.2 引用类型转基本类型(拆箱操作)

了解拆箱操作之前需要先了解valueOftoString函数

4.2.1 toString

  • toString返回一个表示该对象的字符串,当对象表示为文本值或以期望的字符串方式被引用时,toString方法被自动调用,当值为数字类型或bigint时可以传参数来定义数字的基数,转换成指定进制的字符串,默认基数为10

  • 基本数据类型调用toString,直接换成字符串,nullundefined原型上没有toString方法

console.log('123a'.toString())      // '123a'
console.log((123).toString())       // '123',整数型使用toString,需要带上括号或定义成变量,不然点运算符(.)会被解释成小数点而报错
console.log((123).toString(2))       // '1111011'
console.log(true.toString())        // 'true'
console.log(123n.toString())        // '123'
console.log(Symbol().toString())    // 'Symbol()'
console.log(null.toString())        // Cannot read properties of null (reading 'toString')
console.log(undefined.toString())   // Cannot read properties of undefined (reading 'toString')
  • 引用数据类型调用toString,根据[[class]]类型的不同来获取不同值
//Object,普通的对象会转化成"[object Object]"
console.log({}.toString())       // "[object Object]"
console.log({a:1}.toString())    // "[object Object]"

//Array 数组,将每一项转换为字符串然后再用","连接,相当于直接调用join()方法
console.log([1,2,3].toString())                 // '1,2,3'
console.log(['','',''].toString())              // ',,'
console.log([].toString())                      // ''
console.log([{ a: 1 }, { a: 2 }].toString())    // '[object Object],[object Object]'

//Function 函数和正则,转成源代码字符串
console.log(function () { return 3 }.toString())                                 // 'function () { return 3 }'
console.log(class Test { constructor(data) { this.data = data } }.toString())   // 'class Test { constructor(data) { this.data = data } }'
console.log(/^1d{10}$/.toString())                                              // '/^1d{10}$/'

//Date 日期,转为本地时区的日期字符串
console.log(new Date().toString())  // 'Tue Nov 09 2021 11:09:37 GMT+0800 (中国标准时间)'

//Map、Set和Promise,原型中存在Symbol.toStringTag属性,所以会返回Symbol.toStringTag类型值
console.log(new Map().toString())         // "[object Map]"
console.log(new Set().toString())         // "[object Set]"
console.log(Promise.resolve().toString()) // "[object Promise]"

// 包装对象直接返回它们基本类型值的字符串
console.log(new Object(1).toString())     // "1"
console.log(new Object(true).toString())  // "true"

4.2.2 valueOf

  • valueOf返回当前对象的基本类型值,具体功能与toString大同小异,同样具有自动调用功能

  • 基本数据类型调用valueOf,直接返回调用者原本的值,类型还是原来类型

  • 引用类型调用valueOf,日期对象会返回一个毫秒数,其它还是返回调用者本身

4.2.3 拆箱操作-toPrimitive

  • 当一个对象转换为对应的基本类型时,会自动调用toPrimitive函数即拆箱操作,ToPrimitive(value, type?)函数中value是调用的对象,type是期望返回的结果类型(numberstring,默认为number

    typestring也就是需要转换成字符串,对象中存在toString()就调用这个方法,如果返回一个基本类型值,js将这个值转换为字符串,如果该对象没有toString()方法或调用之后返回的不是基本类型,则调用valueOf()方法,valueOf()方法返回的如果不是基本类型值则抛出一个类型错误异常,

    typenumber则先调用valueOf(),再调用toString()方法(如果对象为日期则默认typestring

  • ES6引入的Symbol.toPrimitive可以来重新定义toPrimitive的返回值,接收参数类型和toPrimitive一样,只要显示定义了Symbol.toPrimitive,它的返回结果就是作为最终的返回结果,不会再调用toString()valueOf()方法,即Symbol.toPrimitive优先级高于toString()valueOf()

4.3 其它类型转数字类型

4.3.1 Number

Number函数把值转换为数字,如果值无法转换为数字,那么Number()函数返回NaN

//字符串转数字
console.log(Number(''))       // 0
console.log(Number('123'))    // 123
console.log(Number('0x16'))   // 22
console.log(Number('123a'))   // NaN
//布尔类型转数字
console.log(Number(true))   // 1
console.log(Number(false))  // 0
//null转数字
console.log(Number(null))     // 0
//undefined转数字
console.log(Number(undefined))  // NaN
//bigint转数字
console.log(Number(123n))  // 123
//symbol转数字
console.log(Number(Symbol()))  // Uncaught TypeError: Cannot convert a Symbol value to a number

//对象转数字,根据toPrimitive原则,转数字会先调用valueOf()后调用toString()
console.log(Number({}))           // NaN,先valueOf->{},再toString->"[object Object]",最后转换成数字NaN
console.log(Number([]))           // 0,先valueOf->[],再toString->"",最后转换成数字0
console.log(Number([1]))          // 1,先valueOf->[1],再toString->"1",最后转换成数字1
console.log(Number([1,2]))        // NaN,先valueOf->[1,2],再toString->"1,2",最后转换成数字NaN
console.log(Number(new Object('123')))  // 123,先valueOf->'123',因为是基本类型了就不调用toString了,最后转换成数字123
console.log(Number(function(){}))        // NaN,先valueOf->function(){},再toString->"function(){}",最后转换成数字NaN
console.log(Number(new Date()))   // 1636165977196 毫秒数,先valueOf->'1636165977196',最后转换成数字1636165977196

4.3.2 parseInt 和 parseFloat

  • parseInt(str, radix)函数把一个字符串按指定进制解析,解析按从左到右的顺序,如果遇到非数字字符就停止,最后返回一个十进制的整数

    str:要被解析的值,如果参数不是一个字符串,则将其转换为字符串。字符串开头的空白符将会被忽略

    radix:可选参数,数字基数,可以理解为进制,范围为2~36,当radix不填写时默认当做十进制处理,但注意10并不是默认值,当字符串以0x开头是会直接处理为16进制,当解析值超过数字基数则停止解析

//字符串转数字
console.log(parseInt(''))       // NaN
console.log(parseInt('123'))    // 123
console.log(parseInt('0x16'))   // 22
console.log(parseInt('5.66'))   // 5
console.log(parseInt('123aa'))  // 123
//布尔类型转数字
console.log(parseInt(true))   // NaN
//null转数字
console.log(parseInt(null))     // NaN
//undefined转数字
console.log(parseInt(undefined))  // NaN
//bigint转数字
console.log(parseInt(123n))  // 123
//symbol转数字
console.log(parseInt(Symbol()))  // Uncaught TypeError: Cannot convert a Symbol value to a number

//对象转数字,根据toPrimitive原则,转数字会先调用valueOf()后调用toString()
console.log(parseInt({}))           // NaN,先valueOf->{},再toString->"[object Object]"
console.log(parseInt([]))           // NaN,先valueOf->[],再toString->""
console.log(parseInt([1]))          // 1,先valueOf->[1],再toString->"1"
console.log(parseInt([1,2]))        // 1,先valueOf->[1,2],再toString->"1,2"
console.log(parseInt(new Object('123')))  // 123,先valueOf->'123',因为是基本类型了就不调用toString了
console.log(parseInt(function(){}))        // NaN,先valueOf->function(){},再toString->"function(){}"
console.log(parseInt(new Date()))   // NaN
//注意点
//Math.pow 结果转换
console.log(parseInt('123', 6))
// 51,将'123'看作6进制数,返回十进制数,运算的时候不要使用1*6^2 + 2*6^1 + 3*6^0来计算,结果为27,因为^是位运算符,而这里应该使用次方,所以对应的是1*Math.pow(6, 2) + 2*Math.pow(6, 1) + 3*Math.pow(6, 0)

// 科学计数影响
// 当数字被转化为字符串时,如果一个数字为小数,且整数部分为0,且小数点后6位为0,或数字的整数部分大于21位,那么将被转化为科学记数法
console.log(parseInt(0.000001))     // 0
console.log(parseInt(0.0000001))    // 1,科学记数为'1e-7'
console.log(parseInt(0.0000051))    // 0
console.log(parseInt(5.0000001))    // 5
console.log(parseInt('0.0000001'))  // 0

// 字母对应的进制数字
// 字母A~Z(a~z)表示10~35
console.log(parseInt('abc', 11))  // 10,按11来解析,a表示10,b表示11超过基数,所以截止返回10
console.log(parseInt(false, 16))  // 250,按16来解析,f-15,a-10,l-21,结果计算为15*Math.pow(16, 1) + 10*Math.pow(16, 0)

//map, filter等传参
console.log(['1', '2', '3', '12', '26'].map(parseInt))
// [1, NaN, NaN, 3],实际为['1', '2', '3', '5','2'].map((currentValue, index, arr) => parseInt(currentValue, index))
// parseInt('1', 0)  基数为0,默认为十进制,因此是1
// parseInt('2', 1)  基数为1,范围为0,因此是NaN
// parseInt('3', 2)  基数为2,范围为0-1,因此是NaN
// parseInt('12', 3)  基数为3,计算结果1*Math.pow(3, 1) + 2*Math.pow(3, 0),因此是5
// parseInt('26', 4)  基数为4,范围为0-3,6超出范围所以解析停止,即只解析字符串2,计算结果2*Math.pow(4, 0),因此是2

console.log(['1', '2', '3', '12', '26'].filter(parseInt))   // ['1', '12', '26'],同理可得,因为NaN转换为布尔值为false,所以filter过滤掉
  • parseFloatparseInt的区别主要是整数和浮点数的区别,parseFloat没有第二个参数,默认解析为十进制,解析第二个小数点是无效的

4.4 其它类型转字符串类型

4.4.1 String

String函数将值转为字符串,遵循toPrimitive规则,默认先调用toString函数,所以返回结果和toString返回结果相同,除了nullundefined会直接返回它们本身的字符串和不能像(123).toString(2)那样返回数字的其它进制字符串

4.5 其它类型转布尔类型

4.5.1 Boolean

Boolean函数将值转为布尔值,除了undefined, null, +0, -0, NaN, '', false会转换为false,其它为true

console.log(Boolean(undefined))            // false
console.log(Boolean(null))                 // false
console.log(Boolean(0))                    // false
console.log(Boolean(0n))                   // false
console.log(Boolean(NaN))                  // false
console.log(Boolean(''))                   // false
console.log(Boolean(false))                // false
console.log(Boolean(''))                   // false
console.log(Boolean('0'))                  // true
console.log(Boolean(-Infinity))            // true
console.log(Boolean({}))                   // true
console.log(Boolean([]))                   // true
console.log(Boolean(new Boolean(false)))   // true

4.6 各类操作符引起的隐式转换

当运算符在运算时,如果两边数据类型不统一,CPU无法计算,这时编译器会自动将运算符两边的数据做一个数据类型转换,转换成一样的数据类型在进行运算,不过隐式转换是一把双刃剑,使用它虽然可以写更少的代码但有时会出现难以被发现的bug,所以需要更多的了解转换规则(ts不允许有随意的隐式类型转换)

4.6.1 比较运算符(==,>)

//基本类型和基本类型对比,不同类型通常会转化成数字类型比较
console.log(null == undefined)    // true,undefined、null除了和它自身以及对方相等之外,和其它的比较都为false
console.log(null == false)        // false
console.log(NaN == NaN)           // false
console.log(true == 1)           // true,true转化成1
console.log('true' == 1)         // false,这里true为字符串转化成NaN
console.log('' == 0)           // true,空字符串转化成0
console.log('c' > 'ab')        //true, 当两边都是字符串的时候,字符串是按字符逐个进行比较的,根据unicode编码顺序
console.log( 1 > 'ab')        //true, 字符串会转化成NaN,和任何值比较都是false
console.log( null >= 0)       //true, null会转化成0,它和==号判断逻辑是不同的
console.log( undefined >= 0)  //false, undefined会转化成NaN,和任何值比较都是false,不要使用 >= > < <= 去比较一个可能为 null/undefined 的变量

//存在引用类型,需要先把引用类型按照ToPrimitive规则转换成基本类型
console.log({} == true)      // false,{}转换成"[object Object]",true转换成1,"[object Object]"又转换成NaN
console.log({} == {})        // false,比较的是空间地址指向,不同的对象空间地址是不同的
console.log([] == 0)         // true,[]先转换成空字符串,再由空字符串转换成0
console.log([null] == false) // true,[null]先转换成null字符串,再由null字符串转换成0,false转换成0
console.log(['1'] == ['1'])  // false,空间地址指向不同
console.log([] > -1)         // true,[]先转换成空字符串,再由空字符串转换成数字0
console.log(['1'] > 0)       // true,['1']先转换成字符串1,再由1字符串转成数字1

4.6.2 逻辑运算符(!、||、&&)

  • ||运算符两边的语句会转化为布尔类型,从左到右依次计算操作数,如果结果是true,就停止计算,返回这个操作数的初始值,计算到最后则返回最后一个操作数
console.log(true || false)             // true
console.log(null || false || 'aa')     // aa
console.log(true || 2 && false)       // true,与运算 && 的优先级比或运算 || 要高,相当于true || (2 && false)
console.log(null || true && false)    // false
  • &&运算符两边的语句会转化为布尔类型,从左到右依次计算操作数,如果结果是false,就停止计算,返回这个操作数的初始值,计算到最后则返回最后一个操作数
console.log(true && false)             // false
console.log(null && false && 'aa')     // null
console.log(null && 1 || true && 0)    // 0,相当于(null && 1) || (true && 0)
  • !运算符将操值转化为布尔类型,然后取反
console.log(!'11')       // false11转换为truetrue取反为false
console.log(!!'11')      // true11转换为truetrue取反为falsefalse再取反为true
console.log(!{} == [])   // true,!{}转换为falsefalse转换为0,[]转换为空字符串,空字符串转换为0

4.6.3 算数运算符(+、-、*、/)

  • - * /等运算符会把符号两边转成数字来进行运算
console.log(1 - true)            // 0
console.log(1 * [3])             // 3,遵循toPrimitive规则,先通过valueOf,在通过toString转换为字符串3,最后字符串转为数字3
console.log(1 * {})              // NaN
console.log(1 * null)            // 0
console.log(1 * undefined )      // NaN
  • +运算符作为一元运算符时,直接转换为数字类型,相当于Number函数

  • +运算符作为二元运算符时,当有一侧为字符串或引用类型时,二边都会转为字符串拼接,否则两边转成数字来进行运算

console.log(+ '1a')     // NaN
console.log(+ true)     // 1
console.log(+ {})       // NaN
console.log(+ [])       // 0

console.log('a' + 1)    // 'a1',一侧有字符串
console.log({} + 1)     // '[object Object]1',一侧有引用类型
console.log([2] + 1)     // '21',一侧有引用类型
console.log(true + 1)   // 2
console.log([] + [])   // ""
console.log({} + {})   // "[object Object][object Object]"
{} + []                // 0,当语句开始为'{'时会把'{}'当作区块语句而不是对象字面量,相当于 +[] 语句,即相当于强制求出数字值的 Number([]) 运算
{a: 1} + [1,2]         // NaN,同理+[1,2],Number([1,2])转换为NaN
({}) + []               // "[object Object]",使用括号包起来当做对象字面量而不是区块语句,也可以定义成变量相加
console.log({} + [])   // "[object Object]"
console.log([] + {})   // "[object Object]"

5、其它扩展

5.1 重写 toString、valueOf 或 Symbol.toPrimitive

let str = {
  toString () {
    console.log('toString')
    return 5
  },
  valueOf () {
    console.log('valueOf')
    return [1, 2, 3]
  }
}
console.log(Number(str))    // valueOf toString  5,根据toPrimitive规则,需要转化成数字,所以先调用valueOf,再调用toString

let str = {
  toString () {
    console.log('toString')
    return 5
  },
  valueOf () {
    console.log('valueOf')
    return [1, 2, 3]
  },
  [Symbol.toPrimitive] () {
    console.log('symbol')
    return 2
  }
}
console.log(Number(str))  // symbol 2,Symbol.toPrimitive的优先级,并且不会在调用valueOf和toString

5.2 (a == 1 && a == 2 && a == 3)条件成立

自定义 valueOf、toString 或 Symbol.toPrimitive

let a = {
  value: 0,
  valueOf () {
    return ++this.value
  }
}
console.log(a == 1 && a == 2 && a == 3)   // true,==每次都会触发toPrimitive规则,valueOf可以更换成toString或Symbol.toPrimitive

数组隐式调用 join

let a = [1, 2, 3]
a['join'] = function () {
  return this.shift()
}
console.log(a == 1 && a == 2 && a == 3)   // true,==触发toPrimitive规则,toString时对于数组会自动调用join函数,所以可以重写覆盖join函数

数据劫持 Object.defineProperty 或 Proxy

数据劫持的方法不仅可以判断(a == 1 && a == 2 && a == 3),还可以判断全等于(a === 1 && a === 2 && a === 3),===全等于不会触发隐式转换,也就不会遵循 toPrimitive 规则,所以全等于不能使用 toString 来触发

// Object.defineProperty
let value = 1
Object.defineProperty(window, "a", {
  get() {
    return this.value++
  }
})

console.log(a == 1 && a == 2 && a == 3)   // true,劫持全局变量window上的属性a,每次调用读取a的值时会触发get函数

// Proxy
let test = {
  count: 1
}
test = new Proxy(test, {
  get(target, key) {
    return target[key]++
  }
})
console.log(test.count == 1 && test.count == 2 && test.count == 3)   // true,对于window对象没有效果,所以暂时使用一个对象替代下

5.3 函数柯里化

柯里化是把使用多个参数的一个函数转换成一系列使用一个参数的函数(只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数)。实现方式需要依赖闭包以及递归,通过拆分参数的方式,来调用一个多参数的函数

函数柯里化和普通函数对比

//普通函数
function add(x, y, z){
  return x + y + z
}
//函数柯里化
function curryingAdd(x) {
  return function (y) {
    return function (z) {
      return x + y + z
    }
  }
}
add(1, 2, 3)           // 6
curryingAdd(1)(2)(3)   // 6,先用一个函数接收 x 然后返回一个函数去处理 y 参数最后返回一个函数去处理 z 参数

函数柯里化参数长度固定

function sum(a, b, c) {
  return a + b + c
}
// 多次比较接受的参数总数与函数定义时的入参数量,fn.length为sum函数定义的入参数量3
function curry(fn, ...args) {
  if (args.length >= fn.length) {
      return fn(...args)
  }
  //第一种返回,合并上一次缓存的参数和本次传入的参数
  return function (...args2) {
    return curry(fn, ...args, ...args2)
  }
  // 第二种返回,bind 绑定数据
  // return curry.bind(null, fn, ...args)
}
let add = curry(sum)
console.log(add(1)(2)(3))
console.log(add(1, 2)(3))
console.log(add(1, 2, 3))

函数柯里化参数长度不固定

function sum(...args) {
  return args.reduce((a, b) => a + b)
}
//第一种,判断没有参数传入时,返回结果
function curry(fn) {
  let args = []
  return function temp(...newArgs) {
    if (newArgs.length) {
      args = [...args, ...newArgs]
      return temp
    } else {
      let val = fn.apply(this, args)
      args = [] //保证再次调用时清空
      return val
    }
  }
}
let add = curry(sum)
console.log(add(1)(2)(3)(4)()) //10
console.log(add(1)(2)(3, 4, 5)()) //15
console.log(add(1)(2, 3, 4, 5)(6)()) //21

//第二种,根据toPrimitive规则自动触发toString和valueOf函数
function add() {
  // 第一次执行时,定义一个数组专门用来存储所有的参数
  let _args = Array.prototype.slice.call(arguments)
  // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
  let _adder = function() {
    _args.push(...arguments)
    return _adder
  }
  _adder.toString = function () {
    return _args.reduce((a, b) => a + b)
  }
  return _adder
}
//注意需要使用一元运算符或String等函数来触发隐式转换,从而执行toString函数
console.log(+add(1)(2)(3)(4)())                 //10
console.log(String(add(1)(2)(3, 4, 5)()))       //15
console.log(Number(add(1)(2, 3, 4, 5)(6)()))    //21

参考