你不知道的Javascript(上卷)-作用域笔记

作用域是什么

1.理解作用域

首先看一段声明代码

var a = 2;

在此声明变量并赋值的操作中会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

对于第一个操作中,引擎为变量a进行LHS查询。另外一个查找的类型叫作RHS查询。当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。

如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。RHS查询与简单地查找某个变量的值别无二致,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值。
 

2.作用域嵌套

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套

因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域)为止。

function foo(a){
     
	console.log(a + b);
}
var b = 2;
foo(2); //4

比如在上面这个例子中,对b进行的RHS引用无法在函数foo内部完成,但可以在上一级作用域(在这个例子中就是全局作用域)中完成。

遍历嵌套作用域链的规则很简单:引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。
 

3.异常

在变量未声明的情况下,LHS查询RHS查询的行为是有所不同的。

function foo(a) {
     
            console.log(a + b);
            b = a;
        }

        foo(2);

在上面这段代码中,第一次对b进行RHS查询是无法找到该变量的,此变量未声明,因为在所有作用域中都无法找到它。

RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。值得注意的是,ReferenceError是非常重要的异常类型。

LHS查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非“严格模式”下。
 

4.词法作用域

词法作用域就是定义在词法阶段的作用域。

换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)。

查找

在查找的时候,作用域查找会在找到第一个匹配的标识符时停止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。

欺骗词法

使用eval()with() 可以修改词法作用域,尽量少使用这两个函数,因为此时性能会变差

eval可以对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法作用域(在运行时),函数如果接受了含有一个或多个声明的代码,就会修改其所处的词法作用域,而with声明实际上是根据你传递给它的对象 ,将对象的属性当做作用域中的标识符来处理,从而凭空创建了一个全新的词法作用域

eval(…)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。

function foo(str,a){
     
	eval(str); // 欺骗
	console.log(a,b);
}

var b = 2;
foo("var = b;",1);

在上面这段代码中,eval(…)调用中的"var b = 3; "这段代码会被当作本来就在那里一样来处理。由于那段代码声明了一个新的变量b,因此它对已经存在的foo(…)的词法作用域进行了修改。事实上,和前面提到的原理一样,这段代码实际上在foo(…)内部创建了一个变量b,并遮蔽了外部(全局)作用域中的同名变量。
 
with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。

function foo(obj){
     
	with(obj){
     
		a = 2;
	}
}

var o1 = {
     
	a:3;
}

var o2 = {
     
	b:2;
}

foo(o1);
foo(o2);
console.log(o1);//2
console.log(o2);//undefined
console.log(a); //2 a被泄露到全局作用域中

对于上面的代码,foo()函数接受了一个obj参数,此参数是一个对象引用,并对这个对象引用执行了with(obj){…}.首先我们将o1传递进去,a = 2的赋值操作找到了o1.a并赋值给它。

对于o2来说,它并没有a属性,因此不会创建这个属性而保持undefined。

在最后一句代码中,o2的作用域、foo(…)的作用域和全局作用域中都没有找到标识符a,因此当a=2执行时,自动创建了一个全局变量(因为是非严格模式)。

 

5.函数作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

这种设计方案是非常有用的,能充分利用JavaScript变量可以根据需要改变值类型的“动态”特性。

函数声明和表达式
如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
var a = 2;
(function foo(){
     
	var a = 3;
	console.log(3);
})();
console.log(2);

比如上述函数的声明是以(function… 而不是function来开始,因此它是函数表达式。

(function foo(){ … })作为函数表达式意味着foo只能在..所代表的位置中被访问,外部作用域则不行。foo变量名被隐藏在自身中意味着不会非必要地污染外部作用域。同时foo也是一个立即执行函数表达式(IIFE).。

 

6.块作用域

for(var i = 0; i < 10; i++){
     
	console.log(i);
}

在进行for循环中,不应该把变量i污染到整个函数作用域中,如果在错误的地方使用变量将导致未知变量的异常。变量i的块作用域(如果存在的话)将使得其只能在for循环内部使用,如果在函数中其他地方使用会导致错误。这对保证变量不会被混乱地复用及提升代码的可维护性都有很大帮助。

let关键字
let关键字可以将变量绑定到所在的任意作用域中(通常是{ .. }内部)。
换句话说,let为其声明的变量隐式地劫持了所在的块作用域。
{
     
	console.log(bar); //ReferenceEroor
	let bar = 2;
}

使用let进行的声明不会在块作用域中进行提升。声明的代码被运行之前,声明并不“存在”。

 

6.提升

先有声明,后有赋值

只有声明本身会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变了代码执行的顺序,会造成非常严重的破坏。

foo();

function foo(){
     
	console.log(a);
	var a = 2;
}

由于函数提升,变成了下面的样子

function foo(){
     
	var a;
	console.log(a);//undefined
	a = 2;
}
函数声明会被提升,而函数表达式不会提升
foo();// TypeError

var foo = function bar(){
     ...};

foo时声明提升后并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值)。foo()由于对undefined值进行函数调用而导致非法操作,因此抛出TypeError异常。

函数会首先被提升,然后才是变量。
foo();//1

var foo;

function foo(){
     
	console.log(1);
}

foo = function(){
     
	console.log(2);
}

声明提升后

function foo(){
     
	console.log(1);
}
foo();//1

foo = function(){
     
	console.log(2);
}

var foo尽管出现在function foo()….的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。但是尽管重复的var声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。

你可能感兴趣的:(javascript,前端)