变量类型

Summer2019年5月28日
大约 11 分钟

数据类型

最新的 ECMAScript 标准定义了 7 种数据类型

对象的底层数据结构

🛵JS 原型链open in new window

Symbol类型在实际开发中的应用,手动实现一个简单的Symbol

🚀ES6 Symbolopen in new window

ES6 系列之模拟实现 Symbol 类型open in new window

变量在内存中的具体存储形式

提示

栈内存:原始类型 以键值对的方式储存在栈内存中

堆内存:复杂类型 会在堆内存中开辟一块空间,储存这个对象的值,并同时在栈内存中储存变量和指向对象的指针

JS栈内存与堆内存

栈内存堆内存
存储基础数据类型存储引用数据类型
按值访问按引用访问
存储的值大小固定存储的值大小不定,可动态调整
由系统自动分配内存空间由程序员通过代码进行分配
主要用来执行程序主要用来存放对象
空间小,运行效率高空间大,但是运行效率相对较低
先进后出,后进先出无序存储,可根据引用直接获取

基本类型对应的内置对象,装箱拆箱操作

基本类型对应的内置对象

  • Boolean
  • Number
  • String
true === new Boolean(true) // false
1874 === new Number(1874) // false 
'summer' === new String('summer') // false

true === new Boolean(true).valueOf() // true

注意

引用类型和包装类型的主要区别就是对象的生存期,使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中而自基本类型则只存在于一行代码的执行瞬间,然后立即被销毁,这意味着我们不能在运行时为基本类型添加属性和方法。

const name = 'summer'
name.age = 24
=> name.age // undefined

装箱拆箱操作

  • 装箱转换:把基本类型转换为对应的包装类型
  • 拆箱操作:把引用类型转换为基本类型

既然原始类型不能扩展属性和方法,那么我们是如何使用原始类型调用方法的呢?

每当我们操作一个基础类型时,后台就会自动创建一个包装类型的对象,从而让我们能够调用一些方法和属性,例如下面的代码:

var name = "ConardLi";
var name2 = name.substring(2);

复制代码实际上发生了以下几个过程:

  • 创建一个String的包装类型实例
  • 在实例上调用substring方法
  • 销毁实例

也就是说,我们使用基本类型调用方法,就会自动进行装箱和拆箱操作,相同的,我们使用NumberBoolean类型时,也会发生这个过程。

从引用类型到基本类型的转换,也就是拆箱的过程中,会遵循ECMAScript规范规定的toPrimitive原则,一般会调用引用类型的valueOftoString方法,你也可以直接重写toPeimitive方法。一般转换成不同类型的值遵循的原则不同,例如:

  • 引用类型转换为Number类型,先调用valueOf,再调用toString
  • 引用类型转换为String类型,先调用toString,再调用valueOf

valueOftoString都不存在,或者没有返回基本类型,则抛出TypeError异常。

const obj = {
  valueOf: () => { console.log('valueOf'); return 123 },
  toString: () => { console.log('toString'); return 'ConardLi' }
};
console.log(obj - 1)   // valueOf   122
console.log(`${obj}ConardLi`) // toString  ConardLiConardLi

const obj2 = {
  [Symbol.toPrimitive]: () => { console.log('toPrimitive'); return 123 },
};
console.log(obj2 - 1)  // valueOf   122

const obj3 = {
  valueOf: () => { console.log('valueOf'); return {} },
  toString: () => { console.log('toString'); return {} },
};
console.log(obj3 - 1)
// valueOf  
// toString
// TypeError

Symbol​.toPrimitiveopen in new window

除了程序中的自动拆箱和自动装箱,我们还可以手动进行拆箱和装箱操作。我们可以直接调用包装类型的valueOf或toString,实现拆箱操作:

const num =new Number('123')
console.log( typeof num.valueOf() ) // number
console.log( typeof num.toString() ) // string

理解值类型和引用类型

  • 值类型等于原始类型: 以键值对的方式储存在栈内存中
  • 复杂类型等于引用类型:会在堆内存中开辟一块空间,储存这个对象的值,并同时在栈内存中储存变量和指向对象的指针

null 和 undefined 的区别

null

null表示"没有对象",即该处不应该有值。典型用法是:

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点。
Object.getPrototypeOf(Object.prototype)
// null

undefined

undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。典型用法是:

  • 变量被声明了,但没有赋值时,就等于undefined
  • 调用函数时,应该提供的参数没有提供,该参数等于undefined
  • 对象没有赋值的属性,该属性的值为undefined
  • 函数没有返回值时,默认返回undefined
var i;
i // undefined

function f(x){console.log(x)}
f() // undefined

var  o = new Object();
o.p // undefined

var x = f();
x // undefined

判断 JavaScript 数据类型

判断JS数据类型的四种方法

  • typeOf
  • instanceof
  • constructor
  • toString
['Boolean', 'Number', 'String', 'Null', 'Undefined', 'Symbol', 'Object', 'Function', 'Array', 'Error', 'RegExp', 'Math', 'JSON', 'global']

typeOf

typeof操作符返回一个字符串,表示未经计算的操作数的类型。

调用结果
typeof trueboolean
typeof 1874number
typeof 'summer'string
typeof nullobject
typeof undefinedundefined
typeof Symbol()symbol
typeof {}object
typeof function() {}function
typeof []object
typeof new Error()object
typeof new RegExp()object
typeof Mathobject
typeof JSONobject
typeof windowobject

