在阅读之前需要理解:作用域
和函数作用域
(这两个大家直接去百度就可以),以及 js代码运行过程
、 声明变量
和 变量提升
,有助于理解他们的区别!
1、编译是把代码拿过来创建执行上下文,并创建变量环境、词法环境、可执行代码,将执行上下文压入执行栈。
2、执行是在当前执行上下文环境下执行可执行代码。
变量环境
:通过var声明
或function(){}声明
的变量存在这里,有全局和函数作用域词法环境
:通过let
、const
、with()
、try-catch
创建的变量存在这里,有全局、块、函数作用域可执行代码
:变量声明提前后,剩下的代码
例如正常代码是:
var global1=1;
let global2=2;
function func(){
var func1=11;
let func2=22;
{
var block1=111;
let block2=222;
console.log(block2,func2,global2,global1);
}
console.log(func1,block1,global1);
}
func();
console.log(global1,global2);
实际上的执行顺序为:
//全局代码执行过程
var global1;
let global2;
func=function(){}
global1=1;
global2=2;
func();
console.log(global1,global2);
//func执行过程
var func1;
let func2;
var block1;
func1=11;
func2=22;
{
let block2;
block1=111;
block2=222;
console.log(block2,func2,global2,global1);
}
console.log(func1,block1,global1);
在 JavaScript 中创建变量通常称为"声明"变量。变量在脚本中第一次出现是在声明中。第一次用到时就设置于内存
中,便于在后续中使用。
var a // 单个变量声明
console.log(a) // undefined
var b,c // 多个变量一起声明
var d = 4,f = 5 // 多个变量一起声明 + 初始化赋值
e = 6 // 脚本等同于:window.e = 6
console.log(e) // 6
我们可以在声明变量时直接初始化赋值(例如:var a = 1
),也可以单纯只声明变量(例如:var a
),这时其值实际上是 undefined
注意:如果不使用var、let、const去声明变量,它也是合法的JS语法,js解释器会将其直接声明至window下,给予该变量全局范围的可见度。
JavaScript是单线程语言,所以执行肯定是按顺序执行。但是并不是逐行的分析和执行,而是一段一段地分析执行,会先进行编译阶段
然后才是执行阶段
,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。
注: 在编译阶段阶段,代码真正执行前的几毫秒,会检测到所有的变量和函数声明,所有这些函数和变量声明都被添加到名为Lexical Environment
(词法环境)的JavaScript数据结构内的内存中。所以这些变量和函数能在它们真正被声明之前使用。
词法环境
是一种规范类型,用于根据 ECMAScript 代码的词法嵌套结构定义标识符与特定变量和函数的关联。一个词法环境由一个环境记录和一个对外部词法环境的可能为空的引用组成。
简单地说,词法环境是一个包含标识符变量映射的结构
。(这里的标识符是指变量/函数的名称,变量是对实际对象[包括函数对象和数组对象]或原始值的引用)。
为了方便理解词法环境,举个例子:
var a = 20;
var b = 40;
function foo() {
console.log('bar');
}
上面代码在词法环境看来就是:
lexicalEnvironment = {
a: 20,
b: 40,
foo: <ref. to foo function>
}
来延伸一点其他的知识点:
每个词法环境
都包含2个组成部分:
1、环境记录
:在词法环境中存储变量和函数声明的地方。(存储变量和函数声明的实际位置)
(注意 -对于函数代码,环境记录还包含一个arguments
对象)
2、对外部词法环境的引用
。(实际上就是对外部或者说是父级词法环境的引用。这对理解闭包是如何工作的尤为重要。)
1、声明阶段(Declaration phase)在范围内注册变量
2、初始化阶段(Initialization phase)分配内存并为作用域中的变量创建绑定
3、分配阶段(Assignment phase)为初始化变量分配一个值
JavaScript变量分为 局部变量 和 全局变量,我们通过这个来学习一下:
var a = 123;
b = 456;
console.log(a); // 123
console.log(b); // 456
console.log(window.a); // 123
console.log(window.b); // 456
console.log(window); // 结果如下图
可以看出:在函数外,不管是使用var声明变量,还是不用var声明变量,它们都是全局变量,即:在window对象中添加属性并赋值
function fn() {
var a = 123;
b = 456;
console.log(a); // 123
console.log(b); // 456
console.log(window.a); // undefined
console.log(window.b); // 456
}
fn();
console.log(window); // 结果如下图
可以看出:在函数内,使用var声明的变量为局部变量,不用var声明的变量为全局变量
总结:(区别一)
在函数外,用var声明的变量是全局变量,不用var声明的变量是全局变量
在函数内,用var声明的变量是局部变量,不用var声明的变量是全局变量
注: delete
用来删除对象的属性,如果是不可配置的属性返回false,其他情况返回true
var a = 123;
b = 456;
console.log(window.a); // 123
console.log(window.b); // 456
console.log(delete a); // false
console.log(delete b); // true
console.log(window.a); // 123
console.log(window.b); // undefined
可以看出:变量 a、b 都是全局变量,同为window对象的其中一个属性,但是,a 不可以删除,b 可以删除,那么这是为什么呢?我查了很多资料,总结一下原因:
注意:不使用var声明变量:它并不是声明了一个全局变量,而是创建了一个全局对象的属性。
即便如此,可能你还是很难明白“变量声明”跟“创建对象属性”在这里的区别。事实上,Javascript的变量声明、创建属性以及每个Javascript中的每个属性都有一定的标志说明它们的属性----如只读(ReadOnly)、不可枚举(DontEnum)、不可删除(DontDelete)等等。
由于变量声明自带不可删除属性,比如var num = 1 跟 num = 1,前者是变量声明,带不可删除属性,因此无法被删除;后者为全局变量的一个属性,因此可以从全局变量中删除。
我们来验证一下:
首先我们先来了解两个方法:
Object.getOwnPropertyDescriptor();
// 方法返回某个对象的属性的描述对象
该描述对象包含以下信息:
1、value 属性的值
2、writable 属性是否可读写
3、enumerable 属性是否可枚举
4、configurable 属性是否可配置Object.defineProperty();
// 方法会直接在某个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象
configurable
属性更改为false
,他就变成不可删除的,所以我们可以得出一个结论:对象的属性是否可删除,取决于对象的描述对象属性configurable
总结:(区别二)
用var声明的变量默认带不可删除属性
不用var声明的变量默认带可删除的属性
先总结一下:
var声明
是全局作用域或函数作用域,而let和const
是块作用域。var变量
可以在其范围内更新和重新声明;let变量
可以被更新但不能重新声明;const变量
既不能更新也不能重新声明(const用来声明常量)。均存在变量提升
。但是,var变量
会使用变量undefined
初始化变量,但let和const
未初始化变量。var和let
可以在不初始化
的情况下声明变量,const
在声明期间必须初始化
。
已上是最后的结论,接下来我们来细细区分一下:
- 当在最外层函数的外部声明var变量时,作用域是全局的。这意味着在最外层函数的外部用var声明的任何变量都可以在windows中使用
- 当在函数中声明var时,作用域是局部的。这意味着它只能在函数内访问。
可以在相同的作用域内执行下面的操作,并且不会出错
将var声明的变量会被提升到其作用域的顶部,并使用 undefined 值对其进行初始化.
注意:这几个特点也导致var存在一个缺点,导致我们需要新的方法来声明变量,造就了let 和 const 的出现,用一个示例来进行说明:
var greeter = "hey hi";
var times = 4;
if (times > 3) {
var greeter = "say Hello instead";
}
console.log(greeter) // "say Hello instead"
由于times> 3成立,对greeter 进行重新定义赋值。如果你是故意重新定义greeter,这段代码是没有问题的,但是当你不知道之前已经定义了变量greeter时,这将成为产生问题,就会存在变量污染的风险,为了降低这种风险,在块作用域
中使用let来代替var,这样不会污染块作用域的外部作用域,降低 bug率,使代码更安全。let和const实际上为ES6引入了"块级作用域"的概念。
let
目前已经成为变量声明的首选,他是对var声明的改进,也解决了上面所说的var的问题
块是由 {} 界定的代码块,大括号中有一个块。大括号内的任何内容都包含在一个块级作用域中.
像var一样,用let声明的变量可以在其范围内被修改。但与var不同的是,let变量无法在其作用域内被重新声明。
就像var一样,let声明也被提升到作用域顶部。 但不同的是:
- 用
var声明
的变量会被提升到其作用域的顶部,并使用 undefined 值对其进行初始化。- 用
let声明
的变量会被提升到其作用域的顶部,不会对值进行初始化。
因此,如果你尝试在声明前使用let变量,则会收到Reference Error
。
这意味着用const声明的变量的值保持不变,不能修改或重新声明。
因此,每个const声明都必须在声明时进行初始化
我们在最后讨论一个问题:let是否存在变量提升?
let、const 的「创建」过程被提升了,但是初始化没有提升。
var 的「创建」和「初始化」都被提升了。
function 的「创建」「初始化」和「赋值」都被提升了。
提示:let,const变量声明便随着变量的词法环境,在**
未实例化
之前不允许任何方式访问 也就是暂死区(TDZ)
**
在变量声明之前引用块中的变量会导致ReferenceError
,因为从块开始到处理声明之前,该变量处于“临时死区
”中。
对于let的所谓的暂时性死区怎么解释?let和const到底有没有变量提升?
这个面试极易被cue的问题,我在这个给大家推荐一篇文章:https://zhuanlan.zhihu.com/p/28140450,希望可以对大家有所帮助!!!