关于数据类型分类,构造函数和包装类的重要底层原理

345 阅读6分钟

今天我们将来讲点你不知道的小知识,深挖数据类型分类、构造函数和包装类的底层原理,现在我们一起来看看吧。

数据类型:

  • 简单基本类型(原始类型--在ES6之前,有这五种原始类型):
  1. let num= 123
  2. let str = 'hello'
  3. let flag = true
  4. let un = undefined
  5. let nu = null
  • 复杂基本类型(引用类型):
  1. let obj = {}
  2. let fun = function () {}
  3. let arr = []
  4. let date = new Date()

为什么原始类型是原始类型,引用类型是引用类型,它们是怎么区分的呢?我们一起来探索一下吧,看这段简单的代码,其中a是原始类型:

let a = 1
let b = a
a = 2
console.log(b);

很明显,输出b会得到1对吧,那我们再看这段代码,其中a是引用类型:

let a = {
    age: 18
}
let b = a
a.age = 20
console.log(b.age);

你也想得到,输出b的结果会是20,但是你对比一下这两段代码,明明结构很相似,为什么第一段代码b能输出第一个a的值,而第二段b.age输出的不是原来的18,而是修改后的20呢?

这就是原始类型和引用类型的区别所在啦。

我们来看第一段代码执行的原理:

首先编译全局,全局执行上下文进入调用栈(如果你还不了解js的预编译和调用栈,请先去看一下这两篇文章哦:《简单易懂!!超详细带你了解什么是JavaScript预编译!》《JavaScript调用栈和作用域链指南:新手也能快速上手》),其中a,b的值为undefined,图中简写为un了:

屏幕截图 2023-11-15 155452.png

编译完成后执行代码,将a赋值为1,再将a的值也赋给b,b值为1,之后又将a的值修改为2:

屏幕截图 2023-11-15 160646.png 所以此时输出b会得到1。

我们再来看第二段代码:

首先进行全局编译,全局执行上下文入栈:

屏幕截图 2023-11-15 155452.png

然后开始执行,但是a是一个对象,是引用类型,我们并不能直接把对象给a,因为引用类型里可以继续存放引用类型,我们不知道这个对象到底占了多大的空间,而调用栈是用来管理函数调用关系的,为了使函数间调用得更快来提高V8引擎的效率,调用栈的实际空间是不大的。

屏幕截图 2023-11-15 162259.png

所以我们会把引用类型的值存放进里,而调用栈中它的值为它的值在堆中的地址:

屏幕截图 2023-11-15 162259.png

执行下一条命令b=a,则b同样指向a在堆中的地址1010,之后再修改a.age,堆中age的值变为20,再去打印b.age,b依然指向和a同样的地址,所以b.age此时值为20。

现在我们就知道了:

原始类型值直接存在调用栈中

引用类型值存在堆中,在调用栈里存放的只是堆中的地址

对象

  • 对象的创建有哪几种方法呢
  1. 对象字面量

  2. new Object()

  3. 自定义构造函数

     var obj = {}// 对象字面量
    
     var obj2 = new Object();// 构造函数
    
     // 自定义构造函数
     function Car(){
    
     }    
     
    

构造函数

构造函数是一个工厂,可以批量化生成对象。

function Car() {
    this.name = 'BMW'
    this.lang = 4900
    this.height = 1400
}

let car = new Car()
let car2 = new Car()

而我们生成构造函数的实例对象是需要用到new这个操作符的,代码中car和car2都是Car 的实例对象,每使用一次new Car()都会生成一个Car的实例对象,且每个对象都是独立的。每一个实例对象中都会有构造函数Car中的name,lang和height属性,如果我们执行console.log(car.name),可以看到会输出BMW

实际上,任意函数都可以被new调用,被new调用了的函数就是构造函数。

new的过程

我们用new调用一个函数就可以得到一个对象,那这个过程是怎么实现的呢呢?

  1. 创建this空对象

new的第一步,就是在构造函数里创建了一个this空对象。

function Car() {
    this.name = 'BMW'
    this.lang = 4900
    this.height = 1400
    
    var this = {}
}

2. 执行构造函数中的逻辑

第二步就是往this空对象里添加属性,也就是执行构造函数里的代码,让this对象里拥有那些属性:

function Car() {
    this.name = 'BMW'
    this.lang = 4900
    this.height = 1400
    
    var this = {
        name = 'BMW'
        lang = 4900
        height = 1400
    }
}

3. 返回this对象 最后一步就是返回this对象,这样才能得到一个实例对象:

    function Car() {
        this.name = 'BMW'
        this.lang = 4900
        this.height = 1400
    
        var this = {
            name = 'BMW'
            lang = 4900
            height = 1400
        }
        return this
    }

包装类

1.我们要知道这个概念:原始值是不能有属性和方法的,属性和方法是对象独有的

我们以代码来简单讲解一下:

var num = 123
num.a = 'hello'

如果我们声明一个变量a并赋值123,那num为number类型,我们往它身上添加一个值为hello的a属性能成功吗?我们刚刚说原始值是不能有属性和方法的,属性和方法是对象独有的,如果我们console.log(num.a)的话是不是会直接报错呢?不,实际上会输出undefined

这说明执行它的V8引擎竟然承认了num.a,可是num并不是对象啊,我们来看看这究竟是怎么一回事:

  var num = new Number();

看到这行代码,你可能会奇怪:我知道Object()和Array()构造函数,但是Number()也有吗,有的,不仅有Number(),还有String(),Boolean(),这些都是js里的内置函数。

这些内置函数是用来干嘛的呢,实际上大多数情况都是给V8引擎来用的。

我们回到一开始的代码:

var num = 123
num.a = 'hello'
console.log(num.a);

而这段代码V8执行的时候在它的眼中是这样子的:

var num = new Number(123);
num.a = 'hello' 
console.log(num.a);

所以我们往num上挂一个a在V8眼中是合情合理的,它不报错,但V8又必须满足原始值是不能有属性和方法的,它会执行一个delete num.a,之后num.a就会变成一个空对象,我们访问一个对象里不存在的属性就会返回undefined。

我们将V8添加了一个不能添加的属性之后又把它移除的过程叫做类的包装。

我们再来看段代码:

 var arr = [1,2,3] 
 arr.length = 2
 console.log(arr)
 

执行这段代码会输出[1,2],因为我们让数组arr的长度变成2了,但是:

 var str = 'abc'
 str.length = 2
 console.log(str)
 

执行后str仍然为abc,其实字符串str身上是没有length属性的,它是原始值不能有属性,如果有我们会和arr一样看到str长度变少输出ab,我们改不了str的length,但是V8执行引擎会做一个隐式的包装类,在V8眼中这段代码是这样的:

 var str = new String('abc') 
 str.length = 2
 delete str.length
 console.log(str) 
 

可是如果我们console.log(str.length),却能够输出3,这是因为这个length是内置函数String()身上的,这里的str.length实际上在V8的眼里是new String('abc').length