作用域与作用域链

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);
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值