【JavaScript】有趣的作用域和提升

前言

最近复习到了作用域这块的内容,打算归纳总结一下,加入自己的理解和尝试,更好的理解作用域和提升相关的知识点。

了解一下作用域

1. 什么是作用域

按照我的理解,在程序的运行中,需要获取和存储变量的值,并且在某些情况下需要获取这些值包括状态,那么这些值和状态的集合就可以称之为作用域。

2.作用域有哪些

从行为上分,作用域其实可以分为词法作用域动态作用域,我们假设JS分别使用两种作用域的表现有什么不同:

  • 词法作用域
var a = 3;
function test() {
    console.log(a); //从这里开始查找
}
function test2() {
    var a = 1;
    test();
}
test2(); //3

在词法作用域下,作用域的范围是静态的,由作用域声明的地方来决定。

这里test运行时,会以test函数声明的地方为起点,扩散寻找a变量,因此找到了a的值为3,关于作用域的查找规则我们下面会说到。

  • 动态作用域
var a = 3;
function test() {
    console.log(a);
}
function test2() {
    var a = 1;
    test(); //从这里开始查找
}
test2(); //1

在动态作用域下,作用域的范围是动态的,由具体调用的位置来决定。

在动态作用域下,会以test执行时的位置开始查找,和JS中的this规则非常接近。

讲了两种作用域的区别,那么JS属于哪种作用域?好叭好叭,机智的大家都知道了,JavaScript采用的就是词法作用域,我们下面详细讲解JavaScript中的作用域。

JavaScript中的作用域

上文我们讲到,js采用的是词法作用域,那么具体又分成哪几种类型呢?

我们可以分为这几种:全局作用域函数作用域块级作用域(ES6)

1. 全局作用域

全局作用域在创建时就会生成,关闭时则会销毁,属于作用域的最外层或者说最顶层。直接编写在js文件或者script标签中的代码都属于全局作用域,和window对象在同一层。

全局作用域还有以下特点:

  • 在全局作用域中声明变量,该变量会自动成为window对象的属性
  • 在全局作用域中声明函数,该函数会自动成为window对象的方法

举个简单的例子:

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

window.a;   //1
window.test();  //2

2. 函数作用域

在JavaScript中声明一个函数,会创建一个属于函数本身的作用域集合。在函数中声明的变量,无法从外部访问到,而当函数执行结束以后,这个作用域集合会被释放掉。

举个简单的例子:

function test(){
    var a=3;
}
console.log(a); //Uncaught ReferenceError: a is not defined

3. 块级作用域

很多编程语言都支持块级作用域的概念,这也意味着在使用iffor时会创建出独立的作用域,但JavaScript在ES6之前的语法是没有块级作用域的概念的,这就会导致这样的情况:

var a=2;
if(a){
    var b=a;
}
b //2

if语句中声明的b变量,即使在语句之外也可以访问到。但在ES6之后有了let关键字,就变成了这样:

var a=2;
if(a){
    let b=a;  //var变成了let
}
b //Uncaught ReferenceError: b is not defined

可以看到,使用了let关键字后,if语句也有了和函数一样的独立作用域。

作用域的查找规则

我们刚刚在讲述不同的作用域时,可能会有些疑惑:

  • 作用域的外部和内部是什么意思?
  • 为什么外部的作用域没法访问内部的作用域?
  • 内部为什么又能拿到外部的变量?

这里就要讲到作用域的查找规则了,我们废话不多说,先请出我们的示例图:

image

我们执行这段代码后会发现,最后输出的内容是"我是outer作用域的name"

我们来看看这个过程发生了什么,首先看看输出name的地方:

function inner(){
    console.log(name);
}

我们之前说过,函数具有独立的作用域。我们这里要输出name,需要获取name的值,因此JS引擎会先去变量所在的作用域查找对应的值。

显然,这里name的作用域就是inner函数的作用域,而这里并没有定义name变量,那怎么办呢?

这里就要提到作用域的一个特点了:作用域嵌套

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

根据我们的示例图可以看出来,我们这里一共有三个作用域嵌套在一起,当引擎无法在inner的作用域中查询到name变量时,引擎会继续向他的外层作用域查找

