JavaScript 基础 - 数据类型、运算符、类型转换

500 阅读19分钟

数据类型

原始(Primitive)类型

原始(Primitive)值一般叫做栈数据(一旦开了个房间、不可能在这个房间里对其进行修改)

JS 中,存在着以下几种原始值,分别是:

  • number(typeof 1 === "number")
  • string(typeof '' === 'string')
  • boolean(typeof true === 'boolean')
  • null(typeof null === 'object')
  • undefined(typeof undefined === 'undefined')
  • symbol(typeof Symbol() === 'symbol')
  • bigInt(typeof 10n === 'bigint')(没有正式发布但即将被加入标准的原始类型)

原始类型存储的都是,是没有函数可以调用的,如 undefined.toString() 会报错,一般看到的 '1'.toString() 可以调用成功是因为实际上它已经被强制转换成了 String 类型即对象类型,所以可以调用 toString 函数

image.png

undefined 和 null 的区别

相同点:用 if 判断时两者都会被转换成 false

不同点:

  • number 转换的值不同

    Number(null); // 0
    Number(undefined); // NaN
    
  • null 有值,但这个值是空值,表示为空,代表此处不应该有值的存在。一个对象可以是 null 代表是个空对象,null 一般用作占位

  • undefined 是未定义,表示 不存在,完全没有值的意思。JavaScript 是一⻔动态类型语言,成员除了表示存在的空值外还有可能根本就不存在(因为存不存在只在运行期才知道),这就是 undefined 的意义所在

typeof null 的结果为什么是 object

typeof null 会输出 object,这是 JS 存在的一个悠久 Bug,在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object

为什么 0.1 + 0.2 !== 0.3?

分析详见: 为什么 0.1 + 0.2 !== 0.3

如何判断一个数据是 NaN?

NaN 是非数字,但使用 typeof 检测是 number 类型

typeof(NaN); // 'number'

判断一个数据是 NaN 的方法如下:

  • 利用 NaN 的定义:用 typeof 检测是否是 number 类型且判断是否满足 isNaN

  • 利用 NaN 是唯一一个不等于任何自身的特点 n !== n

  • 利用 ES6 中提供的 Object.is() 方法,判断两个值是否相等 n == NaN

为什么会有 BigInt 的提案?

JavaScript 中 Number.MAX_SAFE_INTEGER 表示最大安全数字,计算结果是 9007199254740991,即在这个数范围内不会出现精度丢失(小数除外)

但一旦超过这个范围,JS 就会出现计算不准确的情况,这在大数计算时不得不依靠一些第三方库进行解决,因此官方提出了 BigInt 来解决此问题

BigInt 类型可以用任意精度表示整数,使用 BigInt 可以安全地存储和操作大整数,甚至可以超过数字的安全整数限制,BigInt 是通过在整数末尾附加 n 或调用构造函数来创建的

引用类型

一般叫做堆数据,包括:

  • 对象(Object)(typeof {} === 'object')
  • 数组(Array)(typeof [] === 'object')
  • 函数(Function)(typeof function(){} === 'function')

