美文网首页我爱编程
JS基础系列(二): JS里的类型

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

作者: squall1744 | 来源:发表于2018-03-26 20:59 被阅读0次

    内置类型


    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的世界中, 很多人倾向于认为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中常用的原生函数有:

    • 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各指向一个对象


    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的属性值的改变, 这种拷贝我们就叫做浅拷贝

    浅拷贝内存图

    浅拷贝

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


    深拷贝

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

    相关文章

      网友评论

        本文标题:JS基础系列(二): JS里的类型

        本文链接:https://www.haomeiwen.com/subject/phqdcftx.html