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](作用域链)属性
- 函数创建时(函数被定义时),生成的一个JS内部的隐式属性[[scope]] ;
- [[scope]] 属性是函数存储作用域链的容器
- [scope Chain]作用域链是存储函数的AO/GO的容器
- AO:函数执行期上下文 GO:全局的执行期上下文,AO是一个即时的存储容器
- 函数执行完成之后,AO是要销毁的,函数重新执行,会重新生成一个新的AO,老的AO会被销毁;
- 函数不调用就不会执行,如果不执行,那么内部的其他函数声明也不会被定义;
作用域链:把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
}
- a函数声明被定义
每一个函数的作用域链里面都是包含GO的,每一个函数被定义的时候,它就已经包含全局执行期上下文GO;
在a函数被定义时,系统自动生成[[scope]]属性,[[scope]]属性保存着该函数的作用域链[Scope Chain], 该作用域链的第0位储存着当前环境下的全局执行期上下文GO,GO里存储全局下的所有对象,其中包括函数a和变量c
- a函数执行与b函数声明被定义
AO是在函数执行的前一刻时形成的,在执行前一刻需要进行预编译;
外部函数执行时,内部的函数被声明,两者的函数作用域链所处的作用域环境是相同。此时a函数执行所处的作用域链与b函数声明被定义的作用域链相同。
当a函数被执行的前一刻时,作用域链顶端的(第0位)储存a函数生成的函数执行期上下文AO,同时第1位储存GO。查找变量是到a函数储存的作用域链中从顶端开始依此向下查找。
- b函数执行
当b函数被执行的前一刻时,作用域链顶端的(第0)位储存b函数生成的函数执行期上下文AO,同时第1位储存a函数生成的函数执行期上下文AO,第2位储存GO。查找变量是到b函数储存作用域中从顶端开始依此向下查找。
- b函数执行完成
当函数b执行完成后,作用域链顶端的第0位储存b函数生成的函数执行期上下文会与AO函数b的执行期上下文失去引用,将AO函数b的执行期上下文销毁;此时b函数所处的作用域环境相当于b函数声明被定义时的作用域环境,也相当于a函数执行时所处的作用域环境。
- 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();