引用类型原始类型的区别:

  • 因为原始值是存放在栈里的,而引用值是存放在堆里的,原始值不可以被改变,引用值可以被改变

  • 原始值的赋值是把值的内容赋值一份给另一个变量,栈内存一旦被赋值了就不可以改变,即使给 num 重新赋值为 234,也是在栈里面重新开辟了一块空间赋值 234,然后把 num 指向了这个空间,前面那个存放 123 的空间还存在

  • 但引用值却不是这样:引用值的变量名存在栈里,但是值却是存在堆里,栈里的变量名只是个指针并指向了一个堆空间,这个堆空间存的是一开始赋的值,当 arr1 = arr 时,其实是把 arr1 指向了和 arr 指向的同一个堆空间,这样当改变 arr 的内容时,其实就是改变了这个堆空间的内容,自然同样指向这个堆空间的 arr1 的值也随着改变

    // num的改变对num1完全没有影响
    var num = 123, num1 = num;
    num = 234;
    console.log(num) // 234
    console.log(num1) // 123
    
    // 只是改变了 arr 的值,但 arr1 也跟着改变了
    var arr = [1,2,3], arr1 = arr;
    arr.push(4)
    console.log(arr) // [1,2,3,4]
    console.log(arr1) // [1,2,3,4]
    

    再来看个函数参数是对象的情况

    function test(person) {
      person.age = 26
      person = {
        name: 'yyy',
        age: 30
      }
      return person
    }
    const p1 = {
      name: 'yck',
      age: 25
    }
    const p2 = test(p1)
    console.log(p1) // {name: "yck", age: 26}
    console.log(p2) // {name: "yyy", age: 30}
    

    (1)上面代码中,首先函数传参是传递对象指针的副本
    (2)到函数内部修改参数的属性这步,当前 p1 的值也被修改了
    (3)但当重新为 person 分配了一个对象时就出现了分歧,请看下图,所以最 后 person 拥有了一个新的地址(指针),即和 p1 没有任何关系,导致了 最终两个变量的值是不相同的 image.png

  • 无法直接操纵堆中的数据,即无法直接操纵对象,但可通过栈中对对象的引用来操作对象,就像通过遥控机操作电视机一样,区别在于这个电视机本身并没有控制按钮

为什么要区分堆栈?

变量的主要形式:

  • 一种内容短小(如 int 整数),需要频繁访问,但生命周期很短,通常只在一个方法内存活
  • 一种内容可能很多(如很长一段字符串),可能不需要太频繁的访问,生命周期较长,通常很多方法中可能都要用到

堆区就是各种慢:申请内存慢、访问慢、修改慢、释放慢、整理慢(或说 GC 垃圾回收机制),但优点不言而喻:访问随机灵活、空间超大、在不超可用内存的情况下要多大就给多大

栈区速度超快,但缺点如生命周期短,一般只能在一个方法内存活;需事先知道需要多大的栈(事实上绝大多数语言栈区要分配的大小在编译期就确定了,如 Java),通常最大栈区可用内存都很小,不可能往栈区堆很多数据

不将原始类型放在堆是因为是为了不影响栈的效率,且通过引用到堆中查找实际对象是要花费时间的,而这个综合成本远大于直接从栈中取得实际值的成本,所以原始类型值直接存放在栈中

堆和栈分别是不同的数据结构:是线性表的一种,而则是树形结构

运算符

