原始值与引用值
js中7种数据类型:
Number、String、Boolean、Null、Undefined、Symbal是属于原始值类型;
Object(Aarray、Function)属于引用值类型。
区别
我们都知道,js中变量的储存都是存在栈内存里面,栈内存的特点是,存取速度快,但是空间较小,所以只会原始值会直接存储,而引用类型,即对象,一般相较于原始值来说,占存储空间会比较大,所以,在栈内存中存储的是一个堆内存的引用(指针),换句话说,就是一个键值对,key存在栈里面,这个key就是堆中该对象的地址,需要访问这个对象时,通过栈中的key,去堆中取出对应的对象。
原始值存储
引用值存储
参数传递
函数Function可以接受参数,那么这个接收的这个参数就有原始值和引用值两种存在,那么这两种参数传递有什么不一样呢,js的参数传递是按值传递还是按引用传递呢?
我们来看一个原始值作为参数的例子
function addItem(num) {
num += 10
return num
}
let count = 10
const res = addItem(count)
console.log(res, count) // 20, 10
我们可以看到,count作为参数传入addItem, 并赋值给num, num进行了自增10并返回,结果count并没有改变,说明,原始值传入函数时,会将值复制给函数的局部变量(num),这个局部变量在函数执行完毕后就会进行销毁(闭包除外)。所以在函数里面对于局部变量的修改并不会影响函数外面的值。
我们再来看一个引用传值的例子:
function sayName(obj) {
obj.name = 'jack'
}
const person = {}
sayName(person)
console.log(person.name) // jack
sayName函数将传入的obj对象的name属性设置为jack, 可以看到person对象打印出了jack,说明函数里面局部变量的修改影响到了外面的person,我们知道person是个引用值,存储是堆内存里面的引用,我们将person复制给局部变量obj的时候,复制的便是该引用,所以obj在执行name赋值的时候,会先到堆内存中取到该对象,在对该对象进行操作,最后读取person.name时,也是到堆内存中读取统一对象。
确定类型
判断一个变量是原始值还是引用类型的值,常用有以下两种方式:
- typeof
- instansof
typeof
typeof是用来判断原始值最好的方式
console.log(typeof 123) // number
console.log(typeof false) // boolean
console.log(typeof '1231') // string
console.log(typeof undefined) //undefined
console.log(typeof Symbol(123)) //symbol
但是用于判断引用类型就没有那么好用了
console.log(typeof null) // object
console.log(typeof new Object({})) // object
console.log(typeof function () {}) // function
console.log(typeof new Array([])) // object
我们要判断引用类型的时候往往是想知道这个变量究竟是Array还是Date,但是typeof给我们的只有一个object,这也太不智能了吧。
为什么typeof不能具体的识别呢,那我们就来说一说typeof的原理吧
在js底层存储变量的时候,会在变量机器码的1~3位存储该变量的类型:
- 000:
object
- 010:
float
- 100:
string
- 110:
boolean
- 1:
number
目前看起来一切都还挺正常的,我们上面还有两个主角没有进行编码,就是null
和undefined
null的机器码全部都是0, 那么问题来了,object的机器码也是000,typeof就会直接判断null
是object
, 这是js的一个历史遗留问题。
undefined
:用 −2^30 整数来表示
instansof我们后面原型链章节在进行阐述。
执行上下文和作用域
我的理解就是代码运行的环境,就像是一个盒子,这个盒子只有一个口子连接外面的世界。
我们代码运行在一个封闭的盒子里,我们只能拿到这个盒子里的物品进行使用,这些物品就是这个盒子里的变量,方法等。
但是
, 我们刚说了,这个盒子还有一个口子通向外面的世界,当我们在自己的盒子里找不到对应的变量时,就会通过这个口子去到上一层的盒子中进行寻找,直到找到变量或者到达最上面一层中还是没有,则会告知没有这个变量。
其中这个盒子就是我们代码运行的作用域,这整个从小盒子到最上层的盒子(window对象)就是执行上下文。
作用域链增强
在代码运行前,强行给该作用域增加一个临时的上下文,这个上下文会在代码执行完毕后删除。
这种作用域增加有两种方式:with
以及try/catch
语句
with
会在作用域前端增加一个指定的对象
function buildUrl() {
let qs = "?debug=true";
with(location){
let url = href + qs;
}
return url;
}
我们可以看到这个例子中,在with对应的代码块中,可以获取到location的属性,即with语句将location作为临时上下文。
try/catch
catch会在该代码块中增加一个错误对象的临时上下文
try{
///
} catch(e) {
// e就是增加的临时上下文,代表错误信息
}
变量声明
js中声明变量有三种方式: var、let、const
var
是js一开始便有的声明变量的方式,let
和const
是在es6中新增的两种方式。
es6之所以要新增,是因为var
存在着比较严重的问题,也就是变量提升
首先我们要知道, var
是函数作用域,let
和const
是块级作用域。
变量提升
我们先用一段代码来理解一下变量提升
function test () {
console.log(a) // undefined
var a = 123
}
这里我们可以看到的是,我们在定义a之前打印了a, 这个a竟然是有值的,只不过这个值并不是我们赋值给a的123,而是undedined
。那么这段代码实际是什么样的呢?
function test () {
var a
console.log(a) // undefined
a = 123
}
我们上面的代码编译后的真正顺序是下面这样的,var a
进行了变量提升,提升到了作用域的最前面,但是要注意的是:提升的只是变量的声明,变量的赋值并没有一起提升
所以这里我们便清楚的知道了为什么打印的是undefined了。
与变量提升相似的还是有函数提升
函数提升有函数声明以及初始化都会提升
a() // 123
function a() {
console.log(123)
}
这里我们可以看到,在a函数定义之前使用该函数,是可以正常的执行的,这说明了a函数的变量和声明都进行了提升。
但是,函数表达式不能进行提升
console.log(a) // undefined
a() // 报错,a不是一个函数
var a = function() {
console.log(123)
}
这里我们可以看到打印a是undeined,是因为var a
进行了变量提升,后面执行a()
报错则说明函数的表达式并没有进行提升。
函数提升与变量提升的先后顺序
在js中函数是一等公民,所以函数提升是优于变量提升的。
console.log(a) // [Function a]
var a = 123
function a () {
console.log(123)
}
编译后:
var a
function a() {}
console.log(a)
a = 123
可以看出,变量提升是在函数提升之前的,但是函数提升的赋值是在变量提升之前的,因为变量提升只会提升声明。
tips: 隐式变量不会进行变量提升
a = 123 不会提升
var a = 123 才会进行提升