一、理解js作用域
1、作用域:作用域是一套规则,用于确定在何处以及如何查找变量(标字符)。
2、LHS查询:查找的目的是对变量进行赋值;
3、RHS查询:查找的目的是获取变量的值;
(赋值操作符会导致LHS查询、=操作符获调用函数是传入参数的操作会都会导致关联作用域的赋值操作)
eg、
function foo(a){
var b=a;
return a+b;
}
var c=foo(2);
//LHS查询(3处)c=...、a=2(调用函数传参的隐式变量分配)、b=..
//RHS查询(4处)foo(2..)、=a、a..、...b
4、JavaScript引擎会先在代码执行前对其进行编译,如var a=2分为两个步骤:
(1)、首先,var a在其作用域中声明新的变量a、这会在最开始的阶段,也就是代码执行前进行(声明提前);
(2)、接下来,a=2会查询(LHS查询)变量a并对其进行赋值。
LHS和RHS查询都会在先在当前执行作用域中开始,如果在当前作用域中没有找到所需标识符,就会向上一级继续查找直到顶层全局作用域;
**不成功的RHS引用会抛出referenceError异常;不成功的LHS引用会导致自动隐式的创建一个全局变量(非严格模式下) **
二、词法作用域
- 定义
。定义在词法阶段的作用域,由你写代码是将变量和块作用域写在哪里所决定 - 查找
。作用域始终从运行时所处的最内部作用域开始查找,逐级向外部进行,直到遇见第一个匹配的标识符为止; - 词法欺骗
。在词法阶段通过代码欺骗和假装成书写,来实现修改词法作用域;
。欺骗词法作用域会导致性能下降,应尽量避免使用;
。js中会“欺骗”词法作用域的两种机制:
(1)eval(..)
*可接收一字符串参数,并将其内容视为在书写时就存在于程序中的这个位置;
eg:
function foo(str,a) {
eval(str);//欺骗
console.log(a,b);
// body...
}
var b=2;
foo("var b=3;",1);//1 3
*在严格模式的程序中,eval()在运行时有其自己的词法作用域,即其中的声明无法修改所在的作用域;
eg:
function foo(str,a) {
"use strict";
eval(str);//欺骗
console.log(a,b);
// body...
}
function foo1(str) {
"use strict";
eval(str);//欺骗
console.log(a);//ReferenceError: a is not defined
// body...
}
foo1("var a=3;");
(2)with
。重复引用同一对象中多个属性的快捷方式;
。with可以将一个没有或有多个属性的对象处理为为一个完全隔离的词法作用域,因此这个对象的属性也会被处理为定义在这个作用域中的词法标识符。
eg:
function foo2(obj){
with(obj){
a=2; //实际是一个LHS引用
}
}
var o1={
a:3
};
var o2={
b:3
};
foo2(o1);
console.log(o1.a);//2;o1传递进去,a=2赋值操作找到o1.a并将2赋值给它
foo2(o2);
console.log(o2.a);//undefined:o2传递进去,o2没用a属性,因此不会创建这个属性
console.log(a)//2----a被泄露到全局作用域
。o2、foo(..)、和全局的作用域中都没有找到标识符a,因此当a=2执行时会自动创建一个全局变量(非严格模式)
。尽管with块可以将一个对象处理为词法作用域,但这个块内部的正常var声明不会被限制在这个块中,而是被添加到with所处的函数作用域中。
eg:
function foo3(obj){
with(obj){
var a=2; //实际是一个LHS引用
}
}
var o1={
a:3
};
var o2={
b:3
};
foo3(o1);
console.log(o1.a);//2;o1传递进去,a=2赋值操作找到o1.a并将2赋值给它
foo3(o2);
console.log(o2.a);//undefined:o2传递进去,o2没用a属性,因此不会创建这个属性
console.log(a)//ReferenceError: a is not defined
三、函数作用域
- 含义:
。指的是属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在 嵌套的作用域中也可以使用) - 函数声明和函数表达式:
。区分:看function关键字出现在声明中的位置;如果是声明中的第一词,那么就是一个函数声明,否则就是一个函数表达式. - IIEF立即执行函数表达式
eg:
var a=2;
(function IIEF(global){
var a=3;
console.log(a);//3
console.log(global.a);//2
})(window);
console.log(a);//2
1)函数被包含在()中,因此成了一个表达式;通过后面的()可以立即执行这个函数;
2)通过后面的()可传入参数,例中传入window对象并命名为global。
。IIEF还有一种变化用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在IIEH执行之后当做参数传递进去
eg:
var a = 2;
(function IIFE( def ) {
def(window);
})(function def( global ) {
var a = 3;
console.log( a ); // 3
console.log( global.a); //2
});
四、块作用域
- 在JavaScript中只有函数作用域,没有块级作用域。
for(var i = 0; i < 10; i++) {}console.log( i ); // 10
。在for循环的头部定义了变量i,通常只是想在for循环内部的上下文使用i,而忽略了i会被绑定到外部作用域(函数或全局)。
var foo = true;if (foo) { var bar = foo * 2; }console.log( bar ); // 2
。bar变量虽然在if声明中的上下文使用,但它们最终都属于外部作用域。
- ** with**
。with也是块级作用域的一种形式,用with从对象中创建出的作用域仅在with声明中有效。 - ** try/catch**
。ES3规范的try/catch的catch分句会创建一个块级作用域,其中声明的变量只在catch内部有效。
try {
foo();
}
catch(err) {
var a = 0; console.log( err ); //可以执行
}
console.log( a ); // 0;
console.log( err ); // err not found
- let
。ES6的let可以将变量绑定到所在的任意作用域(通常是{...})。
var foo = true;
if (foo) {
let bar = foo * 2;
console.log( bar ); // 2
}
console.log( bar ); //referenceError
。**使用let进行的声明不会在块级作用域中提升。声明的代码被运行前,声明并不“存在”。**
console.log( bar ); //ReferenceError
let bar = 2;
- ** const**
。 ES6引入const同样能创建块级作用域,但其值是常量。
var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在if中的块级作用域常量
a = 3;
b = 4; // 错误
}
console.log( a ); // 3
console.log( b ); // ReferenceError
五、提升(声明提前)
eg:
foo();
function foo(){
a=2;
console.log(a);//2
var a;
}
。定义声明如var a;是在编译阶段进行的;赋值声明会留在原地等待执行阶段;
。简单讲,即包含变量和函数在内的所有声明都会在任何代码被执行之前首先被处理.
。注意:函数声明会被提升,当是函数表达式不会被提升。(下例中,var ber被提升,但函数表达式..=function foo(){}并不会提升,故ber()抛出TypeError异常而不是ReferenceError)
eg:
ber(); //TypeError: ber is not a function
var ber=function foo(){
a=2;
console.log(a);
var a;
}
- 函数优先
。函数声明和变量声明都会被提升,但是函数会首先被提升,然后才是变量
eg:
foo1();//a
var foo1;
function foo1(){
console.log("a");
};
foo1=function(){
console.log("b")
};
foo1()//b
。以上代码被引擎理解为:
function foo1(){
console.log("a");
}
foo1();//a
foo1=function(){
console.log("b")
}
foo1()//b