JS 数据类型

179 阅读36分钟

ECMAScript 的类型系统

JavaScript 存在以下两套类型系统:

  • 类型系统1:8 中基本数据类型
  • 类型系统2:值类型与引用类型

ECMAScript 规范又对类型系统做了另外的约定。它叙述了另外两套类型系统:

  • 类型系统3:ECMAScript 语言类型(ECMAScript language types)

    • ECMAScript 语言类型的存在,就是为了撰写ECMAScript 规范本身,以明确叙述该语言的规范。然而在ECMAScript 规范中所描述的语言(严格意义上并不是JavaScript 语言,可以说是ECMAScript 的一种方言)存在着8 种数据类型,包括Undefined、Null、Boolean、String、Symbol、Number、Object和BigInt。这些数据类型与JS 对数据类型的约定有以下不同:

      • ECMAScript 语言类型用首字符大写的单词作为类型名,而JavaScript 语言类型使用字符串作为类型名,且首字母小写,并且叙述中通常在不混淆的情况下也可以省掉单/双引号来直接作为类型名。
      • ECMAScript 规范中的Null 是一个类型,并且有一个唯一值null;而在JS 语言类型中没有Null 类型,null 值是对象类型的一个特殊实例(如果null 不是对象,那么原子和元类型就没有可以确立的定义)
      • 在ECMAScript 语言类型中没有函数类型,函数是对象类型的一个变体(Exotic Object),即对象类型的一种实现;而JS 语言类型中函数是第一类型(First class type),即能用typeof 关键字检查的、与string、object 等同级别的基本类型。
    • ECMAScript 约定它的类型可以归纳成两类:对象和基础类型。这两种类型是可以转换的,这些内部过程称为抽象操作(Abstract Operations),包括ToPrimitive() 和ToObject() 等。

  • 类型系统4:ECMAScript 规范类型(ECMAScript specification types)

    • 为了在规范的行文中实现ECMAScript 语言类型而存在,约定了10 种规范类型:

      • List、Record、Set、Relation,其中主要的是List 和Record 类型,是整个ECMAScript 规范实现中采用最多的数据类型
      • Completion Record、Reference、Property Descriptor,主要用来作为运算的结果或中间结果,其中属性描述符是实现JS 对象时使用的核心组件。
      • Data Blocks,主要用在内存、共享数据等的描述中
      • Lexical Environment、Environment Record,主要用在词法和运行期环境等的描述中
  • 类型系统5:对象类型
  • 类型系统6:原子对象类型系统

数据类型

ECMAScript 数据类型