算术运算符(算术运算符的优先级是从左到右的)

  • +:数学上的相加功能、拼接字符串(字符串和任何数据相加都会变成字符串)
  • /*///%:分别对应数学上的相减、相乘、相除、取余功能
  • =:赋值运算符,优先级最低
  • ():和数学上一样,加括号的部分优先级最高
  • ++:自加 1 运算,当写在变量前时是先自加 1 再执行运算,写在变量后时是先运算再自加 1
  • --:用法和 ++ 一样,不过是减法操作
  • +=:让变量自加多少
  • 相同的还有 -=、/=、*-、%= 等等

比较运算符

  • 比较运算符有 > 、< 、>= 、<= 、!= 、== 不严格等于、===严格等于
  • ===== 的区别:当比较两个数据时,是否先转化成同一个类型的数据后再进行比较
    • == 即这两个数据进行了类型转换后值相等则相等
    • === 则是两个数据不进行数据转化也相等时则相等

注:NaN 不等于任何数据包括它本身,nullundefined 就等于它本身

逻辑运算符

  • 逻辑运算符主要是 与(&&)或(||)

  • &&:只有是 true 时才会继续往后执行,一旦第一个表达式错了后面的第二个表达式根本不执行;若表达式的返回结果都是 true 则这里 && 的返回结果是最后一个正确的表达式的结果

  • ||:只要有一个表达式是 true 则结束,后面的就不走了且返回的结果是这个正确的表达式的结果,若都是 false 则返回结果就是 false

  • 一般来说,&& 有当做短路语句的作用,因为运算符的运算特点,只有第一个条件成立时才会运行第二个表达式,所以可以把简单的 if 语句用 && 来表现出来

  • 一般来说,|| 有当做赋初值的作用,有时希望函数参数有一个初始值,在不使用 ES6 的语法的情况下,最好的做法就是利用 || 语句

注意:这里有一个缺点,当传的参数是一个布尔值且传的是 false,则 || 语句的特点就会忽略掉所传的这个参数值而去赋成默认的初始值,所以为了解决这个弊端,就需利用 ES6 的一些知识

默认为 false 的值:undefinednull" "0-0falseNaN

类型转换

显示类型转换

typeof 能返回的类型一共有:numnerstringbooleanundefinedsymbolbigIntobjectfunction

  • 数组null 的都返回 'object'

  • NaN 属于 number 类型:虽然是非数,但非数也是数字的一种

  • Number(mix):该方法可以把其他类型的数据转换成数字类型的数据

  • parseInt(string, radix):该方法是将字符串转换成整型数字类型的

    • 第二个参数 radix 是可选择的参数
    • string 里既包括数字又包括其他字符时会从左到右只会转换数字部分,遇到其他非数字的字符就停止,即使后面还有数字也不会继续转换
    • radix 不为空时,该函数可用来作为进制转换,radix 作用则是把第一个参数的数字当成几进制的数字来转换成十进制(radix 参数的范围是 2 ~ 36
      // String(100000000000000000000000) -> "1e+23"
      parseInt(100000000000000000000000);  // 1
      
  • parseFloat(string, radix):这个方法和 parseInt 类似,将字符串转换成浮点类型的数字,同样是碰到第一个非数字型字符停止

    • 由于浮点型数据有小数点,它会识别第一个小数点以及后面的数字,但第二个小数点则无法识别

    • 一旦数字变得足够大,其字符串表示将以指数形式呈现。如下,此时得到的是 1

      parseFloat(100000000000000000000000); // 1e+23
      
  • toString(radix):它是对象上的方法,任何数据类型都可使用,转换成字符串类型

    • 同样 radix 基底是可选参数,当为空时仅仅代表将数据转化成字符串

    • 当写了 radix 基底时则代表要将这个数字转化成几进制的数字型字符串

    注意:undefiendnull 没有 toString 方法

  • String(mix):把任何类型转换成字符串类型

  • Boolean(mix):把任何类型转换成布尔类型

隐式类型转换

isNaN()

这个方法可以检测数据是不是非数类型,这中间隐含了一个隐式转换,先将传的参数调用 Number 方法,再看结果是不是 NaN,该方法可以检测 NaN 本身

isNaN(NaN); // true
isNaN('abc'); // true
isNaN(123); // false

算术运算符

  • ++ n:先将数据调一遍 Number 后,再自加 n
  • n ++:虽然是执行完后才自加 n,但执行前就调用 Number 进行类型转换
  • 同样一目运算符也可以进行类型转换:+-*/ 在执行前都会先转换成数字类型再进行运算

逻辑运算符

逻辑运算符也会隐式调用类型转换

  • &&|| 都是先把表达式调用 Boolean 换成布尔值再进行判断,不过返回的结果还是本身表达式的结果

  • ! 取反操作符返回的结果也是调用 Boolean 方法后的结果

Boolean:在条件判断时除了 undefinednullfalseNaN''0-0,其他所有值都转为 true,包括所有对象

转换规则

原始值转换为数值转换为字符串转换为布尔值
number/0 -> "0",5 -> "5"除了 0、-0、NaN 外均为 true
string"" -> 0,"1" -> 1,"a" -> NaN/除了空字符串均为 true
undefinedNaN"undefined"false
null0"null"false
[]0""true
[10,20]NaN"10,20"true
{}NaN"[object, Object]"true
{a: 1}NaN"[object, Object]"true
function(){}NaN"function(){}"true
true1"true"true
false0"false"false
SymbolNaN"function Symbol(){[native code]}"true
Symbol()抛错"Symbol()"true

引用类型转换为原始类型

[[ToPrimitive]]

引用类型在转换类型时会调用内置的 [[ToPrimitive]] 函数

