一、作用域分类
定义:在js中,作用域是变量,对象,函数可访问的一个范围。
分类:全局作用域,局部作用域,块级作用域
全局作用域:全局代表了整个文档document,变量或者函数在函数外面声明,那它的就是全局变量和全局函数。之所以全局变量在这个文档的任何位置都可以访问是因为它是window下的属性,window是一个全局对象,它本身在页面中任何位置可以用,同样它身上的属性在页面的任何位置也是可以用的。
声明全局作用域的方法:把变量或者是函数放在函数外声明或者变量不用var声明直接赋值(不管是在函数内还是函数外它都是一个全局变量).要避免使用全局变量,声明变量的时候一定要加上var.
<script> var x = 3; //在函数外,全局变量 function fn1() { var c = 10; //函数内,局部变量,它的范围仅限于该函数,函数运行完成后,函数内定义的变量将会自动销毁 c += 3; a=18; console.log(c); //13 x += 10; // 在函数内修改了全局变量后,全局变量就完成了修改,外面调用时,也是修改后的 } fn1(); console.log(x, "______"); //13 "______" b=20; console.log(a);// 18 没有用var声明,虽在函数内声明但也是全局变量 console.log(window); //可以看到a,b,x </script> <script> console.log(x,a); //13 18 在这也可以访问 </script>
全局作用域下带var,声明的一个变量,相当于给window全局对象设置了一个属性,变量的值就是属性值。
console.log(a); //undefined
console.log(window.a);//undefined
console.log('a' in window);//true //在变量提升阶段,在全局作用域中声明了一个变量a此时就已经把a当做属性赋值给window了,只不过此时还没有给a赋值,默认值为undefined //in :检测某个属性是否隶属于这个对象
var a = 12; console.log(a);// 全局变量a 12
console.log(window.a);//window的一个属性名a 12
window.a = 14; //全局变量值修改,window的属性值也跟着修改
console.log(a); //14 //全局变量和window中的属性存在“映射机制”
全局作用域下不带var, 直接赋值,那声明的是window下的属性。不能这样做,有可能会修改window下的属性不安全。
//=> 不加var的本质是window下的属性 // console.log(n);//Uncaught ReferenceError: n is not defined
console.log(window.n);//undefined
console.log('n' in window);//false
n = 12; // <=> window.n = 12
console.log(n);//12
console.log(window.n);//12
局部作用域:变量在函数内声明,变量为局部作用域。只能在函数内部访问。所以不同函数可以使用相同名称的变量。函数执行完后局部变量会自动销毁。函数可以嵌套,嵌套的函数可以访问父函数里的内容。
声明局部作用域的方法:var 变量,function 函数名(){}.
<script> function fn() { var a = 20; var b = 30; function fn1() { console.log(a + b); //50 嵌套函数可以访问父函数里的内容
} fn1(); } fn(); // console.log(a,b); //报错 a,b是局部变量,在外面访问不到 // fn1();//报错 fn1是局部函数,在外面也是问不到的 //全局变量与局部变量重名
var s1 = 10; function fn1() { var s1 = 20; s1 += 20; window.s1 += 10; //如果全局变量的名称在函数中和局部变量相同,想要调用全局变量时要在前面加上window前缀
console.log(s1); //40
} fn1(); console.log(s1); //20 在函数内全局变量进行了改变
var s2 = 10; function fn2() { console.log(s2); //undefined 函数当中有定义局部变量,函数作用范围内所有位置都是这个局部变量, //此函数中下文定义了局部变量s2,但是这里是在定义之前调用的,所以s2的值为undefined
s2 += 10; console.log(s2); //NaN undefined加数字为NaN
var s2 = 20; } fn2(); </script>
什么是作用域链,可以简单的将作用域链理解为变量与函数的查找规则。
查找规则:如是一个函数需要用到一个变量,那它会先在自己的作用域里去找这个变量,如果自己有那它就直接用自己的,如果自己没有,那它就会一层层向外面找,直到找到外层的变量,找到后就用外层的变量(只会向外,不会向内找)。
function fn() { //变量提升无 //console.log(m);//Uncaught ReferenceError: m is not defined
m = 13; console.log(m);//13
console.log('m' in window);//true 在作用域链查找的过程中,如果找到window也没有这个变量,相当于给window设置了一个属性m
} fn(); //console.log(m);//13
var x = [12, 23]; function fn(y) { y[0] = 100; y = [100]; y[1] = 200; console.log(y); fn(x); console.log(x);
在上面这个例子中我们要注意以下几点:
1、创建函数的时候就已经定义了函数的作用域,函数执行的目的是想让存储在堆中的代码字符串执行,如果想让代码字符串执行那就必须有一个执行环境,所以会形成一个私有的上下文
2、形成私有上下文后会进栈操作,把全局上下文放到栈的底部,新形成的上下文放到栈的顶部,执行完后出栈
3、在私有上下文中也有可能创建变量,我们把它叫私有变量,私有变量放在AO中,AO是活动对象,函数中的变量对象都称为AO。AO是VO的一个分支都是变量对象。
4、在代码执行前,要经过几个阶段:初始化作用域链、初始化this指向、初始化实参集合、形参赋值
5、最后才是代码执行
二、变量提升
通过前面的知识我们得知,在当前上下文中,js代码自上而下执行之前,浏览器首先会把当前上下文中所有带“var / function”关键字进行提前的声明和定义,解析到它们对应作用域开始的位置(也可以理解为这是词法解析的一个环节,语法解析发生在代码执行前)这种预先处理的机制叫做变量提升,变量提升的意义在于创建变量前使用这个变量不报错。变量提升也可以称之为预解析。
声明(declare): var a / function sum(默认值为undefined)
定义(defined): a = 12(定义其实就是赋值操作)
在变量提升阶段,带var的只声明未定义,而带function声明和定义都完成了。
变量提升只发生在当前作用域(如:开始加载页面的时候只对全局作用域下的进行提升,因为此时函数中存储的都是字符串而已);浏览器很懒,做过的事情不会重复的执行,也就是当代码执行遇到创建函数这部分代码后,直接跳过(在提升阶段已经完成了函数的赋值操作)。
私有作用域中,带var的在变量提升阶段,声明为私有变量,与外界无关。不带var不是私有变量,会向上级作用域查找,看是否为上级的变量,不是,继续向上查找,它的上级作用域是谁和它在哪里执行无关,和它在哪里创建有关,在哪里创建,它的上级作用域就是谁。
var foo = 1;
function bar() {
if (!foo) {
var foo = 10;
}
console.log(foo);
}
bar();
console.log(g, h); var g = 12, h = 12; function fn() { console.log(g, h); var g = h = 13; console.log(g, h); } fn(); console.log(g, h);
var n = 10; function fn() { var n = 20; function f() { n++; console.log(n); } f(); return f; } var x = fn(); x(); x(); console.log(n);
匿名函数和普通函数的区别:只对等号左边的进行变量的提升。真实项目中建议用函数表达式创建函数,因为这样在变量提升阶段只会声明,不会赋值。最好是把函数表达式匿名函数“具名化”,因为虽然是起了函数有了名字,但是这个名字不能在函数外面进行访问。
/* * 全局上下文中的变量提升 * func=函数 函数在这个阶段赋值都做了 */ func(); function func() { var a = 12; console.log('OK'); } func(); //=>Uncaught TypeError: func is not a function
var func = function () { // 真实项目中建议用函数表达式创建函数,因为这样在变量提升阶段只会声明FUNC,不会赋值
console.log('OK'); }; func(); var func = function AAA() { // 把原本作为值的函数表达式匿名函数“具名化”(虽说是起了名字,但是这个名字不能在外面访问 =>也就是不会在当前当下文中创建这个名字) // 当函数执行,在形成的私有上下文中,会把这个具名化的名字做为私有上下文中的变量(值就是这个函数)来进行处理
console.log('OK'); // console.log(AAA); //=>当前函数 // AAA(); 递归调用 而不用严格模式下都不支持的 arguments.callee 了
}; // AAA(); //=>Uncaught ReferenceError: AAA is not defined
func();
条件判断下的变量提升:在当前作用域下,不管条件是否成立都要进行变量提升。带var的还是只声明,带function的在老版本浏览器渲染机制下,声明和定义都完成,但考虑到es6中的块级作用域,新版本浏览器对于在条件判断中的函数不管条件是否成立只声明不定义。
console.log(i);//undefined
if (1 === 2) { var i = 3; console.log(i); } console.log(i);//undefined
console.log(fn);//undefined
if (1 === 1) { console.log(fn); //函数本身 //当条件成立进入到判断体中(在es6中它是一个块级作用域)第一件事并不是代码执行,而是类似于变量提升一样,先把fn声明和定义了也就是判断体中代码执行之前,fn就已经赋值
function fn() { console.log('ok'); } } fn();//函数本身
基于var或者function在全局上下文中声明的变量(全局变量)会映射到GO(全局对象window)上,作为它的属性,而且一个修改另外一个也会跟着进行修改。
var a = 12; console.log(a); //=>12 全局变量 console.log(window.a); //=>12 映射到GO上的属性a window.a = 13; console.log(a); //=>13 映射机制是一个修改另外一个也会修改
在es6中基于let/const等方式创建的变量或者是函数,不存在变量提升机制,它切断了全局变量和window属性的映射机制。
// console.log(a);//报错
let a = 12; console.log(window.a); //undefined
console.log(a); //12
let b = 12; console.log(b); let b = 13; //Uncaught SyntaxError: Identifier 'b' has already been declared
console.log(b);
现在最新版本要向前兼容es3/5规范, 要注意:1.判断体和函数体等不存在块级上下文,上下文只有全局和私有 2.不论条件是否成立,带function的都要声明和定义。
向后兼容es6规范,要注意:1.存在块级作用域,大括号中出现let/const/function...都会被认为是块级作用域 2.不论是否成立,带function的只提前声明,不会提前赋值。
var a = 0; if (true) { a = 1; function a() {} a = 21; console.log(a); } console.log(a);