到了inner作用域的外层outer的作用域时,在这里我们找到了一个声明的name我是outer作用域的name,于是乎就带着这个值心满意足的回去了,最后输出了这个值。要注意的是,由于已经在内部的作用域找到变量对应的值了,因此外部的同名变量的值就不会被访问到了,这就是"遮蔽效应"

提升

1. 变量提升

跟作用域息息相关的一个概念就是提升,我们先举个简单的小例子来看看它长什么模样:

a=2;
var a;

看着很别扭的代码,按照代码的书写顺序的话,应该是先为a赋值2,但此时应该并不存在a变量,事实上,这是由于js预编译导致的,可以这么概括预编译做的事情:

  • 找到所有的声明,并提升到所在作用域的顶部

因此,刚刚的代码实际上在执行时的顺序就是这样的:

var a;
a=2;

需要注意的是,预编译时只会把变量声明提前,而变量的赋值还是会按照原来的顺序执行,如:

console.log(a);//undefined
var a=2;

这段代码输出的aundefined,说明在执行console.log(a)时,a已经声明了,否则就会抛出ReferenceError了。这也说明了变量的声明确实提前了,但赋值并没有提前,这段代码实际运行的样子是这样的:

var a;
console.log(a);//undefined
a=2;

这样就清晰多了。

2. 函数提升

除了刚刚举例的变量提升,其实函数的声明也是存在提升的,我们看个例子:

f() //test
function f(){
    console.log('test');
}

可以看到,f()语句在声明之前,但最后成功输出了"test",说明函数的声明被提前了。但需要注意的是,通过函数表达式声明函数并不会提升。我们看个例子:

f() //Uncaught TypeError: f is not a function
var f = function(){
    console.log('test');
}

这个例子中,通过表达式的方式声明了函数f,在执行f()语句时报错,可以看出这里没有发生函数提升。其实函数表达式声明函数,可以看做是声明一个变量,这样就好理解了,也就是这样:

var f
f() //Uncaught TypeError: f is not a function
f = function(){
    console.log('test');
}

因为按照我们的说法,变量的声明会被提前,而赋值不会,所以就有了现在的结果。

3. 变量提升和函数提升

了解了变量提升和函数提升,我们做个尝试,当我们试图声明同名的函数和变量时会发生什么?

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

可以看到,最终输出a的结果是一个函数而不是undefined,这不仅说明函数提升的优先级大于变量提升,而且从a的值是个函数也可以看出,函数的声明和赋值其实是一个整体过程,而不是变量提升的只提升声明,不提升赋值。所以刚刚这段代码的真面目就是这样:

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

所以如果在刚刚的代码中再加入一句输出a的语句,就会发现a的值会被2覆盖掉。

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

补充提高

刚刚说的算是普通情况,我们再来看一个特殊情况下的例子:

if(function f(){}){
    console.log(f); //Uncaught ReferenceError: f is not defined
}

为什么在if语句中声明了函数f,但输出的时候却提示未定义?其实是因为,当在表达式中声明函数时,它不会视作函数声明,还是会作为表达式进行评估,评估大致做了这么些事情:

创建一个新的环境上下文,在这个环境中声明这个函数,然后返回这个函数对象。如果返回的对象没有被变量储存的话,这个新的环境上下文会失效,释放f函数对象。

这里的if()中的函数声明没有用变量存储,所以当执行到console.log(f)时,f已经被释放掉了,所以会报错。那我们试试用变量存储起来:

if(f=function(){}){
    console.log(f);  //ƒ (){}
}

完美收工!

总结

本文介绍了作用域的概念和JavaScript中的作用域、以及在作用域下的提升规则,并结合两者看了几个非(sang)常(xin)有(bing)趣(kuang)的例子,收获满满~

写在最后

都看到这里了,如果觉得对你有帮助的话不妨点个赞关注支持一下呗~

以后会陆续更新更多文章和知识点,感兴趣的话可以关注一波~

如果哪里有错误的地方或者描述不准确的地方,也欢迎大家指出交流~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352