ToPrimitive(obj, preferredType)JS 引擎内部转换为原始值,该函数接受两个参数:obj 为被转换的对象,preferredType 为希望转换成的类型(默认为 ,接受的值为 Number 或 String

注意:在执行时若第二个参数为空且 objDate 的实例时,此时 preferredType 会被设置为 String,其他情况下 preferredType 都会被设置为 Number

若没有提供这个值即预设情况,则会被设置转换的 hint 值为 default,这个首选的转换原始类型的指示(hint 值),是在作内部转换时由 JS 视情况自动加上的,一般情况就是预设值

当对象发生到基本类型值的转换时,会按照下面的逻辑调用对象上的方法:

  • 若存在 obj[Symbol.toPrimitive],则先调用 obj[Symbol.toPrimitive]

  • 否则按下面规则来:

    • preferredTypeNumberToPrimitive 执行过程如下:
      • 若 obj 为原始值,直接返回

      • 否则调用 obj.valueOf(),若执行结果是原始值则返回

      • 否则调用 obj.toString(),若执行结果是原始值则返回

      • 否则,抛出 TypeError 错误

    • preferredTypeString,将上面的第 2 步和第 3 步调换,即:
      • obj 为原始值,直接返回

      • 否则调用 obj.toString(),若执行结果是原始值则返回

      • 否则调用 obj.valueOf(),若执行结果是原始值则返回

      • 否则,抛出 TypeError 错误

    • PreferredType 没提供时即 hintdefault 时,此时与 PreferredType 为数字 Number 时的步骤相同

Symbol.toPrimitive

Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数

该函数被调用时会被传递一个字符串参数 hint,表示要转换到的原始值的预期类型。hint参数的取值是 "number""string" 和 "default" 中的任意一个

注意:Symbol.toPrimitive 在类型转换方面优先级是最高的

let ab = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
  [Symbol.toPrimitive]() {
    return 2;
  }
}
console.log(1 + ab); // 3
console.log('1' + ab); // 12

// 拥有 Symbol.toPrimitive 属性的对象
var obj2 = {
  [Symbol.toPrimitive](hint) {
    if(hint == "number"){
      return 10;
    }
    if(hint == "string"){
      return "hello";
    }
    return true;
  }
}
 
console.log(+obj2); // 10    --hint in "number"
console.log(`${obj2}`); // hello --hint is "string"
console.log(obj2 + ""); // "true"

let obj = {
  [Symbol.toPrimitive](hint) {
    if(hint === 'number'){
      console.log('Number场景');
      return 123;
    }
    if(hint === 'string'){
      console.log('String场景');
      return 'str';
    }
    if(hint === 'default'){
      console.log('Default 场景');
      return 'default';
    }
  }
}
console.log(2*obj); // Number场景 246
console.log(3 + obj); // Default 场景 3default
console.log(obj + "");  // Default场景 default
console.log(String(obj)); //String场景 str

valueOf 与 toString 方法

JSObject 原型的设计中,一定会有 valueOftoString 方法,所以这两个方法在所有对象里面都会有,不过它们在转换过程中有可能会交换被调用的顺序

对于原始类型数据,toStringvalueOf 方法的使用

const str = "hello", n = 123, bool = true;
console.log(typeof(str.toString()) + "_" + str.toString()) // string_hello
console.log(typeof(n.toString()) + "_" + n.toString())  // string_123
console.log(typeof(bool.toString()) + "_" + bool.toString()) //string_true

console.log(typeof(str.valueOf()) + "_" + str.valueOf()) //string_hello
console.log(typeof(n.valueOf()) + "_" + n.valueOf()) //number_123
console.log(typeof(bool.valueOf()) + "_" + bool.valueOf()) //boolean_true

// console.log(str.valueOf) => ƒ valueOf() { [native code] }
console.log(str.valueOf === str) // false
// console.log(n.valueOf) => ƒ valueOf() { [native code] }
console.log(n.valueOf === n) // false
// bool.valueOf() => true
console.log(bool.valueOf() === bool) // true

toString 方法对于原始类型数据而言,其效果相当于类型转换,将原始类型转为字符串;而 valueOf 方法对于原始类型数据而言,其效果将相当于返回原数据

复合对象类型数据使用 toStringvalueOf 方法:

var obj = {};
console.log(obj.toString()); // [object Object] 返回对象类型
console.log(obj.valueOf());  // {} 返回对象本身