instanceof

🛵JS 原型链 > instanceof的原理

instanceof运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置。

  1. 所有引用类型都具有对象特性,即可以自由扩展属性
  2. 所有引用类型都具有一个__proto__(隐式原型)属性,是一个普通对象
  3. 所有的函数都具有prototype(显式原型)属性,也是一个普通对象
  4. 所有引用类型__proto__值指向它构造函数的prototype
  5. 当试图得到一个对象的属性时,如果变量本身没有这个属性,则会去他的__proto__中去找

[] instanceof Array实际上是判断Foo.prototype是否在[]的原型链上。

[] instanceof Array // true
new Date() instanceof Date // true
new RegExp() instanceof RegExp // true

注意

使用instanceof来检测数据类型,不会很准确,这不是它设计的初衷:

[] instanceof Object // true
function(){}  instanceof Object // true

使用instanceof也不能检测基本数据类型,所以instanceof并不是一个很好的选择。

constructor

🛵JS 原型链 > constructor

toString

每一个引用类型都有toString方法,默认情况下,toString()方法被每个Object对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中type是对象的类型。

提示

注意,上面提到了如果此方法在自定义对象中未被覆盖,toString才会达到预想的效果,事实上,大部分引用类型比如Array、Date、RegExp等都重写了toString方法。

我们可以直接调用Object原型上未被覆盖的toString()方法,使用call来改变this指向来达到我们想要的效果。

调用结果
Object.prototype.toString.call(true)[object Boolean]
Object.prototype.toString.call(1874)[object Number]
Object.prototype.toString.call('summer')[object String]
Object.prototype.toString.call(null)[object Null]
Object.prototype.toString.call(undefined)[object Undefined]
Object.prototype.toString.call(Symbol())[object Symbol]
Object.prototype.toString.call({})[object Object]
Object.prototype.toString.call(function() {})[object Function]
Object.prototype.toString.call([])[object Array]
Object.prototype.toString.call(new Error())[object Error]
Object.prototype.toString.call(new RegExp())[object RegExp]
Object.prototype.toString.call(Math)[object Math]
Object.prototype.toString.call(JSON)[object JSON]
Object.prototype.toString.call(window)[object Window]

利用闭包 and toString封装

不推荐将这个函数用来检测可能会产生包装类型的基本数据类型上,因为 call 会将第一个参数进行装箱操作

// es6
const typeValidate = type => target => `[object ${type}]` === Object.prototype.toString.call(target)
// es5
function typeValidate(type) {
  return function (target) {
    return "[object " + type + "]" === Object.prototype.toString.call(target)
  }
}

const isArray = typeValidate('Array')
const isString = typeValidate('String')

isString(new String('summer')) // true
isArray([]) // true

当你想判断一个基本类型的数据时,你可以用typeof去判断,它很简单,而且可靠;当你想判断一个对象属于哪个子类型时,你可以使用instanceof运算符或constructor属性,但是你需要有个预期的类型,不然就要针对每一种类型写不一样的if...else...语句,还有一点需要注意的就是constructor属性可以被修改,所以并不可靠;如果你不嫌代码量多,要求准确且全面,那你可以用Object.prototype.toString.call()进行判断。

可能发生隐式类型转换的场景以及转换原则,应如何避免或巧妙应用

非严格相等 ==

相等操作符比较两个值是否相等,在比较前将两个被比较的值转换为相同类型。在转换后(等式的一边或两边都可能被转换),最终的比较方式等同于全等操作符 ===的比较方式。 相等操作符满足交换律。

==

判等

xy=====Object.is
undefinedundefinedtruetruetrue
nullnulltruetruetrue
truetruetruetruetrue
falsefalsetruetruetrue
"foo""foo"truetruetrue
00truetruetrue
+0-0truetruefalse
0falsetruefalsefalse
""falsetruefalsefalse
""0truefalsefalse
"0"0truefalsefalse
"17"17truefalsefalse
[1,2]"1,2"truefalsefalse
new String("foo")"foo"truefalsefalse
nullundefinedtruefalsefalse
nullfalsefalsefalsefalse
undefinedfalsefalsefalsefalse
{ foo: "bar" } { foo: "bar" }falsefalsefalse
new String("foo")new String("foo")falsefalsefalse
0nullfalsefalsefalse
0NaNfalsefalsefalse
"foo"NaNfalsefalsefalse
NaNNaNfalsefalsetrue

Truthy (真值)

JavaScript 中,Truthy (真值)指的是在 布尔值 上下文中转换后的值为真的值。所有值都是真值,除非它们被定义为 falsy (即除了 false0""nullundefined NaN 外)。 JavaScript 中的真值示例如下(将被转换为 true,if 后的代码段将被执行):

if (true)
if ({})
if ([])
if (42)
if ("foo")
if (new Date())
if (-42)
if (3.14)
if (-3.14)
if (Infinity)
if (-Infinity)

出现小数精度丢失的原因,JavaScript 可以存储的最大数字,最大安全数字,JavaScript 处理大数字的方法,避免精度丢失的方法

出现小数精度丢失的原因open in new window

最大数字,最大安全数字

  • Number.MAX_VALUE
  • Number.MAX_SAFE_INTEGER

处理大数字的方法,避免精度丢失的方法

  • node-bignum
  • node-bigint
  • number-precision
  • bigInt类型在es10中被提出,现在Chrome中已经可以使用

参考

Loading...