深入理解JavaScript基础之数据类型

200 阅读4分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

TIP 👉 投我以木瓜,报之以琼琚。匪报也,永以为好也!投我以木桃,报之以琼瑶。匪报也,永以为好也!投我以木李,报之以琼玖。匪报也,永以为好也!

前言

TIP 👉> 请说一下js有哪些数据类型?以及它们的区别

这是一道非常常见的面试题。

你可能会回答:Javascript 中的数据类型包括原始类型和引用类型。

其中原始类型包括nullundefinedbooleanstringsymbolnumberbigInt。引用类型指的是 Object,回答不全的👨‍🎓赶紧检讨一下。

水平差点的面试官或者要求低点的面试管这里只问有哪些数据类型可能就结束了。下一题。

数据类型回答全之后,大多数公司一般会接着追问

这两种数据类型的区别是什么?

我们又会回答:

基本数据类型:按值访问,在存储时变量中存储的是值本身,存储在栈中。

引用数据类型:按引用访问,在存储时变量中存储的仅仅是地址,存储在堆里。

接下来可能给你一段代码,分析一下是如何存储的,例如

function test() {
    var a = 1; 
    var b = a; 
    var c = { name: "hello"}; 
    var d = c;
}
test()

我们来简单分析一下这段代码,我们执行这段代码的时候,首先需要编译,创建执行上下文,按照顺序执行代码,当我们执行到var b = a;这一行代码时

变量 a 和 b 的值都被保存在执行上下文中, 而执行上下文又被压入到了栈中,所以变量 a 和 b 的值都是存放在栈中的。

image.png

接下来执行后面的代码,js判断c是一个引用数据类型,这时js会将该对象分配到堆空间里面,分配完成后,该对象有一个堆地址,然后将数据的地址写进变量值,引用数据类型赋值是复制引用地址,所以就是把c的引用地址赋值给d,如图:

image.png

接下来可能简单给你一段代码,说一下输出的结果,例如:

var a = 1;
var b = {a};
console.log(b) // {a: 1}

继续:

delete b.a  // b是{}
b.a = undefined  // {a: undefined}

到这可能关于两种数据类型的区别基本问题完事了。

由数据类型这个知识点可以引出很多很多的问题,

  • 用过symbol吗?有哪些用处?
  • BigInt 解决了什么问题?
  • 如何判断一个值是数组?
  • null和undefined的区别
  • typeof NaN返回什么?
  • Number的存储空间
  • 类型的判断
  • 为什么0.1+0.2 !== 0.3
  • typeof null?
  • 类型的转换
  • ...

接下来我们一一讲解这些问题

用过symbol吗?有哪些用处?

我们来看,每个从symbol()返回的symbol值都是唯一的,这是该数据类型仅有的目的。

console.log(Symbol('foo') === Symbol('foo'))
// false

唯一性,这是symbol的一个应用场景

使用场景一:定义常量

比如我们可以自定义toast的位置,通过css来控制:

const TYPE_BOTTOM = Symbol()
const TYPE_TOP = Symbol()

switch (this.position) {
    case TYPE_TOP:
        classes.push('is-placetop')
        break
    case TYPE_BOTTOM:
        classes.push('is-placebottom')
        break
    default:
        classes.push('is-placemiddle')
}

其实我们并不关心 TYPE_BOTTOM,TYPE_TOP 的值的意义,我们仅仅是想获得两个个唯一的值作为标志而已。

使用场景二:使用Symbol定义类的私有属性/方法

使用Symbol可以让模块里面的属性和方法变成私有

文件a.js

const A = Symbol()
class Test {
    constructor(name, password) {
        this.name = name,
        this[A] = password
    }

    checkPassword(pwd) {
        return this[A] === pwd
    }
}

export default Test

文件b.js

import Test from './a'

const test = new Test('admin', '123456')

test.checkPassword('123456')   // true

test.A      // 拿不到,达成一个私有变量
test[A]     // 拿不到,达成一个私有变量
test['A']   // 拿不到,达成一个私有变量

应用场景三: 使用symbol来作为对象属性名(key)

使用symbol可以定义对象的属性

//直接在对象上通过symbol创建对象属性(是独一无二的)
let obj = {
  name: '青莲使者',
  [Symbol('a')]: function () {
    console.log("a");
  },
  [Symbol('b')]: function () {
    console.log("b");
  }
}

console.log(obj);、

再让我们来看个Symbol的题目

const obj = {
    count: 0,
    ...
}
forconst item of obj) {
    console.log(item) // 1 2 3 4 5 6 7 8 9 10
}

我们可以借助Symbol.iterator来实现,Symbol.iterator 为每一个对象定义了默认的迭代器。该迭代器可以被 for...of 循环使用。

const obj = {
    count: 0,
    [Symbol.interator]: () => {
        return {
            next: () => {
                obj.count++;
                if (obj.count <= 10) {
                    return {
                        value: obj.count,
                        done: false
                    }
                } else {
                    value: undefined,
                    done: true
                }
            }
        }
    }
}
forconst item of obj) {
    console.log(item) // 1 2 3 4 5 6 7 8 9 10
}

BigInt 解决了什么问题?

它提供了一种方法来表示大于 2^53 - 1 的整数。这原本是 Javascript中可以用 [Number]表示的最大数字。BigInt 可以表示任意大的整数。

console.log(9999999999999999);    // → 10000000000000000
console.log(9999999999999999n);

如何判断一个值是数组?

1、数组自带方法 - Array.isArray()

Array.isArray([1, 2, 3]);  // true
Array.isArray({foo: 123}); // false
Array.isArray('foobar');   // false
Array.isArray(undefined);  // false

2、Object.prototype.toString.call(arr) === '[object Array]'

