JavaScript基础之作用域,作用域链和闭包

203 阅读10分钟

作用域

先认识全局变量和局部变量

  • 局部变量:在函数内部定义的变量,只在函数内部起作用,函数执行结束,变量会自动删除

即在一个函数内部定义的变量,只在本函数范围内有效

  • 全局变量:“在函数外定义的变量”,即从定义变量的位置到本源文件结束都有效

即可以被程序所有对象或者函数引用 

再认识何为“提升”

我们首先要知道,Javascript在执行前会对整个文件的声明部分做完整分析,包括局部变量,但是不能对变量定义做提前解析。(就是提前知道有这变量,但是不看这变量的值)

未提升:

console.log(a);// a is not defined
a=1;

提升后:

console.log(a);// undefined(声明被提升但没有提升初始值)
var a=1;

函数也是如此:

var fun=function(){
console.log("asd");
}
fun();//asd

fun变量声明被提升了,但是初始化值(函数内容)未被提升

fun();
var fun=function(){
    console.log("asd");//fun is not a function
}

最后认识一下this

this对象指向根据函数的执行环境绑定

情况1. 当函数被作为某个对象的方法调用时,this指向那个对象(上一层)

情况2. this等于最后调用函数的对象

举个栗子:

这里第一个this所属的函数在全局定义中,因此如果这个函数被调用,this等于指向整个文件(就是window),由于是this.name=window.name,所以输出为window.name=kiki。

第二个this所属的函数是对象person的方法,被调用时,this指向含有他的对象,也就是上一层(person),所以调用其实this.name=person.name,输出为person.name=chuchu。

作用域

作用域是指代码中特定部分中变量函数和对象的访问性。简单说就是作用域规定了各个部分的作用范围。

举个栗子:

function print(){
	var a=3;
}
console.log(a);// a is not defined

由于是在在全局作用域中使用变量a,而全局变量并未声明变量a,所以并没有控制台输出结果。一个个作用域可以视为一块块独立的模块,变量可以在模块内自由调用,但是一旦在该作用域之外便无法直接调用,作用域的最大用处就是隔离变量,不同作用域下同名变量不会有冲突。

全局作用域

类似C语言的全局变量,在代码中任何地方都能访问到的对象拥有全局作用域

  • 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
var a=3;
    function printOut(){
    	var b=33;
    	function printIn(){
    		var c=333;
    		console.log(c);
    	}
    	printIn();
    }
    console.log(a);
    console.log(b);//not defined
    printIn();//not defined
  1. 变量a作为全局变量,拥有全局作用域,因此在代码任何位置是被声明过。
  2. 变量b作为函数 printOut() 内的变量,作用域限于该函数内,对外部来说并未声明。
  3. 同理,函数printIn() 是在函数 printOut内声明的,因此对最外层来说也没有声明
  • 所有末定义直接赋值的变量自动声明为拥有全局作用域
var a=3;
    function printOut(){
    	b=33;
    }
    printOut();
    console.log(a);
    console.log(b);

a的作用域与上文同理,需要注意的是:如果未定义直接赋值的变量拥有全局作用域,需要调用printOut() 函数,让主函数知道有b的存在,然后才能使之默认成拥有全局作用域的变量

  • 所有window对象的属性拥有全局作用域

简单来说就是window对象的内置属性拥有全局作用域,是最外层的声明。

函数作用域

指声明在函数内部的变量,一般只在特定的代码片段中能被访问,一般出现在函数内部并作用于函数内部。

栗子1:

function print(){
	var a=3;
	function printIn(){
		console.log(a);
	}
}
console.log(a);// not defined
printIn();// not defined

内层作用域可以访问外层作用域的变量

做一个不是很严谨的比喻,作用域就像我国各地的法院,各市法院处理本市案件,不会越权干涉其他市法院办案,但是如果有市法院无法处理的信息,便通报省法院寻求帮助,若省法院无法处理则向更高机关求助...


栗子2:

add = function (){
    var a = 10;
    a++;
    console.log(a);
} 
for(let i=0;i<3;i++)
{
    add();
}
//11 11 11

函数中没有用的局部变量就会被销毁内存,在你每次函数执行完毕后,你的这个a就被当成没有用的变量被回收了,或者换句话说,内存被销毁了。所以你的a始终都是初始的那个10。

而如果a作为全局变量定义在函数外,只有当你关闭页面or浏览器的时候才会被销毁。所以那时候a仿佛就有了存储功能,能够记录每次变化后的值,无论运行与否,a一直保持着上一次++后的值。

块级作用域

大括号“{}”中间的语句被称为块语句 ,如 if 和 switch 条件语句或 for 和 while 循环语句,不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。

概念

通过let和const声明在一个块语句中,变量在块外无法被访问。通常被创建在一个函数/一个块语句内部。

在函数里声明的变量在外边是不能用的、而块级是可以的

特点

  1. 声明变量不会提升到代码块顶部
function kiki() {
	var a=1;
	if(a)
	{
	    let b=10;
	    var c=20;
	}		
console.log(c);	
console.log(b);
}
kiki();
//20
//b is not defined

此处var声明的c可以在函数kiki() 内被调用,但是let只能在if的大括号内调用,let让变量的作用域范围进一步缩小


  1. 禁止重复声明

