今天我们将来讲点你不知道的小知识,深挖数据类型分类、构造函数和包装类的底层原理,现在我们一起来看看吧。
数据类型:
- 简单基本类型(原始类型--在ES6之前,有这五种原始类型):
- let num= 123
- let str = 'hello'
- let flag = true
- let un = undefined
- let nu = null
- 复杂基本类型(引用类型):
- let obj = {}
- let fun = function () {}
- let arr = []
- 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了:
编译完成后执行代码,将a赋值为1,再将a的值也赋给b,b值为1,之后又将a的值修改为2:
所以此时输出b会得到1。
我们再来看第二段代码:
首先进行全局编译,全局执行上下文入栈:
然后开始执行,但是a是一个对象,是引用类型,我们并不能直接把对象给a,因为引用类型里可以继续存放引用类型,我们不知道这个对象到底占了多大的空间,而调用栈是用来管理函数调用关系的,为了使函数间调用得更快来提高V8引擎的效率,调用栈的实际空间是不大的。
所以我们会把引用类型的值存放进堆里,而调用栈中它的值为它的值在堆中的地址:
执行下一条命令b=a,则b同样指向a在堆中的地址1010,之后再修改a.age,堆中age的值变为20,再去打印b.age,b依然指向和a同样的地址,所以b.age此时值为20。
现在我们就知道了:
原始类型值直接存在调用栈中。
引用类型值存在堆中,在调用栈里存放的只是堆中的地址。
对象
- 对象的创建有哪几种方法呢
-
对象字面量
-
new Object()
-
自定义构造函数
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调用一个函数就可以得到一个对象,那这个过程是怎么实现的呢呢?
- 创建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
。