javascript拥有一套设计良好的规则来存储变量,并且之后可以方便的找到这些变量,这套规则叫做作用域。
内部原理
内部原理分成编译、执行、查询、嵌套和异常五部分。今天来简单说一下查询。
var a=2;
这行代码中,在引擎执行的第一步操作中,对变量a进行了查询,这种查询叫做LHS查询。实际上,引擎查询共分为两种:LHS查询和RHS查询。
从字面意思去理解,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。
更准确的是,RHS查询与简单的查找某个变量的值没什么区别,而LHS查询则是试图找到变量的容器本身,从而可以对其赋值。
function foo(){
console.log(a);//2
}
foo(2);
这段代码中,总共包括4个查询,分别是:
1.foo(…)对foo进行了RHS引用
2.函数传参a=2对a进行了LHS引用
3.console.log(…)对console对象进行了RHS引用,并检查其是否有一个log的方法。
4.console.log(a)对a进行了RHS引用,并把得到的值传给了console.log(…)
问题来了,为什么要区别LHS和RHS呢?
因为在变量还没有声明的时候,这两种查询的行为不一样:
RHS
【1】如果RHS查询失败,引擎会抛出ReferenceError(引用错误)异常。
//对b进行RHS查询时,无法找到该变量。也就是说,这是一个“未声明”的变量
function foo(a){
a = b;
}
foo();//ReferenceError: b is not defined
【2】如果RHS查询找到一个变量,但尝试对变量的值进行不合理操作,比如对一个非函数类型值进行函数调用,或者引用null或undefined中的属性,引擎会抛出另一种类型异常:TypeError(类型错误)异常。
function foo(){
var b = 0;
b();
}
foo();//TypeError: b is not a function
LHS
【1】当引擎执行LHS查询时,如果无法找到变量,全局作用域会创建一个具有该名称的变量,并将其返还给引擎。
function foo(){
a = 1;
}
foo();
console.log(a);//1
【2】如果在严格模式中LHS查询失败时,并不会创建并返回一个全局变量,引擎会抛出同RHS查询失败时类似的ReferenceError异常
function foo(){
'use strict';
a = 1;
}
foo();
console.log(a);//ReferenceError: a is not defined
词法作用域和动态作用域
词法作用域
就是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里决定的。无论函数在哪里被调用,也无论他如何被调用,他的词法作用域都只由函数被声明时所处的位置决定。
function foo(a) {
var b = a * 2;
function bar(c) {
console.log( a, b, c );
}
bar(b * 3);
}
foo( 2 ); // 2 4 12
在这个例子中,有三个逐级嵌套的作用域。为了帮助理解,可以将它们想象成几个逐级包含的气泡。
作用域气泡由其对应的作用域块代码写在哪里决定,它们是逐级包含的
气泡1包含着整个全局作用域,其中只有一个标识符:foo
气泡2包含着foo所创建的作用域,其中有三个标识符:a、bar和b
气泡3包含着bar所创建的作用域,其中只有一个标识符:c
在查找的过程中,引擎首先会从最内部的作用域来查找,如果没有找到,引擎会到上一级所嵌套的作用域中去继续查找。
【注意】词法作用域查找只会查找一级标识符,如果代码引用了foo.bar.baz,词法作用域查找只会试图查找foo标识符,找到这个变量后,对象属性访问规则分别接管对bar和baz属性的访问。
foo = {
bar:{
baz: 1
}
};
console.log(foo.bar.baz);//1
作用域查找从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止。在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”,内部的标识符“遮蔽”了外部的标识符。
var a = 0;
function test(){
var a = 1;
console.log(a);//1
}
test();
全局变量会自动为全局对象的属性,可以通过对全局对象属性的引用来对其进行访问。
var a = 0;
function test(){
var a = 1;
console.log(window.a);//0
}
test();
【注意】:但是如果是非全局的变量被遮蔽了,无论如何也无法被访问到了。
动态作用域
javascript使用的是词法作用域,它最重要的特征就是它的定义过程发生在代码的书写阶段。
但是呢,动态作用域不太一样,他关心它们什么时候被调用,在何处被调用。
声明提升
包含变量和函数在内的所有声明都会在任何代码被执行前被处理
变量声明提升
var a=2;
这个代码片段实际上包括两个操作:var a和a=2
第一个定义声明是在编译阶段进行的。第二个赋值操作会被留在原地等待引擎在执行阶段执行。
//对变量a的声明提升到最上面后,再执行代码时,控制台输出2
var a;
a = 2 ;
console.log(a);
函数声明提升
foo();
function foo(){
console.log(1);//1
}
上面代码片段之所以能够在控制台输出1,就是因为foo()函数声明进行了提升,如下:
function foo(){
console.log(1);
}
foo();
【注意】函数声明会提升,但是函数表达式不会提升
foo();
var foo = function(){
console.log(1);//TypeError: foo is not a function
}
提升后是:
//变量提升后,代码如下所示:
var foo;
foo();
foo = function(){
console.log(1);
}
函数覆盖
函数声明和变量声明都会被提升。但是,函数声明会覆盖变量声明。
var a;
function a(){}
console.log(a);//'function a(){}'
但是!!!如果变量存在赋值操作,那么最终的值为变量的值!!!
var a=1;
function a(){}
console.log(a);//1
var a;
function a(){};
console.log(a);//'function a(){}'
a = 1;
console.log(a);//1
块作用域
随着ES6的推广,块作用域用的越来越广泛。
ES6引入了let和const关键字,和var关键字不同,在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量。
{
// 块级作用域中的变量
let greeting = 'Hello World!';
var lang = 'English';
console.log(greeting); // Prints 'Hello World!'
}
// 变量 'English'
console.log(lang);
// 报错:Uncaught ReferenceError: greeting is not defined
console.log(greeting);