综合案例

const test = { 
  i: 10, 
  toString: function() { 
    console.log('toString'); 
    return this.i; 
  }, 
  valueOf: function() { 
    console.log('valueOf'); 
    return this.i;
  } 
} 
alert(test); // 10 toString 
alert(+test); // 10 valueOf 
alert(''+test); // 10 valueOf 
alert(String(test)); // 10 toString 
alert(Number(test)); // 10 valueOf 
alert(test == '10'); // true valueOf 
alert(test === '10'); // false

toString() 和 String() 的区别

它们都可以转换为字符串类型,区别如下:

toString()

  • toString() 可将所有的数据都转换为字符串,但要排除 nullundefinednullundefined 调用 toString() 方法会报错

  • 若当前数据为数字类型,则 toString() 括号中可以写一个数字代表进制,可以将数字转化为对应进制的字符串

var num = 123;
console.log(num.toString() + '_' + typeof(num.toString()));   // 123_string   
console.log(num.toString(2) + '_' + typeof(num.toString()));    // 1111011_string
console.log(num.toString(8) + '_' + typeof(num.toString()));    // 173_string
console.log(num.toString(16) + '_' + typeof(num.toString()));   //7b_string

String()

  • String() 可以将 nullundefined 转换为字符串,但没法转进制字符串

注意下面两点:

  • Symbol.toPrimitivetoString 方法的返回值必须是基本类型值;valueOf 方法除了可以返回基本类型值,也可以返回其他类型值
  • 数字其实是预设的首选类型,即在一般情况下加号运算中的对象要作转型时都是先调用 valueOf 再调用 toString

== 运算规则

// 常规
"0" == null  // false
"0" == undefined // false
"0" == NaN // false
"0" == "" // false
"0" == 0 // true
false == null // false
false == undefined // false
false == NaN // false
false == {} // false
"" == null // false
"" == undefined // false
"" == NaN // false
"" == {} // false
0 == null // false
0 == undefined // false
0 == NaN // false
0 == {} // false

// 非常规
"0" == false // true
0 == false // true
"" == false // true
[] == false // true
"" == 0 // true
"" == [] // true
0 == [] // true

总结:

  • undefined == null,结果是 true 且它俩与所有其他值比较的结果都是 falseundefinednull 是一对特殊的值,它们不会被自动转换成布尔值进行比较)
  • NaN 不等于任何包括自身
  • String == Boolean,需两个操作数同时转为 Number
  • String/Boolean == Number,需要 String/Boolean 转为 Number
  • Object == Primitive,需 Object 转为 Primitive(具体通过 valueOf 和 toString 方法)

image.png

对 == 两边的值认真推敲,以下两个原则可以有效地避免出错,这时最好用 === 来避免不经意的强制类型转换

  • 若两边的值中有 true 或 false,千万不要使用 ==
  • 若两边的值中有 []"" 或 0,尽量不要使用 ==

==、=== 和 Object.is() 的区别

==:等于,===:严格等于,Object.is():加强版严格等于

const a = 3; 
const b = "3"; 
a == b;    // true
a === b;   // false,因为 a、b 的类型不一样 
Object.is(a, b);  // false,因为 a、b 的类型不一样 

=== 这个比较简单,只需利用下面的规则来判断两个值是否恒等

  • 若类型不同,就不相等
  • 若两个都是数值且是同一个值则相等,有一个是 NaN 就不相等
  • 若两个都是字符串且每个位置的字符都一样则相等,否则不相等
  • 若两个值都是同样的 Boolean 值则相等
  • 若两个值都引用同一个对象或函数则相等,即两个对象的物理地址也必须保持一致,否则不相等
  • 若两个值都是 null 或都是 undefined则相等

Object.is() 其行为与 === 基本一致,不过有两处不同:

  • +0 不等于 -0
  • NaN 等于自身
+0 === -0 // true
NaN === NaN // false
Object.is(0, +0) // true
Object.is(0, -0) // false
Object.is(+0, -0) // false
Object.is(-0, -0) // true
Object.is(NaN, 0/0) // true
Object.is(NaN, NaN) // true

