第六节 Javascript作用域与作用域链
一. 作用域
- 理解全部变量和局部变量
一个变量如果定义在了一个function里面,那么这个变量就是一个局部变量,只在这个function里面有定义。出了这个function,就如同没有定义过一样。
function fn(){
var a = 1; //定义在一个函数里面的变量,局部变量,只有在函数里面有定义
console.log("我是函数里面的语句,我认识a值为" + a);
}
fn();
console.log("我是函数外面的语句,我不认识a" + a);
a被var在了function里面,所以现在这个a变量只在函数内定义
JavaScript变量作用域非常的简单,在ES5语法中因为没有块级作用域,因此能封闭作用域的只有一个东西:函数。
如果一个变量,没有定义在任何的function中,那么它将在全部程序范围内都可以使用, 这个变量就被称为全局变量,
如果一个变量,定义在一个函数中,那么这个变量只在这个函数封闭的范围内可以使用, 这个变量就是局部变量
var a = 1; //定义在全局范围内的一个变量,全局变量,在程序任何一个角落都有定义
function fn(){
console.log("我是函数里面的语句,我认识全局变量a值为" + a);
}
fn();
console.log("函数外面的语句也认识a值为" + a)
总结:
- 定义在function里面的变量,叫做局部变量,只在function里面有定义,出了function没有定义的。
- 定义在全局范围内的,没写在任何function里面的,叫做全局变量,全局都认识。
- 作用域
2.1全局作用域
所有在script标签里面的代码,都处在全局作用域中
全局作用域在页面打开时创建全局对象GO(window对象),页面关闭时销毁GO对象
全局作用域中的变量是GO对象的属性名,变量的值是GO对象的属性值
2.2 函数作用域
所有在函数里面的代码,都处在函数作用域中
函数作用域在函数执行时创建AO对象,在函数结束时销毁AO对象
函数作用域中的变量是AO对象的属性名,变量的值是AO对象的属性值
当下一次执行函数时,会创建全新的A0对象
- Js解析引擎执行js代码的步骤
3.1 语法分析
Js解释引擎会先扫描所有的js代码,查看代码有没有低级的语法错误,如果存在语法错误,则整个程序就不会执行,如果没有语法错误,则进入预解析(编译)阶段
报错信息:Uncaught SyntaxError:Invalid or unexpected token表示语法错误
3.2 预编译四部曲
- 创建AO对象 ==> Activation Object(活动对象,作用域,其实叫执行期上下文)
全局对象GO - 找到形参和变量,把形参和变量作为AO对象的属性名,值是undefined
- 实参把值赋给形参
- 在函数中找到函数声明,把函数名作为AO对象的属性名,值是函数体
3.3 执行js代码
function fn(a){
console.log(a);
var a = 123;
console.log(a);
function a (){}
console.log(a);
console.log(b);
var b = function (){}
console.log(b);
function b(){}
}
fn(1)
// 预编译发生在函数执行的前一刻
function test(a,b){
console.log(a);
c = 0;
var c;
a = 3;
b = 2;
console.log(b);
function b(){}
function d(){}
console.log(b)
}
test(1)
global = 100;
function fun(){
console.log(global);
global = 200;
console.log(global)
var global = 300;
console.log(global);
}
fun()
var global
function haha(){
console.log(b)
if(a){
var b = 100;
}
console.log(b);
c = 234;
console.log(c)
}
var a;
haha();
a = 10;
console.log(c);
haha();
执行期上下文:
当函数在执行前一刻,会创建一个执行期上下文的内部对象,一个执行期上下文定义了函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕后,它所产生的执行期上下文被销毁
- 其他注意事项
4.1. 不写var的变量就自动成全局变量了
也就是说未经声明的变量被赋值了,就归全局所有
function fn(){
a = 1; //这个a第一次赋值的时候,并没有var过,
//所以就自动的在全局的范围帮你var了一次
}
fn();
console.log(a);
这是JS的一个机理,如果遇见了一个标识符,从来没有var过,并且还赋值了:
name = 123;
那么JS会自动帮你在全局范围内帮你定义var name;
这个实例告诉我们, 没有特殊情况下, 所有的变量都尽量乖乖的声明定义
a = 100;
console.log(a);
console.log(window.a)
4.2. 声明的全局变量,都归window所有
var a = 100;
console.log(a); // ==> 访问的就是window.a
console.log(window.a)
window 就像我们最大的仓库
function text(){
var a = b = 123;
}
text();
// 这里b是没有声明的 会先把值123赋值给b 在赋值给a
console.log(window.b)
4.3.函数的参数,会默认定义为这个函数的局部变量
function fn(a,b,c,d){
}
a,b,c,d就是一个fn内部的局部变量,出了fn就没有定义。
5.全局变量的作用
5.1 通信,共同操作同一个变量
两个函数同时操作同一个变量,一个增加,一个减少,函数和函数通信。
var num = 0;
function add(){
num++;
}
function remove(){
num--;
}
add();
add();
add();
add();
remove();
remove();
add();
add();
console.log(num);
5.2 累加,重复调用函数的时候,不会重置
var num = 0;
function baoshu(){
num++;
console.log(num);
}
baoshu(); //1
baoshu(); //2
baoshu(); //3
如果num定义在baoshu里面,每次执行函数就会把num重置为0:
function baoshu(){
var num = 0;
num++;
console.log(num);
}
baoshu(); //1
baoshu(); //1
baoshu(); //1
6.函数内部也可以定义函数作用于
//这个函数返回a的平方加b的平方
function pingfanghe(a,b){
return pingfang(a) + pingfang(b);
//返回m的平方
function pingfang(m){
return Math.pow(m,2)
}
}
// 现在相求4的平方,想输出16
pingfang(4); //报错,因为全局作用域下,没有一个函数叫做pingfang
机理:
function big{
function small{
}
small(); //可以运行
}
small();
//不能运行,因为小函数定义在了大函数里面,离开大函数没有作用域。
二. 作用域链
- 变量查询规则
当遇见一个变量时,JS引擎会从其所在的作用域依次向外层查找,查找会在找到第一个匹配的标识符的时候停止.
//变量的作用域,就是它var的时候最内层的function
function outer(){
var a = 1; //a的作用域就是outer
inner();
function inner(){
var b = 2; //b的作用域就是inner
console.log(a); //能够正常输出1,a在本层没有定义,就是找上层
console.log(b); //能够正常输出2
}
}
outer();
console.log(a); //报错,因为a的作用域outer
多层嵌套,如果有同名的变量,那么就会发生“遮蔽效应”:
var a = 1; //全局变量
function fn(){
var a = 5; //就把外层的a给遮蔽了,这函数内部看不见外层的a了。
console.log(a); //输出5,变量在当前作用域寻找,找到了a的定义值为5
}
fn();
console.log(a); //输出1,变量在当前作用域寻找,找到了a的定义值为1
作用域链:一个变量在使用的时候得几呢?就会在当前层去寻找它的定义,找不到,找上一层function,直到找到全局变量,如果全局也没有,就报错。
//比较复杂的题目
var a = 1; //全局变量
var b = 2; //全局变量
function outer(){
var a = 3; //遮蔽了外层的a,a局部变量
function inner(){
var b = 4; //遮蔽了外层的b,b局部变量
console.log(a); //① 输出3,a现在在当前层找不到定义的,所以就上一层寻找
console.log(b); //② 输出4
}
inner(); //调用函数
console.log(a); //③ 输出3
console.log(b); //④ 输出2 b现在在当前层找不到定义的,所以就上一层寻找
}
outer(); //执行函数,控制权交给了outer
console.log(a); // ⑤ 输出1
console.log(b); // ⑥ 输出2
函数是一个对象,其中有些属性我们可以访问,比如name,length,arguments.但有些不可以,这些属性仅供Js解释引擎存取,[[scope]]就是其中一个。[[scope]]这个属性指的就是常说的作用域,其中存储了AO对象的集合。
- 作用域链原理理解:
[[scope]]中所存储的就是执行期上下文对象的集合,这个几个呈链式连接,我们把这种链式链接叫做作用域链
当函数执行时会生成AO对象,并且把这个AO对象放在[[scope]],翻译过来就是范围的意思.的最顶端,和函数创建时的环境,形成链式结构,我们把这种链式结构叫做作用域链。
作用域链 = 函数执行时的AO对象 + 函数创建时的环境
变量查找规则:沿着当前函数作用域链作用域链顶端,自上而下寻找变量
例子
function outer(){
function inner(){
var a = 111;
c = 222;
console.log(a);
console.log(b);
console.log(c);
}
var b = 333;
inner();
}
var a = 444;
outer();
在查找变量a b c时,从作用域链顶端从上到下开始寻找对应的变量,分别找到的是
inner函数AO中的a,
outer函数AO中的b,
GO中的c
function aa(){
function bb(){
var b = 666;
console.log(a);
}
bb()
var a = 10
console.log(cc)
}
var cc = 888;
aa();
改变一下代码
function aa(){
function bb(){
var b = 666;
console.log(a);
}
var a = 250;
return bb;
}
var c = 888;
var dd = aa();
dd();
三. 闭包
实例:
function test(){
var arr = [];
for(var i = 0; i< 10; i++){
arr[i] = function(){
console.log(i)
}
}
return arr;
}
var myArr = test();
-
闭包理解
//非常经典的闭包:
function outer(){
var a = 333;
function inner(){
console.log(a);
}
return inner;
}var inn = outer();
inn(); //弹出333
推导过程:
我们之前已经学习过,inner()这个函数不能在outer外面调用,因为outer外面没有inner的定义:
function outer(){
var a = 888;
function inner(){
console.log(a);
}
}
//在全局调用inner但是全局没有inner的定义,所以报错
inner();
但是我们现在就想在全局作用域下,运行outer内部的inner,此时我们必须想一些奇奇怪怪的方法。
有一个简单可行的办法,就是让outer自己return掉inner:
function outer(){
var a = 100;
function inner(){
a ++;
console.log(a);
}
return inner; //outer返回了inner的引用
}
var inn = outer(); //inn就是inner函数了
inn(); //执行inn,全局作用域下没有a的定义
//但是函数闭包,能够把定义函数的时候的作用域一起记忆住
//能够输出101
inn(); // 102
这就说明了,inner函数能够持久保存自己定义时的所处环境,并且即使自己在其他的环境被调用的时候,依然可以访问自己定义时所处环境的值。
一个函数可以把它自己内部的语句,和自己声明时所处的作用域一起封装成了一个密闭环境,我们称为“闭包” (Closures)。
每个函数都是闭包,每个函数天生都能够记忆自己定义时所处的作用域环境。但是,我们必须将这个函数,挪到别的作用域,才能更好的观察闭包。这样才能实验它有没有把作用域给“记住”。
我们发现,把一个函数从它定义的那个作用域,挪走,运行。嘿,这个函数居然能够记忆住定义时的那个作用域。不管函数走到哪里,定义时的作用域就带到了哪里。这就是闭包。
注意:
闭包在工作中是一个用来防止产生隐患的事情,而有的时候又需要根据其性质加以利用。所以我们需要理解闭包形成的原理和了解其特性
闭包的变种
var inner;
function outer(){
var aa = 200;
inner = function(){
console.log(aa);
}
}
outer(); //outer 必须先运行,否则,inner不是一个函数
var aa = 300;
inner(); // 因为闭包的原因闭包,打印出200
function outer(x){
function inner(y){
console.log(x + y)
}
return inner;
}
// 拿到inner函数,inn就是函数inner函数
var inn = outer(5);
inn(6); // 11 只要inn一运行,inner就记住x是5
inn(4); // 5 与4 和
2.闭包的全新特性
每次重新引用函数的时候,闭包是全新的。
function outer(){
var count = 0;
function inner(){
count++;
console.log(count);
}
return inner;
}
var inn1 = outer();
var inn2 = outer();
inn1(); //1
inn1(); //2
inn1(); //3
inn1(); //4
inn2(); //1
inn2(); //2
inn1(); //5
var inn3 = outer();
inn3(); //1
inn3(); //2
inn1(); //6
// 这个时候我们是不是有封装的感觉,因为此时你对count无能为力
函数定义一次,可以进行多次调用,每次调用一个函数,都会产生新的闭包.新的闭包知道是,语句全新,所处环境也是全新的
无论它在何处被调用,它总是能访问它定义时所处作用域中的全部变量
示例2
function outer(){
var a = 3;
function inner(){
return a++;
}
return inner;
}
var a = outer();
console.log(a()); //3
console.log(a()); //4
console.log(a()); //5
在全局作用域下,试图不通过调用a(),而直接修改a,是不可行的/
函数的闭包,记住了定义是所有的作用域,这个作用域的变量不是一成不变的.
看一个例子
function fun1(x,y){
function fun2(x){
console.log(x + y)
}
return fun2;
}
var f = fun1(3,5);
f(8); // 13 8替换掉了3
每一函数都是一个闭包,无论它在哪里调用,它总是能访问到定义时所处作用域中的全部变量
闭包是天生存在,并不需要什么特殊的结构才存在,只不过我们需要刻意的把函数放到其他的作用域中调用才能明显的观察到闭包的性质.
闭包会导致原有作用域链不释放,造成内存泄漏(占用的多,然后变得少)
- 闭包的作用;
3.1 实现共有变量
函数累加器
function outer(){
var count = 0;
function inner(){
count++;
console.log(count);
}
return inner;
}
var inn1 = outer();
inn1(); //1
inn1(); //2
3.2 可以做缓存(存储结构)
function test(){
var num = 100;
function aa(){
num ++;
console.log(num);
}
function bb(){
num --;
console.log(num)
}
return [aa,bb]
}
var myArr = test()
myArr[0]()
myArr[1]()