作用域是因为函数的产生而产生的独特的东西
引入
都说一切对象都有属性,函数也是一种特殊的对象,叫做函数类对象,所以说函数上也有属性。 例如name、prototype等可以访问的属性。
function test(){}
test.name //test
test.prototype
还有一些不能够访问的属性,只供JavaScript引擎存取,例如 [[scope]] (域)
[[scope]]里面存的就是由这个函数产生而产生的**[作用域]**
[[scope]]:作用域,其中存储了执行期上下文的集合。其实[[scope]]里面存的是[作用域链]。
执行期上下文
定义:当函数执行的前一刻的时候(预编译),会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境(例如函数提升,声明提升),函数每次执行时,对应的执行上下文都是独一无二的,所以多次调用一个函数会创建多个执行上下文,当函数执行完毕,它所产生的执行上下文被销毁。
预编译
引入:js运行三部曲
- 语法分析
- 预编译
- 解释说明
预编译
函数声明整体提升。变量 声明提升。
函数声明整体提升
即不管函数写在哪里,系统总是会把函数提到逻辑的最前面。
所以不管是在函数前面调用函数,还是在后面调用函数,本质上都是在函数后面调用。
变量 声明提升
如果 var a = 123,这一步骤是变量声明加变量赋值。
系统会拆分成 var a; a = 123。然后将var a提升到逻辑的最前面。
但是这两句话太过肤浅,不能解决所有问题。这两句话只是把预编译的两个现象抽象出来当作方法来用。
// 如果有这么一个问题
console.log(a)
function a(){
}
var a = 123
//那么a打印的是什么呢?
预编译前奏
imply global 暗示全局变量
任何变量未经声明就赋值,此变量就为全局对象(window)所有。
例如:a = 123
这就未经声明就赋值了,也能访问变量,变量a是全局对象所有,相当于window.a = 10
一切声明的全局变量,全是window的属性
全局上的任何变量,即使声明了也归window所有
例如:在全局上声明一个 var a = 123, 那window.a = 10
总的来说,window就是全局的域
var a = 123
想要访问这个a,需要在一个空间去拿这个变量。
变量首次放的位置就是window,window是一个对象,就像一个仓库
window {
a :123
}
就相当于在window上挂了一个属性a,值为123。
预编译四部曲(函数预编译)
预编译发生在函数执行的前一刻
- 创建AO对象(Activation Object) (执行期上下文)AO{}
- 找函数里面的形参和变量声明,将变量名和形参名作为AO对象的属性名,值为undefined
- 将实参值和形参相统一
- 在函数体里面找函数声明,值赋予函数体
//举个例子
function test(a){
var a = 123;
function a(){}
var b = function(){}
function d(){}
}
test(1)
/*
过程:
1. 创建AO对象 AO{}
2. 找形参和变量声明
AO{
a:undefined,
b:undefined
}
3. 实参和形参统一,将实参的值放到形参里面去
AO{
a:1.
b:undefined
}
4. 找函数体里面的函数声明
AO{
a:function a(){},
b:undefined,
d:function d(){}
}
*/
预编译三部曲(全局)
- 创建GO对象(Global Object)(全局的执行上下文) (就是window)GO{}
- 找变量声明,将变量声明作为GO对象的属性名,值赋予undefined
- 找全局里的函数声明,将函数名作为GO对象的属性名,值赋予函数体
function test(){
var a = b = 123
}
test()
/*
AO{
a:undefined
//没有b,b不是声明也不是参数,则触发了暗示全局变量。
}
//b未经声明就赋值,就归window所有,GO === window
所以
GO{
b:undefined
}
*/
作用域链
scope chain
[[scope]]中所存储的执行期上下文的集合,这个集合呈链式链接,我们把链式链接叫做作用域链。
function a(){
function b(){
var b = 234
}
var a = 123
b()
}
var global = 100
a()
/*
a定义:产生GO a.[[scope]] -->scope chain --> 0:GO{}
a执行:产生执行期上下文AO,然后把执行上下文放到作用域的最顶端
a.[[scope]] -->scope chain -->0 : AO{}
1 : GO{}
b定义: b.[[scope]] -->scope chain -->0 : AO{}
1 : GO{}
b执行: b.[[scope]] -->scope chain -->0 : b的AO{}
1 : a的AO{}
2 : GO{}
*/
查找变量:从作用域链的顶端依次向下查找
在哪个作用域里面查找,就从哪个作用域链的顶端向下查找
作用域
作用域指的是变量的可访问性和可见性。定义了变量的区域。
规定了变量能够被访问的”范围“,离开了这个”范围“便不能被访问。
作用域的作用:提高了程序逻辑的局部性,增强了程序的可靠性,减少了名字冲突。
作用域分为两大类: 全局作用域、局部作用域
通俗地来说,全局作用域在整个范围内有效,局部作用域只在某个范围内有效。
全局作用域
全局作用域是作用在所有代码执行的环境,例如整个script标签里,或者是一个独立的js文件。
在此声明的变量在函数内部也可以被访问,任何其他作用域都可以被访问。
var a = 123 //定义一个全局变量
function demo(){
console.log(a); //123
//函数内部可以使用全局作用域的变量
}
demo()
注意:
未经声明就赋值的变量为全局变量。
一切声明的全局变量,都是window的属性,所以给window添加属性,就是全局的。
要尽可能少的声明全局变量,防止全局变量被污染。
局部作用域
局部作用域又分为函数作用域和块级作用域。
函数作用域
这些变量只能在函数内部访问,不能在函数以外去访问。
function demo(){
var a = 123
}
demo()
console.log(a); //ReferenceError: a is not defined
//函数外部不能访问在函数内部声明的变量
总结:
- 函数内部声明的变量,在函数外部无法被访问;
- 函数的参数也是函数内部的局部变量;
- 不同函数内部声明的变量无法相互访问;
- 函数执行完毕后,函数内部的变量实际被清空了。
块级作用域
在ES6之前只有全局作用域和函数作用域,ES6引入了let和const关键字,从而产生了块级作用域。
使用{}包裹的代码称之为代码块,在大括号中使用let和const声明的变量存在于块级作用域中,在大括号之外不能访问这些变量。
代码块内部的声明的变量外部有可能无法被访问。当用var声明时,外部就能访问变量。
(函数也有大括号,在某些意义上来讲,函数也是块作用域。)
条件语句、循环语句等有大括号的都会产生块作用域。
for(let i = 1;i<3;i++){
console.log(i); // 1 2
}
console.log(i); //ReferenceError: i is not defined
for(var i = 1;i<3;i++){
console.log(i); // 1 2
}
console.log(i); //3
总结:
- let声明的变量会产生块作用域,var不会产生块作用域;
- const声明的常量也会产生块作用域;
- 不同代码块之间的变量无法互相访问;
- 推荐使用let和const
而es6新特性let、const和var有一定的区别
let、const、var的区别
在ES6之前,声明变量的方式只有var,而为了解决作用域的问题,ES6引入了let和const两种声明变量的方式。
那它们的区别有:
1、 作用域
var、let、const的作用域都可以是全局作用域或者是函数作用域,而let和const还存在块级作用域。
2、 重复声明
var允许重复声明变量,后面的值将会覆盖前面的值;
let和const不允许在同一作用域中重复声明,否则将抛出异常。
var a = 123
console.log(a); //123
var a = 234
console.log(a); //234
// var可以重复声明
//let和const不允许在同一个作用域下重复声明
let b = 123
let b = 234
// SyntaxError: Identifier 'b' has already been declared
const c = 123
const c = 234
// SyntaxError: Identifier 'c' has already been declared
let b = 123
{
let b = 234
console.log(b); //234
}
console.log(b); //123
const c = 123
{
const c = 234
console.log(c); //234
}
console.log(c); //123
// let和const在不同作用域下可以重复声明
3、 修改声明的变量
var和let声明的是变量,因此可以修改变量;
而const声明的是常量,因此不可以被修改,会抛出异常。
var a = 123
a = 321
console.log(a); //321
let b = 234
b = 432
console.log(b); //432
const c = 345
c = 543
// TypeError: Assignment to constant variable.
4、 变量 声明提升
如果 var a = 123,这一步骤是变量声明加变量赋值。
系统会拆分成 var a; a = 123。然后将var a提升到逻辑的最前面。
var声明的变量存在变量 声明提升,即变量可以在声明之前调用,值为undefined;
let和const不存在变量 声明变量提升,即变量在声明之前调用的话会抛出异常ReferenceError,称为 ”暂时性死区“ ,所以只能在声明变量后使用该变量。
console.log(a); //undefined
var a = 123
console.log(b); //ReferenceError: Cannot access 'b' before initialization
let b = 234
console.log(c); //ReferenceError: Cannot access 'b' before initialization
const c = 345
什么是暂时性死区?
当程序的控制流程在新的作用域(module function 或 block作用域)进行实例化时,在此作用域中用let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,所以是不能被访问的,如果访问就会抛出错误。因此,在这运行流程进入作用域创建变量,到变量可以被访问之间的这一段时间,就称之为暂时死区。
5、 变量赋值
var和let声明的是变量
而const声明的是常量,所以声明前要进行初始化,必须赋值。
var a;
console.log(a); //undefined
let b;
console.log(b); //undefined
const c;
console.log(c); //SyntaxError: Missing initializer in const declaration
总结
var关键字的特点:
- 有全局作用域、函数作用域的概念,没有块级作用域的概念
- 存在变量 声明提升
- 全局作用域声明的变量会挂载到window下
- 同一作用域中允许重复声明
- 值可以被修改
let关键字的特点:
- 多了块级作用域的概念
- 不存在变量 声明提升
- 暂时性死区
- 同一作用域中不允许重复声明
- 值可以被修改
const关键字的特点:
和let一样的特点差不多,但是
- 声明变量后必须赋值,进行初始化
- 值不能被修改