作用域、作用域链、预编译、闭包基础

231 阅读7分钟

1. 作用域与作用域链

为什么需要了解AO和GO呢?因为要利用AO和GO解决作用域,作用域链相关所产生的一切问题;

1.1 函数也是一种对象的类型,也是引用类型,引用值

函数也拥有属性,所以也是对象的一种形式;fn.name,fn.length,fn.prototype等,静态属性;

var obj = {
	name:'实例',
  address:'地址',
  teach:function(){}
}

// 函数也是对象的一种类型
function fn(a, b) {}
console.log(fn.name);
console.log(fn.length);
console.log(fn.prototype);

1.2 函数的隐式属性(无法访问,JS引擎内部固有的隐式属性)

1. [[scope]](作用域)属性、[scope Chain](作用域链)属性

  1. 函数创建时(函数被定义时),生成的一个JS内部的隐式属性[[scope]]
  2. [[scope]] 属性是函数存储作用域链的容器
  3. [scope Chain]作用域链存储函数的AO/GO的容器
  4. AO:函数执行期上下文 GO:全局的执行期上下文,AO是一个即时的存储容器
  5. 函数执行完成之后,AO是要销毁的,函数重新执行,会重新生成一个新的AO,老的AO会被销毁
  6. 函数不调用就不会执行,如果不执行,那么内部的其他函数声明也不会被定义;

作用域链:把AO与GO从上往下排列起来,将这些GO和AO形成链式的关系,就叫做作用域链;

2. 实例操作演示作用域和作用域链的产生与销毁

function a() {
	function b() {
  	var b = 2;
  }
	var a = 1;
  b();
}
var c = 3;
a();

// 预编译 AO,GO
GO = {
	c:undefined,
  -->3,
  a:function a(){};
}

// a函数的AO
AO = {
	a:undefined,
  --> 1
  b:function b() {}
}

// b函数的AO 
AO = {
	b:undefined;
  --> 2
}
  1. a函数声明被定义

每一个函数的作用域链里面都是包含GO的,每一个函数被定义的时候,它就已经包含全局执行期上下文GO;

a函数被定义时,系统自动生成[[scope]]属性,[[scope]]属性保存着该函数的作用域链[Scope Chain], 该作用域链的第0位储存着当前环境下的全局执行期上下文GO,GO里存储全局下的所有对象,其中包括函数a和变量c

  1. a函数执行与b函数声明被定义

AO是在函数执行的前一刻时形成的,在执行前一刻需要进行预编译;

外部函数执行时,内部的函数被声明,两者的函数作用域链所处的作用域环境是相同。此时a函数执行所处的作用域链与b函数声明被定义的作用域链相同。

当a函数被执行的前一刻时,作用域链顶端的(第0位)储存a函数生成的函数执行期上下文AO,同时第1位储存GO。查找变量是到a函数储存的作用域链中从顶端开始依此向下查找。

  1. b函数执行

当b函数被执行的前一刻时,作用域链顶端的(第0)位储存b函数生成的函数执行期上下文AO,同时第1位储存a函数生成的函数执行期上下文AO,第2位储存GO。查找变量是到b函数储存作用域中从顶端开始依此向下查找。

  1. b函数执行完成

当函数b执行完成后,作用域链顶端的第0位储存b函数生成的函数执行期上下文会与AO函数b的执行期上下文失去引用,将AO函数b的执行期上下文销毁;此时b函数所处的作用域环境相当于b函数声明被定义时的作用域环境,也相当于a函数执行时所处的作用域环境。

  1. a函数执行完成

当a函数执行完毕后,作用域链顶端的第0位储存a函数生成的函数执行期上下文与AO函数的执行期上下文失去引用,然后因为函数a执行期上下文中变量b并且引用着函数b,所以当销毁函数a的执行期上下文时,彻底的将函数b的作用域、作用域链、函数b的执行期上下文销毁。此时a函数所处的作用域环境等于a函数声明被定义时的作用域环境。

3. 函数声明和函数的表达式产生作用域和作用域链的时间

全局执行的前一刻时产生GO,而在预编译的阶段时,函数声明已经声明提升,所以在预编译的阶段时,函数就被定义,就产生作用域与作用域链;

全局执行的时候,函数表达式才会被执行,因为预编译的只会提升变量,不会提升赋值,所以函数表达式是在全局执行的时候才被定义,此时生成作用域与

作用域链;

函数被定义时产生作用域与作用域链,而函数在执行的前一刻时产生GO与AO,进行预编译;然后再执行;

// 函数声明被定义:
[[scope]] --> Scope Chain ---> GO

// 函数被执行:
AO

var fn1 = function() {};
function fn2() {};
Go = {
	fn1: undefined;
  	--> function() {};
  fn2: function fn2() {};
}

4. 为什么外部的函数不能访问内部的环境呢?

因为外部函数的作用域链中不存在内部函数的AO,所以不能够访问到内部环境;

1.3 闭包的初始(重要)

1. 闭包定义

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。
  • 闭包也是一种现象,当内部的函数被返回到外部并保存时,一定会产生闭包,闭包会产生原来的而作用域链不释放,过度的闭包可能导致内存泄漏,或加载过慢。

2. 闭包的作用域与作用域链的产生与销毁

function fn1() {
	function fn2() {
  	var b = 2;
    a = 2;
    console.log(a);
  }
  var a = 1;
  return fn2;
}
var fun = fn1(); // 如果不是闭包的作用,fn1()函数执行后,销毁fn1函数的AO执行期上下文,一并会将内部函数fn2的作用域、作用域链、fn2函数的AO执行上下文彻底销毁,fn2函数没有执行。当fn2被返回出来在全局环境下执行,理论fn2的作用域链第0位是自己的AO,第1位是GO,所以应该打印变量a应该是undefined,但是输出的是2,这是为什么呢?看下面过程
fun();

当fn1函数执行完成后,本应该销毁fn1函数的执行期上下文,fn1函数回到fn1函数声明被定义的时候作用域环境。但是由于fn2函数的作用域链也在引用着fn1的函数执行期上下文,所以导致fn1函数执行期上下文在堆内存中无法销毁,一直保留下来(因为return发挥了闭包的特性,return将整个闭包函数fn2(fn1AO环境+fn2AO环境)返回到全局作用域中,导致fn1的AO环境并没有执行后销毁)。从而在全局环境下调用fun变量相当于调用fn2函数,此时根据fn2函数作用域链的查找顺序,仍然能获取fn1函数的执行期上下文,从而找到a变量 a = 2;

3. 闭包案例

function fn() {
  var n = 100;
  function add() {
    n++;
    console.log(n);
  }
  function reduce() {
  	n--;
    console.log(n);
  }
  return [add, reduce];
}
var arr = fn();
arr[0](); // 101
arr[1](); // 100
arr[0](); // 101
// 面包管理器
function breadMgr(num) {
	// 设置默认值
  var breadNum = arguments[0] || 10;
  function supply() {
  	breadNum += 10;
    console.log(breadNum);
  }
  function sale() {
    breadNum--;
    console.log(breadNum);
  }
  return [supply,sale];
}
var arr = breadMgr(50);
arr[0](); //60
arr[1](); //59
arr[0](); //69
arr[1]();	//68
// 计划管理器
function sunSched() {
	var sunSched = '';
  var operation = {
  	setSched:function(thing) {
    	sunSched = thing;
    },
    showSched:function() {
    	console.log('my schedule on Sunday is ' + sunSched);
    }
  }
  return operation;
}
var obj = sunSched();
obj.setSched('吃饭');
obj.showSched();