一个变量被var申明后进制let声明,否则报错,但如果在嵌套的作用域内使用let声明一个同名的新变量,则不会报错:

var a = 30;
if (1) 
{
let a = 40;
}

  1. 循环中的绑定块作用域

可用于for循环内进行计数 for(let i=0;i<10;i++) 可以避免变量污染

(1)当使用let声明循环变量

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[3](); // 10
a[8](); // 10

变量i是var声明的,为全局变量作用域,在整个for循环中,每次循环中当i发生改变时,a中每个 i 值也是同步改变。

由于a数组是由i一个一个声明的,因此当i=0时,a[0]内存为0,当i=1时,a[i]的所有元素都为a[i](即1)

当i=0时 a: [ a[0] ]  =>> a=[0]
当i=1时 a: [ a[1],a[1]]  =>> a=[1,1]
当i=2时 a: [ a[2],a[2],a[2] ]  =>> a=[2,2,2]

(2)当使用let声明循环变量

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

当前的i只在本次循环有效,每次循环i都是新变量,不会干扰之前赋值的a[i]。

作用域链

自由变量

当前作用域没有定义的变量称为自由变量。 自由变量可以通过向上层(父级作用域寻找)

var a = 10;
function fn(){
    var b=20;
    console.log(a);// 10
    console.log(b);
}

作用域链

如果父级也没有,就一层一层向上寻找,直到找到全局作用域还是没找到,就宣布没有声明。

这种一层一层的关系,就是作用域链 。

  • 作用域链的前端,始终都是当前执行的代码所在环境的变量对象
  • 作用域链中的下一个对象来自于外部环境,而在下一个变量对象则来自下一个外部环境,一直到全局执行环境
  • 全局执行环境的变量对象始终都是作用域链上的最后一个对象

举个栗子1:


function foo() {
    let a = 1;
    function bar() {
        let a = 2;
        function baz() {
            let a = 3;
            console.log(a);
        }
        baz();
    }
    bar();
}
foo();

baz 的 scope: [baz,bar,foo,window]

bar 的 scope: [bar,foo,window]

foo 的 scope: [foo,window]


举个栗子2:


let a = 1;
function foo(){
    let a = 2;
    function baz(){            
    console.log(a);     
  }
  bar(baz);//
}

function bar(fn){
  let a = 3;    
  fn();//
}

foo();

foo 的 scope: [foo,window]

bar 的 scope: [bar,foo,window]

baz 的 scope: [baz,bar,foo,window]

闭包

认识闭包

function foo(){
    var a = 1;
    function bar(){
        console.log(a);
    }
}

对于这个函数,我想在foo()外部调用bar()函数,但是从上面所学作用域内容来看是不可以的,这时候我们需要使用闭包来保存函数内的函数或变量

优点:

  • 闭包可以形成独立的空间,永久的保存局部变量。
  • 避免全局变量污染
  • 变量长期在内存中

缺点:

  • 内存泄露
  • 性能降低

用处:

  • 在外部操作函数内部变量
  • 变量始终保持在内存中,不会函数执行后就被销毁

栗子:

function foo(){
    var a = 1;
    function bar(){
        console.log(a);
    }
    
    return bar;
}

var baz = foo();

将bar()作为返回值从foo()中返回,这样就可以在使用foo()时调用内部的bar()

分析几个闭包

  • 代码一:

在函数math() 中包含了一个add() 函数,并将其作为返回值返回。因为在math() 函数被调用结束后,math() 的局部上下文就会被销毁,所以虽然是将math() 赋给变量adder() ,其实是将add() 连同内部的函数求和部分,赋给变量adder()

这时,原本值为undefined的变量adder() 便拥有了函数意义(变成函数)。


  • 代码二:
function createCounter() {
   let counter = 0
   const myFunction = function() {
     counter = counter + 1
     return counter
   }
   return myFunction
 }
 
 const increment = createCounter()
 const c1 = increment()
 const c2 = increment()
 const c3 = increment()
 console.log('example increment', c1, c2, c3)

myFunction() 函数作为返回值通过createCounter() 返回,下面调用createCounter() 的时候,会调用内部的myFunction() 函数。

  1. 在c1调用的时候,开始执行myFunction() 函数,寻找变量counter,在闭包createCounter() 内有counter变量,其值为0,在myFunction() 函数结束后返回counter的值到c1。
  2. 在c2调用的时候,由于闭包的作用,内部counter变量的值还保留为1,这时myFunction() 函数结束后返回counter=2到c2。
  3. c3同上

  • 代码三:
let c = 4
function addX(x) {
  return function(n) {
     return n + x
  }
}

const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)

addX() 函数可以接受参数x并返回另一个函数。他所返回的函数还接受一个参数并将其添加到变量x中。

开始调用时,addX(3)意为将x=3传入addX()函数并复制给内函数中的x,并返回内函数的接口。

变量addThree也拥有函数可以接受参数的能力,传入c=4进入函数addThree(),这时c=4进入并赋值给n。

变量d接受了函数addThree()最后的结果:内函数所返回的3+4=7

销毁闭包

类似《寻梦环游记》的设定: 一个人真正的死亡,是被所有的人遗忘。

当再也没有代码需要这个函数的时候,他和他内部的变量都会被销毁掉。

只要代码中不再保存这个函数的引用了,这个函数和函数所形成的闭包也就会被一并销毁