JavaScript 的变量提升
在 ES5 中,JavaScript 的 var 会进行变量提升,在所以我们在变量声明前访问变量,不会报错,而是得到 undefined。
变量提升可以这样看:
console.log(a) // undefined
var a = 1
console.log(a) // 1
可以看作:
var a;
console.log(a) // undefined
a = 1;
console.log(a) // 1
而 let 和 const 也会进行声明提升,但是在初始化前,会有一个暂时性死区(TDZ)。
即在真正的初始化前,使用 let 或 const 声明的变量,会报错(var 不推荐使用,ES6 后,推荐使用 let 定义变量,const 定义常量)。
所有的声明都是被提升到块顶部,var顶部申明同时初始化为undefined,let 在顶部申明直到遇到 let 才进行初始化,function 顶部申明同时初始化和赋值(不像let可以留空初始化为undefined),变量初始化之后才能进行其他操作,TDZ 就是这么来的。
JavaScript 的函数提升
JavaScript 声明的函数会有提升,所有我们声明在后面的函数可以在它的前面使用它。如果声明了两个同样的函数,后面的函数会覆盖前面的函数,无论如何我们使用的只会是后面声明的函数。(当变量对象中名称已经存在时,变量声明什么也不做。)
//函数提升
foo(); //foo2
function foo(){ //fun1
console.log('foo1');
}
foo(); //foo2
function foo(){ //fun2
console.log('foo2');
}
foo(); //foo2
等同于:
function foo(){ //fun1
console.log('foo1');
}
function foo(){ //fun2
console.log('foo2');
}
foo(); //foo2
foo(); //foo2
foo(); //foo2
JavaScript 声明提升,函数 > 变量
JavaScript 的声明优先级中,函数的声明优先级是大于变量的。究其原因是因为函数的声明提升在变量的声明提升前(当变量对象中名称已经存在时,变量声明什么也不做。)
// 声明优先级,函数 > 变量
foo(); //foo2 //这里是函数
var foo = function(){
console.log('foo1');
}
foo(); //foo1,foo重新赋值
function foo(){
console.log('foo2');
}
foo(); //foo1
等同于:
function foo(){
console.log('foo2');
}
var foo;
foo(); //foo2 //这里是函数
foo = function(){
console.log('foo1');
}
foo(); //foo1,foo重新赋值
foo(); //foo1
JavaScript 的执行上下文和执行栈
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念,JavaScript 中运行任何的代码都是在执行上下文中运行。
执行上下文的生命周期包括:创建阶段=>执行阶段=>回收阶段
JavaScript 是使用执行栈来管理执行上下文的。
执行栈
执行栈是一个存储函数调用的栈结构,遵循先进后出原则。
- JavaScript 是单线程的,所有的代码都是排队执行的。
- 一开始浏览器执行全局的代码时,首先会创建全局的执行上下文,压入执行栈的顶部。
- **每当进入一个函数的执行就会创建函数的执行上下文,并且把它压入执行栈的顶部。**当前函数执行完成后,当前函数的执行上下文出栈,并等待垃圾回收。
- 浏览器的 JS 执行引擎总是访问栈顶的执行上下文。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
执行上下文
JavaScript 的执行上下文的创建阶段会做一下三件事:
- 创建变量对象:首先初始化函数的参数 arguments,提升函数声明和变量声明。
- 创建作用域链
- 确定 this 指向
function test(arg){
// 1. 形参 arg 是 "hi"
// 2. 因为函数声明比变量声明优先级高,所以此时 arg 是 function
console.log(arg);
var arg = 'hello'; // 3.var arg 变量声明被忽略, arg = 'hello'被执行
function arg(){
console.log('hello world')
}
console.log(arg);
}
test('hi');
/* 输出:
function arg() {
console.log('hello world');
}
hello
*/
当函数执行的时候,首先会形成一个新的私有的作用域,然后依次按照如下步骤执行:
- 如果有形参,先给形参赋值
- 进行私有作用域中的预解释,函数声明优先级比变量声明优先级高,最后后者会被前者所覆盖,但是可以重新赋值
- 私有作用域中的代码从上到下执行
总结
每次调用函数,都会创建新的执行上下文,JavaScript 引擎通过执行栈管理执行上下文,遵循栈的先进后出原则。
JavaScript 变量的在内存中的存放
在内存中,是以栈和堆的形式存放变量的,JavaScript 中有两种数据类型,分别是:
基本数据类型:String,Number,Boolean,Null,Undefined,Symbol(ES6)
引用数据类型:Object(我们常用的 Array 就是 Object 类型的数据)
基本数据类型在内存中有固定大小的空间,它通过按值来访问。JavaScript 把基本数据类型直接放在栈内存中。
引用数据类型在内存中的空间大小是不固定的,它的大小根据所放内容的大小而改变。JavaScript 不能把它直接放在栈内存中,而是把引用数据放在堆内存中,栈内存中存放的是它在堆内存中的地址。
但是要注意的是,闭包中的变量并不是保存在栈内存中的,而是保存在堆内存中的,这就解释了为什么闭包能够引用到函数内的变量。
const 的不可变也是指的栈内存中的数据不可变,这就是为什么我们用 cosnt 定义了一个 Object,依然可以改变它的内容的原因:我们改变的是堆内存中的数据,栈内存中的地址只要没有重新赋值就不会变化。
参考
这个系列的文章来自我以前的读书笔记,所以文章中可能参考了其他人的文章却因为我在记笔记时没有记录文章地址而无法列在参考中,这里先说一声抱歉,我会尽量找全参考文章的。