作用域和作用域链

90 阅读10分钟

作用域是因为函数的产生而产生的独特的东西

引入

都说一切对象都有属性,函数也是一种特殊的对象,叫做函数类对象,所以说函数上也有属性。 例如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。

预编译四部曲(函数预编译)

预编译发生在函数执行的前一刻

  1. 创建AO对象(Activation Object) (执行期上下文)AO{}
  2. 找函数里面的形参和变量声明,将变量名和形参名作为AO对象的属性名,值为undefined
  3. 将实参值和形参相统一
  4. 在函数体里面找函数声明,值赋予函数体
//举个例子
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(){}
	}
*/

预编译三部曲(全局)

  1. 创建GO对象(Global Object)(全局的执行上下文) (就是window)GO{}
  2. 找变量声明,将变量声明作为GO对象的属性名,值赋予undefined
  3. 找全局里的函数声明,将函数名作为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
//函数外部不能访问在函数内部声明的变量

总结:

  1. 函数内部声明的变量,在函数外部无法被访问;
  2. 函数的参数也是函数内部的局部变量;
  3. 不同函数内部声明的变量无法相互访问;
  4. 函数执行完毕后,函数内部的变量实际被清空了。

块级作用域

在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

总结:

  1. let声明的变量会产生块作用域,var不会产生块作用域;
  2. const声明的常量也会产生块作用域;
  3. 不同代码块之间的变量无法互相访问;
  4. 推荐使用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一样的特点差不多,但是

  • 声明变量后必须赋值,进行初始化
  • 值不能被修改