Object.is('foo', 'foo'); // true
Object.is(window, window); // true
 
Object.is('foo', 'bar'); // false
Object.is([], []); // false
 
const foo = { a: 1 };
const bar = { a: 1 };
Object.is(foo, foo); // true
Object.is(foo, bar); // false
 
Object.is(null, null); // true

Object.is() 在严格等于的基础上修复了一些特殊情况下的失误,即 +0 和 -0NaN 和 NaN

function objectIs(x, y) {
  if(x === y) {
    // 运行到 1/x === 1/y 时 x 和 y 都为 0
    // 但 1/+0 = +Infinity,1/-0 = -Infinity 是不一样的
    return x !== 0 || y !== 0 || 1 / x === 1 / y;
  } else {
    // NaN === NaN 是 false,在这里做个拦截,x !== x 一定是 NaN, y 同理
    // 两个都是 NaN 时返回 true
    return x !== x && y !== y;
  }
}

扩展:JS 对于 Object 与 Array 的设计

JS 中所设计的 Object 纯对象类型的 valueOftoString 方法,它们的返回如下:

  • valueOf 方法返回值:对象本身

  • toString 方法返回值:"[object Object]" 字符串值,不同的内建对象的返回值是 "[object type]" 字符串

    • type 指的是对象本身的类型识别,如 Math 对象是返回 "[object Math]" 字符串
    • 但有些内置对象因为覆盖了这个方法,所以直接调用时不是这种值(注意:这个返回字符串的前面的 "object" 开头英文是小写,后面开头英文是大写)

因此可利用 Object 中的 toString 来进行各种不同对象进行判断,这在以前 JS 能用的函数库或方法不多的年代经常看到,不过它需要配合使用函数中的 call 方法才能输出正确的对象类型值,如:

Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(new Date); // "[object Date]"

对象的这两个方法均可被覆盖,可用下面的代码来观察这两个方法的运行顺序,下面这个都是先调用 valueOf 的情况:

let obj = {
  valueOf: function () {
    console.log('valueOf');
    return {}; // object
  },
  toString: function () {
    console.log('toString');
    return 'obj'; // string
  }
}
console.log(1 + obj);  //valueOf -> toString -> '1obj'
console.log(+obj); // // valueOf -> toString -> NaN
console.log('' + obj); // valueOf -> toString -> 'obj'

先调用 toString 的情况比较少见,大概只有 Date 对象或强制要转换为字符串时才会看到:

let obj = {
  valueOf: function () {
    console.log('valueOf');
    return 1; // number
  },
  toString: function () {
    console.log('toString');
    return {}; // object
  }
}
alert(obj); // toString -> valueOf -> alert("1");
String(obj); // toString -> valueOf -> "1";

而下面这个例子会造成错误,因为不论顺序如何都得不到原始数据类型的值,错误消息是TypeError: Cannot convert object to primitive value,从这个消息中可以得知它这里面会需要转换对象到原始数据类型:

let obj = {
  valueOf: function () {
    console.log('valueOf');
    return {}; // object
  },
  toString: function () {
    console.log('toString');
    return {}; // object
  }
}
console.log(obj + obj); // valueOf -> toString -> error!

数组 Array 很常用,虽然它是个对象类型,但它与 Object 的设计不同,它的 toString 有覆盖,说明一下数组的 valueOftoString 的两个方法的返回值:

  • valueOf 方法返回值:对象本身(与Object一样)
  • toString 方法返回值:相当于用数组值调用 join(',') 所返回的字符串,即[1,2,3].toString() 会是 "1,2,3",这点要特别注意

Function 对象很少会用到,它的 toString 也有被覆盖,所以并不是 Object 中的那个 toStringFunction 对象的 valueOftoString 的两个方法的返回值:

  • valueOf 方法返回值:对象本身(与Object一样)
  • toString 方法返回值:函数中包含的代码转为字符串值

扩展:{} + [] 的结果是什么?

详见:JS 的 {} + {} 与 {} + [] 的结果是什么?

扩展:['1','2','3'].map(parseInt)的返回值是什么?

详见:['1','2','3'].map(parseInt)的返回值是什么?