我爱编程

JS基础系列(二): JS里的类型

2018-03-26  本文已影响0人  squall1744

内置类型


JS中一共有七个内置类型:

typeof操作的bug

typeof undefined === 'undefined' //true
typeof true === 'true' //true
typeof 42 === 'number' //true
typeof "42" === 'string' //true
typeof { life: 42 } === 'object' //true

// ES6中新加入的类型
typeof Symbol() === 'symbol' // true

上面六种类型用typeof操作均有同名的字符串与之对应。但是对于null来说, 结果可能跟我们想象的不同

typeof null === 'object' //true

我们预期的返回结果应该是'null', 但是返回的却是object, 这个bug在js中存在大概有已经有20年了, 也许永远都不会修复了.....

因此对于null的检测, 我们需要用到复合检测

let a = null

if(!a && typeof a === 'object') {
  ...
}

js的变量没有类型, 只有值才有, 变量可以随时持有任何类型的值, 所以对变量进行typeof操作, 实际上操作的是变量当前持有的值的类型。

其他特殊情况

对于函数来说typeof操作返回的是'function', 其length为参数的个数, 数组的typeof返回的则是'object', 其length为数组的长度。

undefined和undeclared

在js的世界中, 很多人倾向于认为undefinedundeclared是同一个东西, 但是实际上他们是两个不同的东西, 有如下代码

let a
a //undefined
b //ReferenceError: b is not defined

上例中, b is not defined很容易让人觉得b是一个undefined, 实际上b是一个undeclared, 但是好在js中有一个机制可以让我们避免undeclared造成的程序报错, 有如下代码

if(DEBUG) {
  console.log('Debugger is starting')
}

对于上面的代码, 如果全局环境中没有DEBUG变量, 则程序直接报错, 为了解决上述问题, 我们可以用typeof来解决, 对于undeclared, typeof返回的也是undefined

if(typeof DEBUG !== 'undefined') {
  console.log('Debugger is starting')
}

经过上面的改造, 无论DEBUG是否声明, 程序都不会报错。

原生函数


js中常用的原生函数有:

let a = new Number(50)
console.log(a) //50

但是, 用原生函数构造出来的对象可能个我们设想的有所不同

console.log(typeof a) //'Object'
console.log(a instanceof Number) //true

通过构造函数(如 new String("abc") )创建出来的是封装了基本类型值(如 "abc" )的封装对象。

js的装箱和拆箱

由于基本类型没有.length.toString()等方法, 所以当我们对基本类型使用这些方法时, js会自动为基本类型包装一个对应的封装对象, 比如数字会自动变为Number, 字符串会自动变为String,这种现象就叫做装箱

由于js会自动为基本类型进行装箱, 所以一般我们不建议手动直接使用封装对象。

封装对象释疑

使用封装对象时有些地方需要特别注意。比如Boolean

let a = new Boolean(false)

if(!a) {
  console.log('aaa') //执行不到这里
}

我们为false创建了一个封装对象, 我们的本意是想让这个封装对象的值是false, 然而一个对象的值永远都是真值。所以我们得到了截然相反的结果。

如果想得到封装对象中的基本类型, 则我们需要拆箱。在js中我们可以使用valueOf()函数来进行拆箱:

var a = new String( "abc" );
var b = new Number( 42 ); 
var c = new Boolean( true );

a.valueOf(); // "abc"
b.valueOf(); // 42
c.valueOf(); // true

在需要用到封装对象中的基本类型值的地方会发生隐式拆箱。 具体过程(即强制类型转 换)将在后面详细介绍。

类型转换


将值从一种类型转换为另一种类型通常称为类型转换 (type casting) ,这是显式的情况;隐式的情况称为 强制类型转换 (coercion)

let a = 42
let b = a + '' //强制类型转换成字符串
let c = String(a) //显式类型转换为字符串

转换成字符串

我们可以使用全局方法String()将其他字符转换成字符串

String(1) //'1'
String(true) //'true'
String({}) // [object Object]
String(undefined) //'undefined'
String(null) //'null'

或者用toString()方法, 用toString()方法需要注意以下几点

(1).toString() //'1'
undefined.toString() //报错
null.toString() //报错

转换成数字

我们可以使用全局方法Number()将其他字符转换成字符串, 这里面需要注意几点

Number('') //0
Number(undefined) //NaN
Number(null) //0
Number(NaN) //NaN
Number({}) //NaN
Number(true) //1
Number(false) //0
Number('123') //123
Number('123asc') //NaN

我们还可以用parseInt()来转换成整数, parseFloat()转换成小数

这里个需要注意的地方, 当遇到'123df'这种字符串的时候, parseInt()只会解析到最后一个数字处, 所以结果是123, parseFloat()同理, '1.26dcd'会解析成1.26

parseInt('1234dcsd') //1234
parseFloat('1.26ddd') //1.26

转换成boolean

我们可以用Boolean()将其他类型转换成boolean类型, 这里也有一点需要注意

我们也可以用!!后跟一个值来转换成boolean如, !!1就是true

关于+和-的骚操作

我稍后加啊

内存图


基本类型在内存中存储的示意图

基本类型的值按值传递


基本类型

引用类型的内存示意图

引用类型的值按引用传递


引用类型

关于内存的几个题目

1.最简单的,有以下代码

let a = 1
let b = a
b=2

请问a的值是多少?

