1. 原始值与引用值
JavaScript中变量可以分为两种类型,一种是原始值,另外一种是引用值。原始值就是基础类型的数据,引用值就是类似数组,对象,方法等由多个值构成的对象。
1.1 值的存储以及栈内存 / 堆内存
原始值包含有null, undefined, string, number, boolean, symbol等六种,而引用值则包含有object, array, function, Date, Math等... 。且原始值和引用值的存储位置也不同,原始值是存储在栈内存中,而引用值则存储在堆内存中,而栈内存和堆内存的引用方式也不一样。首先我们来说一下栈内存。
栈内存: 我们在声明使用原始值变量时,栈内存会分配一块区域来存这个变量名和变量值,使用时也会在内存中直接调用这个变量。原始值在复制变量时是重新拷贝一份变量来的,就是在内存中再次分配一块儿区域给新复制的变量使用
let num1 = 5
let num2 = num1
如图所示,首先声明了一个变量num1他的值为5,之后又声明了一个新的变量num2,并将num1的值赋给num2,而在内存中展示的就是新开了一块内存空间,这里有点深拷贝的味道了
堆内存: 将一个引用值赋值给一个变量名时,这是数据存储在堆内存中,而且此时变量名的实际值并不是该引用数据,而是一个地址。为什么呢?是因为存储的时候引用值是存储在堆内存中,而同时生成了一个地址指向该引用值在堆内存中的位置,而变量名的值就是这个地址,每次访问数据或者修改引用值的时候都是通过这个地址的。
let obj = {name: '楚泽'}
let obj1 = obj
结合上面的代码和下面的图来说:声明了一个引用值,变量名是obj,之后将obj的值赋给了obj1,但是实际上赋 obj1 的并不是引用值本身,而是它对应的保存在栈内存中的引用地址。引用地址不变,所以只想的引用值也是同一个不会变化的
1.2 值传递和引用传递
值传递: 就是在函数调用时传递一个基本类型的值,这个值发生变化后不会影响原本的值,如下面代码:
let num = 12
function fun(a){
a = 13
console.log(a)
}
fun() // 13
console.log(num) 12
代码中函数fun执行过程中将传进来的参数a重新赋值为13,但是在函数外面打印num时值时没有发生变化的。是因为函数体内a重新赋值时,它的值是保存在栈内存中的,所以就重新分配了一个新的值,新值和外面的旧值时没有关联的。
引用传递: 引用传递就是在函数调用时使用参数传递进去一个引用值,但是这里在函数体内改变引用值属性的时候,作为参数传进去的引用值也会发生变化,但是这分为两种情况,一种是修改引用值,另一种是完全覆盖引用值,采用完全覆盖的时候效果和值传递的效果是一样的,看代码
// 1. 修改引用值
let num = {}
function fun(a) {
a.name = '楚泽'
console.log(a)
}
fun(num) // {name: '楚泽'}
console.log(num) // {name: '楚泽'}
// 2. 覆盖引用值
let num = {}
function fun(a) {
a = []
console.log(a)
}
fun(num) // []
console.log(num) // {}
如代码所示,第一种情况是把变量 num 当作参数传到了方法中,但是仅对其添加了一个 name 属性,所以后面打印的时候两者都打印了相同的结果。
第二种情况是在方法体内直接将参数值给覆盖掉了,之后打印出来的结果和值传递的结果是一样的。这点以后日常开发需要注意到
1.3 变量类型
1.3.1 typeof
typeof最适合用来判断一个变量是否是原始类型,比如
let s = "Nicholas";
let b = true;
let i = 22;
let u;
let n = null;
let o = new Object();
console.log(typeof s); // string
console.log(typeof i); // number
console.log(typeof b); // boolean
console.log(typeof u); // undefined
console.log(typeof n); // object
console.log(typeof o); // object
typeof虽然用来判断原始值很有作用,但是判断引用类型值的时候却不准确,引用类型的值以及null在使用typeof来判断时返回的结果都是object,所以这个时候可以使用instanceof操作符来判断变量的类型
1.3.2 instanceof
instanceof主要用来判断某个变量是否是某个引用类型的实例,同时用来判断变量是否是某个构造函数的实例的话也是可以的
let str = []
let fun = function () { }
console.log(str instanceof Array)
console.log(fun instanceof Function);
// 判断是否是某个构造函数的实力
function Person() { }
const person1 = new Person()
console.log(person1 instanceof Person)
2. 执行上下文与作用域
2.1 执行栈
首先先来大致了解下什么是执行栈,栈是一个后进先出的数据结构,javascript在执行的过中首先会将全局执行上下文放到执行栈的底部,然后执行代码的过程职工遇到A函数执行时会将该函数的执行上下文放到执行栈中,然后执行,在执行A函数的过程中如果再遇到B函数执行,则会再将B函数的执行上下文放到执行栈中,之后继续执行函数B,B函数执行完之后执行栈会将B函数的执行上下文弹出执行栈,然后继续执行,A函数执行完成后会将A函数的执行上下文弹出调用栈,直到所有的代码执行完成,最后将全局上下文的弹出调用栈,可以结合下面的代码和图片来梳理
function cFn() {}
function pFn() {
cFn()
}
pFn()
2.2 执行上下文
什么是执行上下文? 执行上下文就是执行javascript时的环境。只要javascript代码执行,那么它就是在上下文中执行的
执行上下文的三种类型:
- 全局执行上下文
- 函数执行上下文
- eval函数执行上下文
2.2.1 作用域链
每个函数调用都有自己的上下文。当代码执行流进入函数时,函数的上下文会被推到一个上下文栈中,当函数执行完成之后,这个函数执行上下文栈会被弹出上下文栈,并将执行权还给之前的执行上下文,js的执行流就是靠这个执行上下文来控制的。
上下文中的代码在执行的时候,会创建变量对象的一个作用域链,这个作用域链决定了各级上下文在访问函数和变量时的顺序,代码正在执行的上下文的变量始终位于作用域链的最前端
2.2.2 作用域链增强
虽然执行上下文有全局执行上下文和函数执行上下文,但是还有其他的方式来增强作用域链。某些语句会导致在作用域链前端临时增加一个上下文,这个上下文在代码执行完成后会被删除。通常在以下两种情况会出现作用域链增强的情况
try/catch语句的catch块,with语句 这两种情况下都会在作用域链前端添加一个变量对象,对with语句来说会在作用域链前端添加一个指定的变量对象;对于catch来说会创建一个新的变量对象,这个变量对象包含要抛出的错误对象的声明
function buildUrl() {
let qs = "?debug=true";
with(location){
let url = href + qs;
}
return url;
}
这里,with 语句将 location 对象作为上下文,因此 location 会被添加到作用域链前端。 buildUrl()函数中定义了一个变量 qs。当 with 语句中的代码引用变量 href 时,实际上引用的是 location.href,也就是自己变量对象的属性。在引用 qs 时,引用的则是定义在 buildUrl()中的那 个变量,它定义在函数上下文的变量对象上。而在 with 语句中使用 var 声明的变量 url 会成为函数 上下文的一部分,可以作为函数的值被返回;但像这里使用 let 声明的变量 url,因为被限制在块级作 用域(稍后介绍),所以在 with 块之外没有定义