作用域和闭包

146 阅读6分钟

作用域和闭包----你不知道的JavaScript(上)

  1. 作用域

  • js引擎:从头到尾负责js程序的编译及执行过程
  • 编译器,负责语法分析和代码生成(ast树转换为可执行代码)
  • 作用域

eg:var a = 2;

编译器进行词法分析时,会问作用域是否有该变量,若无则声明一个新变量a。

代码运行时,通过引擎来进行赋值操作,引擎先问作用域是不是有一个变量a,是就会直接使用变量进行赋值,否则就向上一级作用域寻找。

  • LHS对赋值操作进行变量查询
  • RHS对引用,找变量源头进行查询。会出现ReferencError,作用域判别失败。
  1. 词法作用域

函数的词法作用域由函数被声明时所处的位置决定。查找变量时从当层开始找,当层找不到就向上一层

  1. 函数作用域

属于这个函数的全部变量都可以在整个函数范围内使用及复用。外部无法直接访问函数内部的变量。因此可以对于代码片段进行封装,使得“隐藏”起来。

方式一:
function foo1() {
  var a = 3;
  console.log(a);
}
方式二:
(function foo1() {
  var a = 3;
  console.log(a)
});

方式一和二的区别在于,方式一中function是声明中第一个词,是一个函数声明,函数标识符foo会被绑定在全局作用域中;而方式二开头是“(”,会被当作函数表达式来看,函数标识符foo绑定在函数表达式自身。会立即执行,而不需要通过调用foo1.

同时,方式二表明函数只能在()中访问,外部无法被访问到,不会污染全局作用域。

函数表达式可匿名,为function(){},无名称标识符“foo”.例如:

setTimeout(function(){
  console.log('aaa')
},1000)
  • 函数表达式可以匿名,函数声明不可以匿名。

IIFE:立即执行函数

方式一:
(function foo1() {
  var a = 3;
  console.log(a)
})();
方式二:
(function foo1() {
  var a = 3;
  console.log(a)
}());

IIFE进阶,将所在作用域的参数传递到立即执行函数里面

(function foo1() {
  var a = 3;
  console.log(a)
})(window);
  1. 块作用域

let

let以{ }内部为作用域,无变量提升(在声明之前不能引用)

应用:

  1. 垃圾回收:

当函数在运行一次之后,之后不会被用到该变量,就可以对该变量进行let声明。会自动在没有引用之后对块作用域进行垃圾回收,不占用大量内存。

  1. let 的for循环

声明时的范围若在函数内,则在函数作用域内可访问变量。若在if判断语句中,变量可在全局访问。

var foo = true,baz = 10;
if (foo) {
  var bar = 3;
  if (baz > bar) {
    console.log(baz);//10,可访问全局的baz
  }
}
function aaa() {
  var a = 1;
}
console.log(bar);//可访问if语句中的变量bar;当bar用let声明时,块级作用域会报错。
console.log(a);//不可访问函数里面的变量a

const

无变量提升,值也不可更改,为常量。当定义对象时,对象的属性可更改

  1. 提升

先编译,声明变量;运行逻辑会留在原地。赋值和console.log都在执行阶段进行。

a=2;
var a;
console.log(a)
//打印2

console.log(a)
var a=2
//undefined
//以上等效为下述顺序

var a//编译阶段
a=2 //执行阶段
console.log(a)//执行

var a//编译阶段
console.log(a)//执行阶段
a=2//执行阶段

只有声明会被提升。

函数声明function foo( ){  }会被提升,函数表达式var foo = function bar( ){  }不会。

函数会优先变量进行提升。

foo();//TypeError.变量标识符foo()被提升,但没有赋值,foo()对undefined值进行函数调用
bar();//ReferenceError
var foo = function bar(){
  //....
}

//上述等效于以下形式
var foo;
foo();
bar();
foo = function(){
  var bar = ...
}

普通块内部的函数声明会提升到作用域顶部。

  1. 闭包

定义:当函数可以记住并访问所在的词法作用域时,即使函数在当前词法作用域之外执行,就产生了闭包。

可访问函数内部之外的作用域即产生了闭包。

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

上述即为闭包,全局作用域底下调用 baz:即使在函数外部,依然可以访问到内部bar的词法作用域。bar词法作用域可以访问foo内部作用域。

原本foo()执行后,其内部作用域会被销毁进行垃圾回收。但闭包使得内存不被回收。bar()本身在使用该内部作用域。

理解:var baz = foo()时,将foo()的返回值bar()赋值给baz。之后调用baz,实际上是调用了内部函数bar().

闭包的应用:

定时器:

function wait(message){
  setTimeout(function timer(){
    console.log(message);
  },1000);
}
wait('hello')

将内部函数timer传递setTimeout()。timer具有涵盖wait()作用域的闭包,因此保有对变量message的引用。

setTimeout持有对参数的引用,引擎会调用这个函数timer,词法作用域在此过程中保持完整。

总结:在定时器、事件监听器等异步(或同步)任务中,只要使用了回调函数,实际上就是在使用闭包。

应用一:IIFE

那么,IIFE立即执行函数是闭包吗?

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

严格来讲,不是闭包。因为函数foo并不是在它本身的词法作用域之外进行的。它在定义所在的作用域中执行(而外部作用域,也就是全局作用域也持有a)。a是通过普通的词法作用域查找而非闭包被发现的

应用二:for循环

for循环是闭包最常见的例子.

for(var i=1;i<=5;i++){
  setTimeout(function timer(){
    console.log(i);
  },i*1000)
}
//6,6,6,6,6间隔一秒出现

预期结果是,按照1-5顺序依次间隔一秒输出。实际运行结果为以每秒一次的频率输出5次6.

原因:循环终止时,不满足i<=5,因此为6. 延迟函数timer的回调会在循环结束后才执行。

深入探讨是啥缺陷导致结果与预期不一致的呢?尽管5个函数是在各自迭代中分别定义的,但是都在一个全局作用域中,实际上引用的只有一个i.我们需要每个迭代时都能有一个i的副本。所以需要更多的闭包作用域。每个迭代都有一个闭包作用域。

解决:每个迭代都要一个闭包作用域。

for(var i=1;i<=5;i++){
  (function(){
    setTimeout(function timer(){
      console.log(i);
    },i*1000);
	})();
}
//6,6,6,6,6

有独立作用域,但是作用域为空,i没有传递进去。并不会有5个i的副本。

for(var i=1;i<=5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j);
    },i*1000);
	})(i);//<-----将i传递进函数,为区别,函数内部参数定为j
}
//6,6,6,6,6

第二种方案,let块作用域。

for(let i=1;i<=5;i++){
  setTimeout(function timer(){
    console.log(i);
  },i*1000)
}
//6,6,6,6,6

应用三:模块化

var foo = (function coolModule(id){
  function change(){
    publicAPI.identify = identify2;
  };
  function identify1(){
    console.log(id)
  };
  function identify2(){
    console.log(id.toUppercase())
  };
  var publicAPI = {
    change:change,
    identify:identify1
  };
  return publicAPI;
})('hello')
foo.identify();//hello
foo.change();
foo.identify();//HELLO

import 可以将一个模块中的一个或者多个API导入当前作用域中;export将当前模块标识符导出为公共API。

附录:

静态作用域和动态作用域:

静态:在声明时就定义

动态:在调用时定义。

JavaScript没有动态作用域,只有词法作用域,但this机制很像动态作用域。