答: a的值是1, 因为基本类型是按值传递

2.来一个稍微复杂点的

let a = {name: 'a'}
let b = a
b = {name: 'b'}

请问a的值是多少?

答: a的值是{name: 'a'}

解析:

let a = {name: 'a'} //a指向堆内存中的{name: 'a'}, 此时a存的是{name: 'a'}的地址
let b = a  //将{name: 'a'}的地址赋值给b, 此时a和b指向同一个对象{name: 'a'}
b = {name: 'b'} //将一个新的对象{name: 'b'}的地址赋值给b, 此时a和b指向了不同的对象

此过程的内存图如下


内存图

3.在继续来一个

let a = {name: 'a'}
let b =a
b.name = 'b'

请问 a.name是什么?

答: a.name是'b'

解析

let a = {name: 'a'} //a指向堆内存中的{name: 'a'}, 此时a存的是{name: 'a'}的地址
let b = a  //将{name: 'a'}的地址赋值给b, 此时a和b指向同一个对象{name: 'a'}
b.name = 'b' //修改对象{name: 'a'}的name为'b', 由于a也指向这个对象, 所以a.name也是'b'

内存图如下


内存图

4.最后再来一个

let a = {name: 'a'}
let b = a
b = null

请问a.name是什么?

答: a.name是'a'

解析

let a = {name: 'a'} //a指向堆内存中的{name: 'a'}, 此时a存的是{name: 'a'}的地址
let b = a  //将{name: 'a'}的地址赋值给b, 此时a和b指向同一个对象{name: 'a'}
b = null //将null赋值给b, 此时b的值是null, 不是对象的地址, 与对象的链接已断开

内存图如下


内存图

解决引用类型赋值相关问题的解题方法就一个: 画内存图

循环引用问题

假设我们有如下代码

let a = {
  name: 'Adam',
  age: 25
}
a.self = a

当我们调用a.self的时候, 我们神奇的发现, a.self竟然指向的是他自己, 然后他自己里面依然有self, 我们再调用的时候, 发现我擦, 还能调用自己, 于是我就就来了一个骚操作:
a.self.self.self.self.self.self.self
我们发现, 不管我们调用多少次self, 都会指向自己, 这就是循环引用

继续上内存图


内存图

通过内存图我们发现, 当我们给a.self赋值a之后,在对象中, 会有一个self属性, 它的值就是a对象的地址, 所以每次我们调用a.self的时候, 它都会通过地址引用自己, 所以我们才可以无限次调用

再来一个题就结束吧

let a = {n:1}
let b = a
a.x = a = {n:2}
alert(a.x)
alert(b.x)

请问alert(a.x)是多少, alert(b.x)是多少?

答: a.x是undefined, b.x是[object Object]

解析

let a = {n:1}
let b = a

前两行很好理解, 就是把对象的地址赋值给b, 重点是后面一句

a.x = a = {n:2}

这里有一个小陷阱, 对于对象的连续=运算,js会先固定对象的地址, 当整个运算完成后, 对象地址才会改变

什么意思呢, 我们把上面代码变形下

1.我们把原来地址的a叫做a1, 赋值{n:2}后的叫a2, 在没开始计算前, js是这样解析的
a1.x = a1 = {n:2}

2.先做a1= {n:2}运算, 这个运算相当于指向一个新对象, 我们为了区分, 所以叫a2, a2指向{n:2}

3.那么经过这一步运算代码等价于
a1.x = a2

4.所以结果是a1.x指向{n: 2}, a2.x并没有指向任何一个对象
5.当赋值语句执行完后, 此时的a才会变为a2
6.所以alert(a.x)其实等价于alert(a2.x), alert(b.x)等价于alert(a1.x)

还是来个内存图


内存图

GC垃圾回收

如果一个对象没有被引用, 它就是垃圾, 将被回收

看下面的代码

let a = {name: 'a'}
let b = {name: 'b'}
a = b

上内存图

刚开始的时候a和b各指向一个对象


0

后来,我们改变了a的值, a指向了b指向的对象, 所以之前a指向的那个对象, 就变成了垃圾


1

这个垃圾在浏览器觉得没有用的时候, 就会被回收

再来个例子

let fn = function() {}
document.body.onclick = fn
fn = null

请问fn是不是垃圾

答: 不是

来再上内存图

刚开始的时候

开始时

赋值后


赋值后

内存泄露

这个代码按理说到这就解析完了, 但是在IE里有个bug

还是刚才那个代码

let fn = function() {}
document.body.onclick = fn
fn = null

我们刚才分析过了, fn不是垃圾,虽然不是垃圾, 但是如果我们把当前网页关了, 那fn按理说应该是被销毁的, 但是在IE里,如果你仅仅是关闭页面, 是不会被销毁的, 只有关整个浏览器才会销毁

深拷贝与浅拷贝

浅拷贝

let a = {name: 'a'}
let b = a
b.name = 'b'
a.name //b

我们将a的值传给b, 但是我们改变b指向的对象的值会引起a的属性值的改变, 这种拷贝我们就叫做浅拷贝

浅拷贝内存图

浅拷贝

深拷贝
所有基本类型的复制都是深拷贝, 所以我们不讨论基本类型的深拷贝
深拷贝内存图


深拷贝

深拷贝就到下回更新.....

上一篇下一篇

猜你喜欢

热点阅读