ECMAScript 变量可以包含两种不同类型的数据:

  • primitive value(原始值):

    • Undefined 类型:undefined(missing value,指从未赋值)

    • Null 类型:null

      • 逻辑上讲,null 值表示一个空对象指针(empty value,指曾赋过值,但目前没有值)

      • undefined 值是由null 值派生而来,因此ECMA-262 将它们定义为表面上相等(console.log(null == undefined) // true

      • 需要使用符合条件来检测null 值的类型:

        var a = null
        (!a && typof a === 'object') // true
        
    • Boolean 类型

    • Number 类型

    • String 类型

    • Symbol 类型

    • BigInt 类型:54 位有符号整数类型

  • reference value(引用值)

    • Object 类型

      • ECMAScript 中的Object 也是派生其他对象的基类。Object 类型的所有属性和方法在派生的对象上同样存在。

      • 每个Object 实例都有如下属性和方法:

        • constructor:用于创建当前对象的函数。
        • hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(o.hasOwnProperty(‘name’))或符号。
        • isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。
        • propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用for-in 语句枚举。
        • toLocalString():返回对象的字符串表示。
        • toString():返回对象的字符串表示。
        • valueOf():返回对象对应的字符串、数值或布尔值表示

JavaScript 数据类型

undefined 类型

变量声明但未赋值。

undefined 不是一个保留字

它有可能在非全局作用域中被当作标识符(变量名)来使用,非常不建议这样做。

可以用void 0 来获得undefined。

Null 类型

null 主要用于赋值给一些可能会返回对象的变量,作为初始化。

Number 类型

【数值字面量】

  • 本质上是一个对与该字面量真实值最接近的内置number 对象的引用,有时候,字面量与该值完全吻合,但有时候会相差 9.979 201 547 673 599 058 281 863 565 184 2e291
  • 整数的字面量是一个十进制数字序列

使用IEEE 754 (二进制浮点数算术标准,一种为英特尔iPAX-432处理器设计的标准,zh.wikipedia.org/wiki/IEEE_7…)格式表示整数(32 位数值)和浮点值(双精度值,64 位数值)

  • JavaScript 的浮点数并不是IEEE 754标准的完全实现。Java 的浮点数实现用的是IEEE 754 的一个子集,而JavaScript 用的则是该子集的子集,所以JavaScript 的number 类型与Java 的double 类型非常相似,都是64 位的浮点数类型。一个number 类型包含1 位符号位(sign)、11 位指数位以及53 位有效位数。

    • 与其他一些浮点数系统一样,IEEE 754 也是基于二进制运作的。它的第一部分包含两个子部分:符号位和有效位数。符号位在整个64 位的最高位中,若该位为1 说明该数是负数;有效位数则在64 位的最低几位中,通常表示一个范围内的小数。有效位数的最高位理论上始终为1。因此该位实际上并不需要被存放于number 中,于是就多出了能用的1 位,称作彩蛋位(bonus bit)。第二部分:指数。指数存在于符号位与有效位数之间的那些位中。指数设计的精妙性使得两个浮点数可以在比较的时候装作自己是64 位整数,从而直接进行大小比较。指数还可以用于表示NaN、Infinity 以及非规格化浮点数(subnormal,如一些特别小的数和0)。
    • 0.1 和0.2 在转换成二进制后会无限循环,由于标准位数的限制(最高可达17位) ,后面多余的位数会被截掉,此时就已经出现了精度的损失,相加后因浮点数小数位的限制而截断的二进制数字在转换为十进制就会变成0.300 000 000 000 000 04。
  • 整数精度:-253 ~ 253,如果超出这个范围,整数将会失去尾数精度

  • JavaScript 使用有限数字表示法(finite numeric representation),所以和纯粹的数学运算不同,JS 的运算结果有可能溢出,此时结果为Infinity 或者-Infinity。

  • 如果数学运算的结果超出处理范围,则由IEEE 754 规范中的“就近取整”(round-to-nearest)模式来决定最后的结果。

    round down 向下取整:

    image-20221016162438664

    round up 向上取整:

    image-20221016162737861

  • 从数学运算角度和JS 语言的角度,Infinity/Infinity 是一个未定义操作,结果为NaN。

正零和负零在所有情况下都被认为是等同的(0 === -0

  • -0 除了可以用作常量以外,也可以是某些数学运算(乘、除)的返回值,加法和减法不会得到负零(negative zero)
  • 有些应用程序中的数据需要以级数形式来表示(比如动画帧的移动速度),数字的符号位(sign)用来代表其他信息(如移动的方向)。此时如果一个值为0 的变量失去了它的符号位,它的方向信息就会丢失。所以保留0 值的符号位可以防止这类情况发生。

可以表示的最小值(一个接近0 的数)保存在Number.MIN_VALUE中,这个值在多数浏览器中是5e-324(2 ** -1074),最大值保存在Number.MAX_VALUE 中,这个值在多数浏览器中是1.797 693 134 862 315 7e308(Number.MAX_SAFE_INTEGER * 2 ** 971),就是1 后面跟着308 位数字。这个值的失精程度很大。该值只有15.9 位有效位数,剩下的292 位都是二进制转十进制时产生的误差。

如果某个计算得到的值超出了JS 可表示的范围,那么值会被自动转换为一个特殊的Infinity 值。要确定一个值是不是有限大,可用isFinite() 函数。

NaN 不等于包括NaN 在内的任何值:console.log(NaN == NaN) // false,可以理解为无效数值、失败数值或者坏数值。NaN 是一个警戒值(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

  • 特殊数值NaN,在ECMAScript 中,0、+0或-0相除会返回NaN

     console.log(0/0) // NaN
     console.log(-0/0) // NaN
     console.log(0/-0) // NaN
     console.log(-0/-0) // NaN
    
  • ECMAScript 提供了isNaN() 函数,该函数接收一个参数,可以是任意数据类型,然后检查参数是否不是NaN,也不是数字。但是这样做的结果并不太准确:

     console.log(isNaN(NaN)) // true
     console.log(isNaN(10)) // false
     console.log(isNaN('10')) // false
     console.log(isNaN('blue')) // true,这个bug 自JavaScript 问世以来就一直存在
     console.log(isNaN(true)) // false
     ​
     // 从ES6 开始可以使用Number.isNaN(...)
     console.log(Number.isNaN('blue')) // false
    

ES6 新方法Object.is(..)

developer.mozilla.org/zh-CN/docs/…

 var a = 2 / 'foo'
 var b = -3 * 0
 ​
 Object.is(a, NaN) // true,判断两个值是否相等
 Object.is(b, -0) // true
 Object.is(b, 0) // false

【常见方法】

 Number.isFinite() // 检查一个数值是否为有限的
 Number.isNaN() // 检查一个值是否为NaN
 ​
 Number.parseInt()
 Number.parseFloat()
 ​
 ​
 Number.EPSILON // 2.220446049250313080847263336181640625e-16,它是JavaScript 中最小的正数。
 Number.MAX_SAFE_INTEGER // 9007199254740991,表示最大安全整数。在最大安全整数和最小安全整数之间的整数统称为安全整数。
 Number.MIN_SAFE_INTEGER // -9007199254740991
 // 在 JavaScript 中,只有在所有的运算因子、运算结果以及中间结果都是安全整数的情况下,才能进行精确的整数运算,才适用于加法结合律和乘法分配律。
 ​
 ​
 Number.isInteger() // 是否是整数
 Number.isSageInteger()  // 判断一个数是否是安全整数,-2^53 ~ 2^53
 ​
 Math.trunc() // 返回整数部分
String 类型

【字符串字面量】

简单类型字符串可以调用方法

 e.g.1
 var str = 'abc'
 console.log(str.toUpperCase()) // ABC
 ​
 【分析】
 // 字符串会先隐式转为String 对象,然后调用String.prototype 上的toUpperCase 方法
 ​
 e.g.2
 '1'.toString()
 // 等价于
 var s = new Object('1')
 s.toString()
 s = null
 ​
 【分析】
 // 第一步:创建Object 类实例。为什么不是String ?由于Symbol 和BigInt 的出现,对它们调用new 会报错,目前ES6 规范也不建议用new 来创建基本类型的包装类
 // 第二步:调用实例方法
 // 第三步:执行完方法立即销毁这个实例
 // 整个过程体现了基本包装类型的性质。

【ES6+】

  • ES6 加强了对Unicode 的支持,并且扩展了字符串对象。

     // 有6 种方式表示一个字符串
     '\z' === 'z' // true
     '\172' === 'z' // true
     '\x7A' === 'z' // true
     '\u007A' === 'z' // true
     '\u{7A}' === 'z' // true
    
  • codePointAt()

    JS 内部字符以UTF-16 的格式存储,每个字符固定为2 个字节。对于那些需要4 个字节存储的字符(Unicode 码点大于0xFFFF 的字符),JS 会认为它们是2 个字符。

     var s = '𠮷'
     s.charCodeAt(0) // 55362
     s.charCodeAt(1) // 57271
     ​
     var t = '𠮷a'
     s.codePointAt(0) // 134071
     s.codePointAt(1) // 57271
     s.codePointAt(2) // 97
     ​
     // codePointAt 方法返回的是码点的十进制值。它是测试一个字符是由2 个字符还是4 个字符组成的最简单方法。
     function is32Bit(c) {
         return c.codePointAt(0) > 0xFFFF
     }
     is32Bit('𠮷') // true
     is32Bit('a') // false
    
  • ES6 为字符串添加了遍历器接口,使得字符串可以由for...of 循环遍历。

  • ES6 新方法:

    • includes()
    • startsWith()
    • endsWith()
    • repeat()
    • padStart()
    • padEnd()

【模板字面量】

Web 为很多恶意脚本提供了滋生的温床,而模板让情况更糟糕了。虽然大部分模板工具提供了一些诸如防蛀入等恶意攻击的机制,但仍远远不够。最要命的是,想模板字符串字面量这种写法,天生是没有任何这类防护的。

Template string 是增强版的字符串,用反引号标识。

  • 可以当作普通字符串使用
  • 可以用来定义多行字符串
  • 在字符串中嵌入变量
  • 调用函数
 function fn() {return 'Hello World!'}
 `foo ${fn()} bar`

模板字面量也支持定义标签函数(tag function) ,而通过标签函数可以自定义插值行为。标签函数会接收被插值记号分隔后的模板和对每个表达式求值的结果。传入的参数则是该模板字符串及每个占位表达式的值。

 let a = 6
 let b = 9
 ​
 function simpleTag(strings, ...expressions) {
 console.log(strings) // ['', ' + ', ' = ', '']
 for (const expression of expressions) {
 console.log(expression) // 6、9、15
 }
 }
 let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`

标签函数的设计其实挺让人摸不着头脑:它将字符串以数组的形式传入,却将后面的值单独传入。如果被正确地编写和使用,标签函数的确可以在一定程度上有效地减轻XSS 攻击和其他安全风险。

使用模板字面量可以直接获取原始的模板字面量内容(如换行符或Unicode 字符),而不是被转换后的字符串。可以使用String.raw 标签函数。

 console.log(String.raw`\u00A9`) // \u00A9
Boolean 类型

【true、false】

要将一个其他类型的值转换为布尔值,调用Boolean() 转型函数

数据类型转换为true 的值转换为false 的值
Booleantruefalse
String非空字串空字符串
Number非零数值(包括无穷值)0、-0、NaN
Object任意对象null
UndefinedN/A(不存在)undefined

If 等流控制语句会自动执行其他类型值转到布尔值的转换。

Symbol 类型

【基本用法】

ES5 的对象属性名都是字符串,容易造成属性名的冲突。

ES6 引入一种新的原始数据类型Symbol,表示独一无二的值。Symbol 值通过Symbol 函数生成。对象的属性名现在可以有两种数据类型:

  • 字符串
  • Symbol 类型

Symbol 函数前不能使用new 命令,否则会报错。这个因为生成的Symbol 是一个原始类型,不是对象,不能添加属性。

Symbol 函数可以接受一个字符串作为参数,表示对Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串是比较容易区分。

Symbol 函数的参数只表示对当前Symbol 值的描述,因此相同参数的Symbol 函数的返回值是不相等的。

Symbol 值作为对象属性名时不能使用点运算符,并且该属性是公开属性,不是私有属性。

Symbol 作为属性名,该属性不会出现在for .. in 、for ... of循环中,也不会被Object.keys()、Object.getOwnPropertyNames() 返回。但可以通过Object.getOwnPropertySymbols 方法获取指定对象的所有Symbol 属性名。

运用:消除魔术字符串

 const shapeType = {
     triangle: Symbol()
 }
BigInt 类型

在JS 中,所有的数字都以双精度64 位浮点格式表示,这导致JS 中的Number 无法精确表示非常大的整数,它会将非常大的整数四舍五入,确切地说,JS 中的Number 类型只能安全地表示-9007199254740991(-(253 -1))和9007199254740991((253 -1)),任何超出此范围的整数值都可能失去精度。

 // 会有一定的安全性问题:
 9007199254740992 === 9007199254740993;    // → true 居然是true!
 ​
 // 创建,在数字末尾追加n
 console.log(9007199254740995n)
 ​
 // 用BigInt() 构造函数
 BigInt("9007199254740995")
 ​
 // 使用
 10n + 20n;    // → 30n  
 10n - 20n;    // → -10n 
 +10n;         // → TypeError: Cannot convert a BigInt value to a number 
 -10n;         // → -10n 
 10n * 20n;    // → 200n 
 20n / 10n;    // → 2n   
 23n % 10n;    // → 3n   
 10n ** 3n;    // → 1000n    
 ​
 const x = 10n;  
 ++x;          // → 11n  
 --x;          // → 9n
 console.log(typeof x);   // "bigint"

【注意】

  • BigInt不支持一元加号运算符, 这可能是某些程序可能依赖于 + 始终生成 Number 的不变量,或者抛出异常。另外,更改 + 的行为也会破坏 asm.js代码。

  • 因为隐式类型转换可能丢失信息,所以不允许在bigint 和 Number 之间进行混合操作。当混合使用大整数和浮点数时,结果值可能无法由BigInt 或Number 精确表示。

     10 + 10n;    // → TypeError
    
  • 不能将BigInt 传递给Web api和内置的 JS 函数,这些函数需要一个 Number 类型的数字。尝试这样做会报TypeError错误。

     Math.max(2n, 4n, 6n);    // → TypeError
    
  • 当 Boolean 类型与 BigInt 类型相遇时,BigInt 的处理方式与Number 类似,换句话说,只要不是0n,BigInt 就被视为truthy 的值。

     if(0n){} // 条件判断为false
     if(3n){} // 条件为true
    
  • 元素都为BigInt 的数组可以进行sort。

  • BigInt 可以正常地进行位运算,如 |、&、<<、>>和^

Object 类型

【对象声明与实例创建】

  • 使用构造器创建对象实例(语法:new Constructor[(arguments)]

    • 构造器既可以是一般函数,也可以是从ES6 开始支持的类
  • 声明对象字面量(语法:{ PropertyDefinitionList, ... }

  • 数组及其字面量

    • 使用new 运算来创建一个数组

    • 使用字面量声明(语法:[element0, element1[, ...[, elementN]]]

      • 数组元素的类型可以不同(异质)
      • 数组元素可以是不同维度的数组(交错)
  • 正则表达式及其字面量

    • 语法:/expression pattern/flags,可以不指定flags 而使用默认值,或指定为标志字符(u、m、g、i、y)的组合,expression pattern 由元字符(a~z、0~9等)构成:

       rx = /abcd\n\r/gi
       rx = new RegExp('abcd\n\r', 'gi')
      
  • 存取器(accessor)=> get/setter

【对象的键名转换】

  • 对象的键名只能是字符串和 Symbol 类型。
  • 其他类型的键名会被转换成字符串类型。
  • 对象转字符串默认会调用 toString 方法。
 // example 3
 var a = {}, b = {key:'123'}, c = {key:'456'}
 ​
 // b 不是字符串也不是 Symbol 类型,需要转换成字符串。
 // 对象类型会调用 toString 方法转换成字符串 [object Object]。
 a[b]='b'
 ​
 // c 不是字符串也不是 Symbol 类型,需要转换成字符串。
 // 对象类型会调用 toString 方法转换成字符串 [object Object]。这里会把 b 覆盖掉。
 a[c]='c'
 ​
 // 输出 c
 console.log(a[b])

【对象成员】

三种性质:可读写、可列举(枚举)、可重置:

对象成员是否能被列举,称为成员的可列举性。当某个对象成员不存在或它不可列举时,对该成员调用propertyIsEnumerable() 方法将返回false。

 var obj = new Object()
 ​
 // 不存在‘aCustomMember’
 console.log(obj.propertyIsEnumerable('aCustomMember'))
 ​
 // 数组的.length 属性是隐藏的
 console.log([].propertyIsEnumerable('length'))

一直以来,对propertyIsEnumerable() 的设计存在歧义。按照现有规范,该方法是不检测对象的原型链的,即对象继承来的成员不能被列举。但是更合理的设计是让该方法检测原型链。因为事实上它们是可以被for … in 语句列举的:

 // 定义原型链
 function MyObject() {}
 function MyObjectEx() {}
 MyObjectEx.prototype = new Object()
 ​
 // aCustomMember 是原型链上(父类的)的成员
 MyObject.prototype.aCustomMember = 'MyObject'
 ​
 // 显示false,因为propertyIsEnumerable() 不检测继承来的成员
 var obj = new MyObjectEx()
 console.log(obj.propertyIsEnumerable('aCustomMember'))
 ​
 // 但在列举obj 时,将包括aCustomMember
 for (var propName in obj) {
     console.log(propName)
 }

ES5 以前的for … in 语句只操作那些显式的成员,而无论它们是通过何种方式显式声明或继承的,甚至只是出于引擎约定。而从ES5 开始,JS 提供了更多操作对象成员的方法:

键名成员语法含义
一般键名仅显式成员for…in可列举的成员名(含原型链)
一般键名仅显式成员Object.keys() Object.values() Object.entries()可列举的、非符号的自有属性名
一般键名包含隐式成员Object.getOwnPropertyNames()全部的、非符号的自有属性名
符号键名包含隐式成员Object.getOwnPropertySymbols()全部的、符号键名的自有属性名
 Object.prototype.fun = () => {}
 const obj = { 2: 'a', 1: 'b' }
 for (const i in obj) {
     console.log(i, ':', obj[i])
 }
 // 1: b
 // 2: a
 // fun : () => {} Object 原型链上扩展的方法也被遍历出来
 ​
 for (const i in obj) {
     if (Object.prototype.hasOwnProperty.call(obj, i)) {
         console.log(i, ':', obj[i])
     }
 }
 // name: a 不属于自身的属性将被 hasOwnProperty 过滤
 // 实现Object.entries
 ​
 // Generate 函数的版本
 function* entries(obj) {
     for (let key of Object.keys(obj)) {
         yield [key, obj[key]]
     }
 }
 // 非Generate 函数的版本
 function entries(obj) {
     let arr = []
     for (let key of Object.keys(obj) {
         arr.push([key, obj[key]])
     }
     return arr
 }

【合并/混入对象】

 Object.assign(target, source1, source2)
 ​
 // 第一个参数是目标对象,后面的参数都是源对象
 // 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
 // 如果只有一个参数,会直接返回该参数
 ​
 // 如果非对象参数出现在源对象的位置(非首参数),处理规则将有所不同:这些参数都会转成对象,如果无法转成对象便会跳过,这意味着如果undefined 和null 不在首参数便不会报错
 let obj = {a: 1}
 Object.assign(obj, undefined) === obj // true
 Object.assign(obj, null) === obj // true
 // 其他类型的值(数值、字符串、布尔值)不在首参数也不会报错。但除了字符串会以数组形式复制到目标对象,其他值均不产生效果,因为只有字符串的包装对象会产生可枚举属性
 var v1 = 'abc'
 var obj = Object.assign({}, v1) // {'0': 'a', '1': 'b', '2': 'c'}
 ​
 Object(true)
 Object(10)
 Object('abc')
 ​
 {[[PrimitiveValue]]: true}
 {[[PrimitiveValue]]: 10}
 {0: 'a', 1: 'b', 2: 'c', length; 3, [[PrimitiveValue]]: 'abc'}

布尔值、数值、字符串分别转成对应的包转对象,它们的原始值都在包装对象的内部属性[[Primitive]] 上面,这个属性是不会被Object.assign 复制的,只有字符串的包装对象会产生可枚举的实义属性,那些属性则会被拷贝。

Object.assign 复制的属性是有限制的,值复制源对象的自身属性(不复制即成属性),也不复制不可枚举的属性,属性名为Symbol 值的属性也会被Object.assign 复制

Object.assign 方法实行的是浅复制,如果源对象某个属性的值是对象,那么目标对象复制得到的是这个对象的引用。

 var obj1 = {a: {b: 1}}
 var obj2 = Object.assign({}, obj1)
 obj1.a.b = 2
 obj2.a.b // 2

对于嵌套的对象,一旦遇到同名属性,Object.assign 的处理方法是替换而不是添加。

 var target = {a: {b: 'c', d: 'e'}}
 var source = {a: {b: 'hello'}}
 Object.assign(target, source) // {a: {b: 'hello'}}

assign 方法可以用于处理数组

 console.log(Object.assign([1, 2, 3], [4, 5])) // [4, 5, 3]

不过会把数组视为对象,比如这里会把目标数组视为是属性为0、1、2的对象,所以源数组的0、1属性的值覆盖了目标对象的值。

值类型和引用类型

变量不但有数据类型之别,而且还有值类型与引用类型之别,这种分类方式主要约定了变量的使用方法

  • 值类型(原始数据/原始值/标量基本类型值-scalar primitive):undefined、number、boolean、string、symbol、bigint
  • 引用类型(复合值-compound value):function、object(包括数组和封装对象)

在把一个值赋给变量时,JavaScript 引擎必须确定这个值是原始值还是引用值:

  • 保存原始值的变量是按value 访问的,因为操作的就是存储在变量中的实际值。
  • 引用值是保存在内存中的对象。JavaScript 不允许直接访问内存位置,因此也就不能直接操作对象所在的内存空间。在操作对象时,实际上操作的是该对象的引用(reference)而非实际的对象本身。为此,保存引用值的变量是按引用访问的。
值的操作
复制值

把值赋予给变量,或者通过变量把值赋值给另一个变量、属性或数组元素。

原始值在赋值语句中,会产生一个值的副本,副本与实际值之间没有任何联系。

引用值在赋值语句中,所赋的值是对原值的引用。

连等

let a = b = 10

(function(){ 
  	let a = b = 20 
})()

console.log(a) // 10
console.log(b) // 20

连等操作是从右向左执行的,相当于b = 10、let a = b,很明显b 没有声明就直接赋值了,所以会隐式创建为一个全局变量,函数内的也是一样,并没有声明b,直接就对b 赋值了,因为作用域链,会一层一层向上查找,找了到全局的b,所以全局的b 就被修改为20 了,而函数内的a 因为重新声明了,所以只是局部变量,不影响全局的a,所以a 还是10。

var a = {n: 1}
var b = a
a.x = a = {n: 2}
console.log(a.x)
console.log(b.x)

// undefined
// {n: 2}
传递值

把值作为参数传递给函数或方法。

原始值传递的仅是副本,而不是值本身。

当使用引用将数据传递给函数时,传递给函数的也是对原值的一个引用,函数可以使用这个引用来修改原值本身,任何修改在函数外部都是可见的。但是如果在函数内部使用一个新的值覆盖原来的引用,那么在函数内部的修改就不会影响原来引用的值。

image-20221016213107299

虽然传递的是指向数字对象的引用副本,但并不能通过它来更改其中的基本类型值。原因是标量基本类型值是不可更改的。

可以为数字对象添加属性(只要不更改其内部的基本类型值),通过它们间接地进行数据交换。

比较值

通过逻辑运算符,把值与另一个进行比较,看是否相等。

当对原始值进行比较时,比较的是值本身,而不是值所处的位置,比较结果可能会想等,这只能说明它们所包含的字节信息是相同的。

当比较两个引用值时,比较的是两个引用地址,看它们引用的原值是否为同一个副本,而不是比较它们的原值字节是否相等。

如果一方为字符串值,另一方为对象引用,那么调用Object.toString()将对象引用转为字符串之后,再对两个字符串内容进行比较。

1、相等

== 检查的是允许类型转换情况下的值的相等性

===检查不允许类型转换情况下的值的相等性(严格相等)

Object.is(v1, v2) - 一般情况下和三等号的判断相同,它处理了一些特殊的情况,如-0 和+0 不再相等,两个NaN 是相等的。

var c = [1, 2, 3]
var d = [1, 2, 3]
console.log(c == d) // false
console.log([] == ![]) // true

【分析】
// == 中,左右两边都需要转换为数字然后进行比较。
// [] 转换为数字为0。
// ![] 首先是转换为布尔值,由于[]作为一个引用类型转换为布尔值为true,因此![] 为false,进而在转换成数字,变为0
'0' == false // true
false == 0 // true
false == '' // true
false == [] // true
'' == 0 // true
'' == [] // true
0 == [] // true

-0 === +0 // true
NaN === NaN // false

Object.is(-0, +0) // false
Object.is(NaN, NaN) // true

2、不等

  • 比较的两个值都是字符串,那么按照字典中的字母表顺序进行
  • 如果其中一边或两边都不是字符串,那么这两个值的类型都转换为数字,然后进行普通的数字比较
  • 当其中一个值无法转换为有效数字时,如字符串类型转换为无效数字NaN,规范规定NaN 既不小于也不大于其他任何值,比较结果为false

判断数据类型

JavaScript 中的变量是没有类型的,只有值才有。变量可以随时持有任何类型的值。JavaScript 不做“类型强制”,即语言引擎不要求变量总是持有与其初始值同类型的值。一个变量可以现在被赋值为String 类型值,随后又被赋值为Number 类型值。

typeof
  • typeof   并不是在询问“变量的类型”,而是“变量中当前值的类型”。在JavaScript 中,只有值有类型,变量只是这些值的容器。
  • typeof 可以准确判断出除null 以外的基本数据类型,function 类型、symbol 类型;null 会被typeof 判断为object。typeof 操作符在用于检测函数时会返回“function”。function 是object 的一个“子类型”。函数是“可调用对象”,它有一个内部属性[[Call]],该属性使其可以被调用。
 typeof function a() {} === 'function' // true
 typeof(1) // 'number'
 typeof(typeof(1)) // 'string'
  • 对于undeclared(或者not defined)变量,typeof 照样返回“undefined”。这是因为typeof 有一个特殊的安全防范机制。
  • typeof 对RegExp 类型返回object(Chrome v105)。
  • NaN 用typeof 检测是number

2ality.com/2013/10/typ…

This is a bug and one that unfortunately can’t be fixed, because it would break existing code.

The “typeof null” bug is a remnant from the first version of JavaScript. In this version, values were stored in 32 bit units, which consisted of a small type tag (1–3 bits) and the actual data of the value. The type tags were stored in the lower bits of the units. There were five of them:

  • 000: object. The data is a reference to an object.
  • 1: int. The data is a 31 bit signed integer.
  • 010: double. The data is a reference to a double floating point number.
  • 100: string. The data is a reference to a string.
  • 110: boolean. The data is a boolean.

That is, the lowest bit was either one, then the type tag was only one bit long. Or it was zero, then the type tag was three bits in length, providing two additional bits, for four types.

Two values were special:

  • undefined (JSVAL_VOID) was the integer −230 (a number outside the integer range).
  • null (JSVAL_NULL) was the machine code NULL pointer. Or: an object type tag plus a reference that is zero.

It should now be obvious why typeof thought that null was an object: it examined its type tag and the type tag said “object”.

instanceof

result = variable instanceof constructor

如果变量是给定引用类型(由其原型链决定)的实例,则instanceof 操作符返回true。

a instance of B 判断的是,a 是否为B 的实例,即a 的原型链上是否存在B 的构造函数。

 function Person(name) {
     this.name = name
 }
 const p = new Person('tom')
 p instanceof Person // true
 ​
 // 这里的p 是Person 构造出来的实例对象。同时,顺着p 的原型链也能找到Object 的构造函数
 p.__proto__.__proto__ === Object.prototype
 5 instanceof Number // false
 // 因为5 是基本类型,它并不是Number 构造函数构造出来的实例对象。而如果稍加修改,使其变为判断以下关系,则返回true
 ​
 new Number(3) instanceof Number // true
 ​
 ​
 ({}) instanceof Object              // true
 ([]) instanceof Array               // true --> 注意
 (/aa/g) instanceof RegExp           // true --> 注意
 (function(){}) instanceof Function  // true
instanceof 的原理
 // L 表示左表达式,R 表示右表达式
 const instanceofMock = (L, R) => {
     if (typeof L !== 'object') {
         return false
     }
     while (true) {
         if (L === null) {
             // 已经遍历到了顶端
             return false
         }
         if (R.prototype === L.__proto__) {
             return true
         }
         L = L.__proto__
     }
 }
 instanceofMock('', String) // false
 ​
 function Person(name) {
     this.name = name
 }
 const p = new Person('tom')
 instanceofMock(p, Person) // true
手写instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

object instanceof constructor ⇒ 用来检测 constructor.prototype 是否存在于参数 object 的原型链上。

 // 定义构造函数
 function C () {}
 function D () {}
 let o = new C()
 console.log(o instanceof C, o.__proto__ === C.prototype) // true,true C.prototype 在 o 的原型链上
 console.log(o instanceof D, o.__proto__ === D.prototype) // false,false D.prototype 不在 o 的原型链上
 console.log(o instanceof Object, o.__proto__.__proto__ === Object.prototype) // true true
 C.prototype = {}
 let o2 = new C()
 console.log(o2 instanceof C) // true
 console.log(o instanceof C) // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上.
 D.prototype = new C() // 继承
 let o3 = new D()
 console.log(o3 instanceof D) // true
 console.log(o3 instanceof C) // true 因为 C.prototype 现在在 o3 的原型链上
 let simpleStr = "This is a simple string"
 let myString = new String()
 let newStr = new String("String created with constructor")
 let myDate = new Date()
 let myObj = {}
 let myNonObj = Object.create(null)
  
 simpleStr instanceof String; // 返回 false, simpleStr并不是对象
 myString  instanceof String; // 返回 true
 newStr    instanceof String; // 返回 true
 myString  instanceof Object; // 返回 true
  
 myObj instanceof Object;    // 返回 true, 尽管原型没有定义
 ({})  instanceof Object;    // 返回 true, 同上
 myNonObj instanceof Object; // 返回 false, 一种创建非 Object 实例的对象的方法
  
 myString instanceof Date; // 返回 false
  
 myDate instanceof Date;     // 返回 true
 myDate instanceof Object;   // 返回 true
 myDate instanceof String;   // 返回 false
 // 判断基本数据类型
 class PrimitiveNumber {
   static [Symbol.hasInstance](x) {
     return typeof x === 'number'
   }
 }
 console.log(111 instanceof PrimitiveNumber) // true
 ​
 // 自定义instanceof 行为的一种方式,这里将原有的instanceof 方法重定义,换成了typeof,因此能够判断基本数据类型。
 // 手动实现instanceof
 function myInstanceof (left, right) {
     // 基本数据类型直接返回false
     if (typeof left !== 'object' || left === null) return false
     // getProtypeOf 是Object 对象自带的一个方法,能够拿到参数的原型对象
     let proto = Object.getPrototypeOf(left) // left.__proto__
     while (true) {
         // 查找到尽头,还没找到
         if (!proto) return false
         // 找到相同的原型对象
         if (proto == right.prototype) return true
         proto = Object.getPrototypeOf(proto)
     }
 }
 ​
 console.log(myInstanceof("111", String)) // false
 console.log(myInstanceof(new String("111"), String)) // true
 // 手写instanceof
 ​
 // 判断一个实例是否是其父类或者祖先类型的实例
 let myInstanceof = (target, origin) => {
     while(target) {
         if (target.__proto__ === orgin.prototype) {
             return true
         }
         target = target.__proto__
     }
     return false
 }
 ​
 let a = [1, 2, 3]
 console.log(myInstanceof(a, Array)) // true
 console.log(myInstanceof(a, Object)) // true
Object.prototype.toString

每一个继承Object 的对象都有toString 方法,如果toString 方法没有重写的话,会返回 [Object type] ,其中type 为对象的类型。

toString 是Object 的原型方法,而Array、function 等类型作为Object 的实例,都重写了toString() 方法。不同的对象类型调用toString() 方法时,根据原型链的知识,调用的是对应的重写之后的toString 方法(function 类型返回内容为函数体的字符串,Array 类型返回元素组成的字符串),而不会去调用Object 上原型toString 方法,所以采用obj.toString() 不能得到其对象类型,只能将obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object 原型上的toString 方法。

// “万能方法”、“终极方法" => 返回[object type]
console.log(Object.prototype.toString.call(1)) // [Object Number]
console.log(Object.prototype.toString.call('tom')) // [Object String]
console.log(Object.prototype.toString.call(undefined)) // [Object Undefined]
console.log(Object.prototype.toString.call(true)) // [Object Boolean]
console.log(Object.prototype.toString.call({})) // [Object Object]
console.log(Object.prototype.toString.call([])) // [Object Array]
console.log(Object.prototype.toString.call(function() {})) // [Object Function]
console.log(Object.prototype.toString.call(null)) // [Object Null]
console.log(Object.prototype.toString.call(Symbol('tom'))) // [Object Symbol]
console.log(Object.prototype.toString.call(/^[0-9]$/)) // [object RegExp]
// 京东:下面代码中a 在什么情况下会打印1
var a = ?
if (a == 1 && a == 2 && a==3) {
    console.log(1)
}

// 法1
var a = {
    i: 1,
    toString() {
        return a.i++
    }
}
if (a == 1 && a == 2 && a==3) {
    console.log(1)
}

// 法2
let a = [1, 2, 3]
a.toString = a.shift
if (a == 1 && a == 2 && a==3) {
    console.log(1)
}
constructor

使用constructor 可以查看目标的构造函数,也可以进行数据类型判断,但其中也存在问题。

 var foo = 5
 foo.constructor // f Number() { [native code] }
 ​
 var foo = 'tom'
 foo.constructor // f String() { [native code] }
 ​
 var foo = true
 foo.constructor // f Boolean() { [native code] }
 ​
 var foo = []
 foo.constructor // f Array() { [native code] }
 ​
 var foo = {}
 foo.constructor // f Object() { [native code] }
 ​
 var foo = () => 1
 foo.constructor // f Function() { [native code] }
 ​
 var foo = /^[0-9]$/
 foo.constructor // ƒ RegExp() { [native code] }
 ​
 var foo = new Date()
 foo.constructor // f Date() { [native code] }
 ​
 var foo = Symbol('foo')
 foo.constructor // f Symbol() { [native code] }
 ​
 var foo = undefined
 foo.constructor // Uncaught TypeError: Cannot read properties of undefined (reading 'constructor')
 ​
 var foo = null
 foo.constructor // Uncaught TypeError: Cannot read properties of null (reading 'constructor')
Array.isArray - ES5

用来判断对象是否为数组。

当检测Array 实例时,Array.isArray 优于instanceof,因为Array.isArray 可以检测出iframes。

 const a = []
 Array.isArray(a) // true
 const b = {}
 Array.isArray(b) // false
 ​
 var iframe = document.createElement('iframe')
 document.body.appendChild(iframe)
 var xArray = window.frames[window.frames.length - 1].Array
 var arr = new xArray(1, 2, 3)
Array.prototype.isPrototypeOf
 const arr = [1, 2, 3]
 console.log(Array.prototype.isPrototypeOf(arr)) // true
xxx.constructor.toString()
 const arr3 = [2, 3, 55, 66]
 console.log(arr.constructor.toString()) // function Array() { [native code] }

Object.prototype.isPrototypeOf()

对象与值类型之间的转换

包装类

为了实现“一切皆对象”的目标,JS 在类型系统上做出了一些妥协:为部分基础类型系统中的“值类型”设定对应的包装类;然后通过包装类,将“值类型数据”作为对象来处理。

显式创建

将类构造器当成普通函数使用,该函数能将参数值进行包装,并以该类构造器的一个实例传出。

 var a = new AConstructor(value_a)

这里的AConstructor 包括以下三种值类型的包装:Number()、Boolean() 和String()。

 var flag = new Boolean(false)
 var result = true && flag // 显示的是对象Boolean{false}
 ​
 var result = flag && true //true
显式包装

JS 内建的Object() 类支持显式地将boolean、number、string 和symbol 四种值类型数据包装成对应的对象,这一语法在语义上解释为“基于值来创建等同的对象”。

 // 语法:显式将“值类型数据”包装为对象
 var a = new Object(value_a)
 ​
 console.log(new Object(3)) // [number: 3]
隐式包装

对于值类型的数据来说,如果它用作普通求值运算或赋值运算,那么是以“非对象”的形式存在的。

所谓值类型数据到对象的“隐式包装”,在已知的表达式运算中,其实总是由成员存取运算符触发的。

 const abc = 'abc'
 abc.length // 3
 abc.toUpperCase() // 'ABC'
 ​
 // 在访问'abc'.length 时,JS 将'abc' 在后台转换成String('abc'),然后再访问其length 属性

包装类是JS 用来应对“在值类型数据上调用对象方法”的处理技术。这与后来在.NET 中产生的“装箱(boxing)” 是一样的,只是JS 将这种技术称为“包装”。

 Number.prototype.showDataType = function() {
     console.log('value:' + this + ', type:' + (typeof this))
 }
 var n1 = 100
 console.log(typeof n1)
 n1.showDataType()
 ​
 // 当进行n1.xxx 或n1[xxx] 这样的对象成员存取操作时,JS 用“包装类(Number)”为n1 临时创建了一个对象。
 // 等效的代码Object(n1).showDataType()

在showDataType() 调用结束后(准确地说是在该对象的生存周期结束时),临时创建的包装对象也将被清理掉。

可见“值类型数据的方法调用”其实是被临时地隔离在另外一个对象中完成的。无论如何修改这个新对象的成员,这种修改也不会影响原来的值。

值类型转换

undefined 转换

undefined 能转换为特殊数字值NaN,因此它与数字值的运算结果将会是NaN,而不会导致异常。

数值转换
Number()
  • 布尔值,ture => 1,false => 0

  • 数值,直接返回

  • null => 0

  • undefined => NaN

  • 字符串:

    • 字符串包含数值字符,包括数值字符前面带加减号的情况,转换为一个十进制数值
    • 字符串包含有效的浮点值格式,转换为相应的浮点值
    • 字符串包含有效的十六进制格式,转换为与该十六进制值对应的十进制整数值
    • 空字符串,返回0
    • 字符串包含除上述情况之外的其他字符,返回NaN
  • 对象,调用valueOf() 方法,并按照上述规则转换返回的值。对象返回对象自身。如果转换结果是NaN,则调用toString() 方法,再按照转换字符串的规则转换

parseInt(aString, [radix])

更专注于字符串是否包含数值模式

  • 字符串最前面的空格会被忽略,从第一个非空格字符串开始转换

  • 如果第一个字符不是数值字符、加减号,立即返回NaN,这意味着空字符串返回NaN

  • 如果第一个字符是数值字符、加减号,则继续依次检测每个字符,直到末尾或碰到非数值字符,并返回结果

  • 接收第二个参数,2 ~ 36 的整数,用于指定进制数

  • radix为 undefined,或者radix为 0 或者没有指定的情况下,JavaScript 作如下处理:

    • 如果字符串 string 以"0x"或者"0X"开头,则基数是16 (16进制)
    • 如果字符串 string 以"0"开头,基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript 5 规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出radix 参数的值。
    • 如果字符串 string 以其它任何值开头,则基数是10 (十进制)。
    let num1 = parseInt('1234blue') // 1234
    let num2 = parseInt(' ') // NaN
    let num3 = parseInt('0xA') // 10,解释为十六进制整数
    let num4 = parseInt('22.5') // 22
    
parseIntFloat()
  • 从头开始检测每个字符,直到末尾或一个无效的浮点数值字符为止,第一次出现的小数点是有效的

  • 始终忽略字符串开头的零

  • 十六进制数值始终会返回0

  • 只解析十进制值,不能指定进制数

  • 如果字符串表示整数(没有小数点或者小数点后面只有一个零),返回整数

    let num1 = parseFloat('1234blue') // 1234
    let num2 = parseFloat('0908.5') // 908.5
    let num3 = parseFloat('0xA') // 0
    let num4 = parseFloat('22.5') // 22
    let num5 = parseFloat('30.0') // 30
    
字符串转换
toString()
  • 该方法可用于数值、布尔值、对象和字符串值。字符串值也有toString(),只是简单地返回自身的一个副本;null 和undefined 值没有toString() 方法
  • 在对数值调用这个方法时,toString() 可以接收一个底数参数。默认情况toString() 返回数值的十进制字符串表示。对象返回'[object Object]'
String()
  • 如果值有toString() 方法,则调用该方法(不传参数)并返回结果
  • 如果值是null,返回null
  • 如果值是undefined,返回undefined

+Number() 函数遵循相同的转换规则。

(!(~+[])+{})[--[~+''][+[]]*[~+[]]+~~!+[]]+({}+[])[~!+[]*~+[]] // 'sb'

1 + '1' // '11'
1 + true // 2
1 + false // 1
1 + undefined // NaN
'tom' + true // 'tomtrue'

1 + '2' + '2' // '122'
1 + +'2' + '2' // '32'
1 + -'1' + '2' // '02'
'A' - 'B' + '2' // 'NaN2'
'A' - 'B' + 2   //NaN

当使用+ 运算符计算string 类型和其他数据类型相加时,其他数据类型都会转换为string 类型;在其他情况下,都会转换为number 类型,但是undefined 类型会转换为NaN,相加结果也为NaN。

  • 如果+运算符两边都是number类型,规则如下:

    • 如果+运算符两边存在NaN,则结果为NaN(对NaN 进行typeof 求值,返回number)
    • 如果是Infinity + Infinity,则结果是Infinity。
    • 如果是-Infinity + (-Infinity),则结果是-Infinity。
    • 如果是Infinity + (-Infinity),则结果是-NaN。
  • 如果+运算两边有至少一个是字符串,规则如下:

    • 如果+运算符两边都是字符串,则执行字符串拼接操作
    • 如果+运算符两边只有一个是字符串,则将另外的值转换为字符串,再执行字符串拼接操作
    • 如果+运算符两边有一个是对象,则调用valueOf 或者toString 方法取得值,将其转换为基本数据类型再进行字符串拼接

当使用+ 运算符计算时,如果存在复杂数据类型,那么它将会被转换为基本数据类型再进行运算,这涉及“对象类型转基本类型”这个过程,具体规则是在转换时会调用该对象上的valueOf 或toString 方法,这两个方法的返回值是转换后的结果。

具体调用valueOf 还是toString 呢?这是ES 规范所决定的,实际上,这取决于内置的toPrimitive 的调用结果。

toPrimitive(input, PreferredType?)

PreferredType 没有设置时,Date类型的对象,PreferredType默认设置为String,其他类型对象PreferredType 默认设置为Number。

从主观上说,这个对象倾向于转换成什么,就会优先调用哪个方法。如果倾向于转换为number 类型,就优先调用valueOf;如果倾向于转换为string 类型,就只调用toString。

这个对象倾向于转换成什么,就会优先调用哪个方法。取自规范中的PreferredType 概念,浏览器对PreferredType 的理解比较一致,“对象类型转换为基本类型时”,先调用valueOf,再调用toString 也没有问题。

// 百度:实现 (5).add(3).minus(2) 功能
// 例如:5 + 3 — 2,结果为6


Number.prototype.add = function(n) {
    return this.valueOf() + n
}
Number.prototype.minus = function(m) {
    return this.valueOf() - m
}

(5).add(3).minus(2) // 6

valueOf 及toString 可以被开发者重写:

const foo = {
    toString () {
        return 'tom'
    },
    valueOf () {
        return 1
    }
}

这里对foo 对象的valueOf 以及toString 进行了重写,这时候调用alert(foo) 将输出tom。这里就涉及隐式转换,在调用alert 打印输出时,倾向于使用foo 对象的toString 方法,将foo 转换为基本数据类型,以打印出结果。

然而执行console.log(1 + foo) 将输出2,这时候的隐式转换则倾向于使用foo 对象的valueOf 方法,将foo 转换为基本数据类型,以执行相加操作。

【参考资料】

《JavaScript 高级程序设计》第4 版 章节3

《JavaScript: The Good Parts》appendix A Awful Parts

《JavaScript 语言精髓与编程实践》第3版 章节4、6

《你不知道的JavaScript》中卷 第一部分:类型和语法 章节1、2

《你不知道的JavaScript》下卷 第一部分:起步上路 章节2

《JavaScript 悟道》章节2、3、4、5、6、9