内置类型
JS中一共有七个内置类型:
- number
- string
- boolean
- undefined
- null
- object
- symbol
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的世界中, 很多人倾向于认为undefined
和undeclared
是同一个东西, 但是实际上他们是两个不同的东西, 有如下代码
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中常用的原生函数有:
- String()
- Number()
- Boolean()
- Array()
- Object()
- Function()
- RegExp()
- Date()
- Error()
- Symbol()
我们可以这样用原生函数:
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()方法需要注意以下几点
- 由于数字没有
.
运算, 所以给数字用toString()方法需要用()
包裹 - undefined和null使用toString()方法会报错
(1).toString() //'1'
undefined.toString() //报错
null.toString() //报错
转换成数字
我们可以使用全局方法Number()将其他字符转换成字符串, 这里面需要注意几点
- 通常情况下解析字符串时, 先把参数按照数字解析, 当遇到无法解析成数字时, 整个参数会被解析成NaN
- 空字符串, false, null会转换为0
- true会被解析为1
- undefined, 对象, NaN都会被解析成NaN
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类型, 这里也有一点需要注意
- 除
undefined
,null
,0
,NaN
,''
这五个falsy值是false外, 其余全部解析成true
我们也可以用!!后跟一个值来转换成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各指向一个对象
后来,我们改变了a的值, a指向了b指向的对象, 所以之前a指向的那个对象, 就变成了垃圾
这个垃圾在浏览器觉得没有用的时候, 就会被回收
再来个例子
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的属性值的改变, 这种拷贝我们就叫做浅拷贝
浅拷贝内存图
深拷贝
所有基本类型的复制都是深拷贝, 所以我们不讨论基本类型的深拷贝
深拷贝内存图
深拷贝就到下回更新.....