Object.prototype.toString.call([1, 2, 3]) === '[object Array]'

3、instanceof

const a = [1, 2, 3];
const b = {a: 1};
console.log(a instanceof Array);//true
console.log(a instanceof Object);//true,在数组的原型链上也能找到Object构造函数
console.log(b instanceof Array);//false

4、[].constructor == Array

let c = [1, 2, 3]; 
console.log(c.constructor === Array) // true

5、Array.prototype.isPrototypeOf(arr) // 原型链判断

Array.prototype.isPrototypeOf([1, 2, 3]) // true

null和undefined的区别

Undefined类型只有一个值,即undefined。当声明的变量还未被初始化时,变量的默认值为undefined。用法:

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

Null类型也只有一个值,即null。null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。用法

  • 作为函数的参数,表示该函数的参数不是对象。
  • 作为对象原型链的终点。
var i;
i // undefined

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

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

var x = f();
x // undefined

typeof NaN返回什么?

首先呢 NaN 是 ‘not a number’, NaN 和任何变量都不相等,包括 NaN 自己

typeof NaN //number ECMAScript规范说数字类型包括NaN
console.log(NaN === NaN); // false

Number的存储空间

Number类型的最大值为2的53次方,超过js的number类型最大值会发生截断,不精确,解决办法:在传递给前端时,将Long转为String

为什么0.1+0.2 !== 0.3

要弄清这个问题的原因,首先我们需要了解下在计算机中数字是如何存储和运算的。在计算机中,数字无论是定点数还是[浮点数]都是以多位二进制的方式进行存储的。 在JS中数字采用的IEEE 754的双精度标准进行存储,我们可以无需知道他的存储形式,只需要简单的理解成就是存储一个数值所使用的二进制位数比较多而已,这样得到的数会更加精确。

这里为了简单直观,我们使用定点数来说明问题。在定点数中,如果我们以8位二进制来存储数字。

对于整数来说,十进制的35会被存储为: 00100011 其代表 2^5 + 2^1 + 2^0。

对于纯小数来说,十进制的0.375会被存储为: 0.011 其代表 1/2^2 + 1/2^3 = 1/4 + 1/8 = 0.375

而对于像0.1这样的数值用二进制表示你就会发现无法整除,最后算下来会是 0.000110011....由于存储空间有限,最后计算机会舍弃后面的数值,所以我们最后就只能得到一个近似值。 在0.1 + 0.2这个式子中,0.1和0.2都是近似表示的,在他们相加的时候,两个近似值进行了计算,导致最后得到的值是0.30000000000000004,此时对于JS来说,其不够近似于0.3,于是就出现了0.1 + 0.2 != 0.3 这个现象。

typeof null?

typeof null === 'object'; // true

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"

类型的判断

1、typeof

typeof 对于基本类型,除了 null 都可以显示正确的类型

typeof的 实现原理:

我们可以先想一个很有意思的问题,js 在底层是怎么存储数据的类型信息呢?其实,js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息

  • 000:对象

  • 010:浮点数

  • 100:字符串

  • 110:布尔

  • 1:整数

    但是对于undefinednull 来说,这两个值的信息存储是有点特殊的。

    null:所有机器码均为0

    undefined:用 −2^30 整数来表示

    所以,typeof 在判断 null 的时候就出现问题了,由于 null 的所有机器码均为0,因此直接被当做了对象来看待。

typeof 37 === 'number'; // true
typeof NaN === 'number'; // true
typeof 42n === 'bigint'; // true
typeof '' === 'string'; // true
typeof true === 'boolean'; // true
typeof Symbol() === 'symbol'; // true
typeof undefined === 'undefined'; // true
typeof {a: 1} === 'object'; // true
typeof [1, 2, 4] === 'object'; // true
typeof function() {} === 'function'; // true
typeof null === 'object'; // true

2、instanceof

其实 instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false

let myDate = new Date();

myDate instanceof Date;      // true
myDate instanceof Object;    // true
myDate instanceof String;    // false

让我们来实现一款instanceof

function newInstanceof(left, right) {
  let rightProto = right.prototype // 获取右侧的原型
  		left  = left.__proto__ // 获取左侧的__proto__
  while(left) {
    if (left === null) {
      return false;	
    }
    if (left === rightProto) {
      return true;	
    } 
    left = left.__proto__ 
  }
}

3、Object.prototype.toString.call()

// Boolean 类型,tag 为 "Boolean"
Object.prototype.toString.call(true);            // => "[object Boolean]"

// Number 类型,tag 为 "Number"
Object.prototype.toString.call(1);               // => "[object Number]"

// String 类型,tag 为 "String"
Object.prototype.toString.call("");              // => "[object String]"

// Array 类型,tag 为 "String"
Object.prototype.toString.call([]);              // => "[object Array]"

对于 Object.prototype.toString() 方法,会返回一个形如 “[object XXX]” 的字符串。

如果对象的 toString()方法未被重写,就会返回形如上面形式的字符串。

({}).toString();     // => "[object Object]"
Math.toString();     // => "[object Math]"

但是,大多数对象对于toString()方法都是重写了的,因此这时需要使用 call()方法来调用未被重写前的toString方法。

var a = {
  toString() {
    return "a";
  },
};

a.toString();                                     // => "X"
Object.prototype.toString.call(a);                // => "[object Object]"

类型转换

Number 和 String 之间的相互转换

string-->number

string类型   *1  即可变成  number类型

Number()、parseInt()、parseFloat()都可以将字符串转化为数字

number-->string

var num = 1313.179;

console.log(num.toString());    //1313.179
隐式转换
console.log(''+ num);          //1313.179

console.log(''.concat(num));    //1313.179

「欢迎在评论区讨论」

希望看完的朋友可以给个赞,鼓励一下