JavaScript基础-作用域和闭包

598 阅读4分钟

JavaScript基础-作用域和闭包

作用域是根据名称查找变量的一套规则

嵌套的作用域链⛓都是,引擎从当前的执行作用域开始查找 ,逐级往外的上一级递增查找

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定

eval(...)和with(...){}这两个欺骗语法的代码,没有任何的性能优化,因为优化是依赖于代码根据代码的词法静态分析,并预先确定好所有变量和函数的定义位置

function foo(a){
	var b=a*2;
  function bar(c) {
  	console.log(a,b,c)
  }
  bar(b*3)
}
foo(2); // 2,4,12


作用域可以大致分为函数作用域和块作用域

0. 作用域利用

  1. 利用函数作用域来隐藏一些变量和函数,达到一个内容私有化,符合最小授权或者最小暴露原则
function doSomething(a) {
	function doSomethingElse(a) {
  	return a-1;
  }
  var b;
  b = a+doSomethingElse(a*2);
}

doSomething(2); // 15
  1. 规避冲突,可以避免同名标识之间的冲突,导致变量的值被意外覆盖

1. 函数作用域

区分 函数声明 和 函数表达式 的关键是,如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式,有什么区别就是,它们的名称标识符将会绑定在何处

// 函数声明
function foo(){...}

// 函数表达式
(function foo(){...})();
var foo = function(){...}


始终给函数表达式命名是一个最佳实践

1.1 立即执行函数表达式-IIFE

// 第一种形式
(function IIFE(){}())
// 第二种形式
(function IIFE(){})()
// 挑选哪个看个人喜好❤️

2. 块作用域

let关键字可以将变量绑定到所在的任意作用域中( 通常是{...} 内部 ),这个关键字的书写✍️是遵循先声明再使用,不然就报错了

2.1 垃圾收集♻️

function process(data){
// 在这里做点有趣的事情
}
// 在这个块中定义的内容完事可以销毁,如果没有这个块作用域,由于click函数形成了一个覆盖整个作用域的闭包
// JavaScript引擎极有可能依然保存着这个结构,所以要使用块作用域来利于垃圾收集♻️
{
	let someReallyBigData = {};
  process(someReallyBigData);
}
var btn  = document.getElementById("button");
btn.addEventListener("click",function click(evt){
	console.log("button click");
})

3. 提升

编译器介入的时候,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理

// 变量提升的规则
(function foo() {
	console.log(a); // undefined
  var a = 2;
})();


函数优先:函数会首先被提升,然后才是变量,所以我们平时叫函数是第一等公民

4. 闭包

闭包,是作用域使用场景的一种应用表现,一个内部函数本身当作一个值类型被传递到外面使用,但是它还保留着对它声明时的作用域的引用,这个引用就是闭包

function foo() {
  // bar的父作用域是foo的作用域
	var a =2;
  // 内部函数
  function bar() {
  	console.log(a);
  }
  return bar;
}
var brz = foo(); // 2 -- 这就是闭包的效果

4.1 闭包的应用

本质上无论何时何地,如果将(访问他们各自词法作用域的)函数当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用,定时器⏲,事件监听器,Ajax请求,跨窗口通信,Web Work或者任何其他的异步(同步)任务中,只要使用了回调函数,实际上就是使用了闭包。

function wait(msg) {
	setTimeout(function timer(){
  	console.log(msg);
  },1000)
}
wait("hello BMW");


经典例子

// 只会打印都是5
// 主线程很快就遍历完了 i -> 5,将timer放在事件队列里面等待时间去执行
// 所以直接取得都是循环里面的作用域的i,就是5
for(var i=1;i<=5;i++){
	setTimeout(function timer(){
  	console.log(i); // 5 5 5 5 5
  }, i*1000)
}

// 改造1
for(var i=1;i<=5;i++){
  // 每次执行创建一个作用域
  function(j) {
    // 闭包
    setTimeout(function timer(){
  		console.log(j); // 1 2 3 4 5
  	}, j*1000)
  }(i);
}

// 改造2,let关键字使用,产生块级作用域
for(let i=1;i<=5;i++){ 
    setTimeout(function timer(){
  		console.log(i); // 1 2 3 4 5
  	}, i*1000)
}

4.2 模块

模块机制,特别说一下ES6的模块,它必须被定义在独立的文件中(一个文件一个模块),浏览器或引擎有一个默认的“默认加载器”

// bar.js
function hello(who){
 return "Let me introduce:" + who;
}
export hello;

// foo.js
import hello from "bar";
var hungry = "hippo";

function awesome() {
	console.log(hello(hungry).toUpperCase());
}
export awesome;
// baz.js
module bar from "bar";
module foo from "foo";

console.log(bar.hello("Benz"));// Let me introduce:Benz
console.log(foo.awesome()); // Let me introduce:HIPPO

模块文件中内容会被当作好像包含在作用域闭包中一样来处理,就和函数闭包模块一样的

模块两个特征:

  • 创建内部作用域而调用了一个包装函数
  • 包装函数的返回值必须至少包括一个对内部函数的引用