声明

本系列文章内容全部梳理自以下几个来源:

作为一个前端小白,入门跟着这几个来源学习,感谢作者的分享,在其基础上,通过自己的理解,梳理出的知识点,或许有遗漏,或许有些理解是错误的,如有发现,欢迎指点下。

PS:梳理的内容以《JavaScript权威指南》这本书中的内容为主,因此接下去跟 JavaScript 语法相关的系列文章基本只介绍 ES5 标准规范的内容、ES6 等这系列梳理完再单独来讲讲。

正文-作用域

在 ES5 中,变量的作用域只有两类:

  • 全局作用域

  • 函数作用域

只要不是在函数内部定义的变量,作用域都是全局的,全局的变量在哪里都可以被访问到,即使跨 js 文件。

函数作用域是指在函数体定义的变量,不管有没有在函数体的开头定义,在函数体的任何地方都可以被使用,因为 JavaScript 中的变量有声明提前的行为。

函数作用域需要区别于 Java 语言中的块级作用域:

var i = 0;
function A() {
console.log(i); //输出undefined
for (var i = 0; i < 1; i++) {}
console.log(i); //输出1
}

在 Java 中,类似的代码,在 for 循环前后输出的 i 都会是 0,因为都会使用成员变量 i,for循环内定义的 i 由于块级作用域限制,只在for 循环的 {} 大括号中的代码有效。

但在 JavaScript 中,变量作用域只分函数作用域,而且变量有声明提前的特性,所以在函数体内部第一次输出 i 时,此时变量已经提前声明,但还没初始化,所以会是 undefined。而函数内定义的变量的作用域或者说生命周期是整个函数内,所以即使 for 循环体语句结束,仍旧可以访问到 i 变量。

由于允许变量的重复定义,所以全局变量很容易起冲突,因为无法确保多份 js 文件中是否已经在全局中定义了该变量,一旦起冲突,浏览器行为仅仅是将后定义的覆盖掉前定义的而已,这对于浏览器角度没什么大问题,但对于程序而已,很容易出现不可控的问题。而且,极难排查。

所以,实际编程中,建议不要过多的使用全局变量,有多种方法可以避免:

  • 使用一个全局对象来作为命名空间,将其余不在函数体内部定义的变量,作为该全局对象的属性来定义使用。
  • 使用一个立即执行的函数来作为临时命名空间,函数执行结束释放临时命名空间。
  • 如果临时命名空间内的部分变量需要供外部使用,一可以将这部分变量添加到作为命名空间的全局对象上的属性,二可以利用闭包的特性,返回一个新建的对象,为该对象添加一些接口可访问这部分变量。

全局对象作为命名空间

var DASU = {};
DASU.num = 1;
function a() {
console.log(DASU.num);
}

这里的全局对象意思是说,数据类型为对象的全局变量,简称全局对象,与前端里说的全局对象window是两个不同概念,区分一下。

其实也就是一种思想,将所有函数外需要定义的变量,都替换成对指定对象的属性来操作。

立即执行的函数作为临时命名空间

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

当引入 js 文件到 HTML 时,js 文件中的代码就会被执行,或者声明了 <script> 标签后,在标签内的代码也会立马被执行。但函数只有被调用的时候才会执行,所以,如果我们使用一个立即执行的函数,那这个函数体内部的代码行为就跟正常的 js 文件代码被执行的行为一致了。

而且,还可以利用函数内作用域这一特点,来保证,在这个立即执行的函数内部定义的变量不会影响到全局变量。

缺点就是函数内部代码执行结束后,这些在函数内定义的变量就被回收了。所以,如果有些信息需要跨 js 文件通信,此时要么通过全局对象方式,要么通过闭包特性来辅助实现。

临时命名空间内的变量共享方式

全局变量可以在任何地方被访问,所以可以将那些需要共享给外部使用的临时命名空间内的变量赋值给全局对象的属性,即结合第一种:全局对象做命名空间方式。

或者,通过闭包的特性,作为临时命名空间的立即执行的函数需要有一个返回值,当外部持有这个返回值时,这个函数内的变量就不会被回收。

然后,返回值可以是一个对象,公开一些接口来获取这些需要共享的变量,如:

var model = (function () {
var num = 1;
function a() {
console.log(num);
}
return {
getNum: function () {
return num;
}
}
}());
model.getNum(); //或者:
var model = (function () {
var num = 1;
function a() {
console.log(num);
}
return {
num:num
}
}());
model.num;

变量的声明提前原理

看个例子:

var i = 0;
function A() {
console.log(i); //输出undefined
for (var i = 0; i < 1; i++) {}
console.log(i); //输出1
}
A();

函数内第一个输出 undefined 是因为变量的声明提前,第二个输出 1 是因为变量作用域为函数作用域,而不是块级作用域。

那么,有想过,这些似乎理所当然的基础常识原理是什么吗?

我们先来看些理论,再结合理论返回来分析这个例子,但只分析变量的声明提前原理,至于作用域的原理留着作用域链一节分析。

理论

我们之前有介绍过执行上下文 EC,和变量对象 VO,执行上下文分全局执行上下文和函数执行上下文。在全局执行上下文中,VO 的具体表现是全局对象;在函数执行上下文中,VO 的具体表现是 AO,AO 存储着函数内的变量:形参、局部变量、函数自身引用、this、arguments。

不管是执行函数代码还是全局代码,js 解释器会分两个过程,有的文章翻译成:进入执行上下文阶段、执行代码阶段(我不怎么喜欢这个翻译)。

进入执行上下文阶段:其实本质上就是创建一个执行上下文,这个阶段会解析当前上下文内的代码,将声明的变量都保存到 VO 对象上。

执行代码阶段:就是代码实际运行期,当运行到相对应的变量的赋值语句时,就会将具体的属性值写入 VO 对象上保存的对应变量。

也就是说,在执行代码阶段,代码实际运行时,js 解释器已经解析了一遍上下文内的代码,并创建了执行上下文,且为其添加了一个 VO 属性,在 VO 对象上添加了上下文内声明的所有变量,这就是变量的声明提前行为。而之后函数体内对各变量的操作,其实是对 VO 上保存的变量进行操作了。

我看过一篇文章对这两个过程的翻译是:解析阶段、执行阶段。

我比较喜欢这种翻译,解析阶段主要的工作就是解析上下文内的代码,创建执行上下文,创建变量对象 VO 等,为执行阶段做准备;而执行阶段就是代码实际运行过程。

分析

var i = 0;
function A() {
console.log(i); //输出undefined
for (var i = 0; i < 1; i++) {}
console.log(i); //输出1
}
A();

再回过头来看这个简单的例子,假设这段代码放在一份单独的 js 文件中,解释器第一次执行这份代码,那么当执行全局代码时,首先进入全局执行上下文的解析阶段:

  1. 解析代码创建全局执行上下文
  2. 创建VO,并为其添加属性 i、A
  3. 省略该过程其他工作
  4. 将创建的全局EC放入ECS栈内

当实际开始执行第一行全局代码时,js解释器经过了解析阶段已经做了如上的工作,得到了一些基本的信息。之后便是执行全局代码,如果执行的代码是访问全局变量,那么直接读取全局 EC 中 VO 里的对应变量;如果是对全局变量赋值操作,那么写入全局 EC 中的 VO 里对应变量的属性值。

如果执行的代码是调用某个函数,此时就会为这个函数的执行创建一个函数执行上下文,那么这个过程同样需要两个阶段:解析阶段和执行阶段。

所以当代码执行到最后一行 A() 时,此时新的函数执行上下文的解析阶段做的工作:

  1. 解析 A() 函数内代码,并创建函数执行上下文 A函数EC
  2. 创建 AO,并为其添加属性
  3. 省略其他工作介绍
  4. 将创建的A函数EC放入ECS栈内

所以当执行函数 A 内的代码时,第一行输出才会输出 undefined,因为变量的声明提前特性在调用函数时创建函数执行上下文的过程中,已经解析了函数内的声明语句,并将这些变量添加到函数上下文 EC 的 AO 中了。

AO 就是变量对象 VO 在函数执行上下文中的具体表现。

而当执行完 for 循环语句,A 函数 EC 中的 AO 里的i属性已经被赋值为 1 了,而 A 函数 EC 是直到函数执行结束才销毁,所以即使在 for 语句内定义的 i 变量也可以在后面继续使用。

以上,就是变量声明提前的原理,当然,创建执行上下文的过程中,还涉及到其他很多工作,用来实现例如作用域链等机制,留待后续来说。


大家好,我是 dasu,欢迎关注我的公众号(dasuAndroidTv),公众号中有我的联系方式,欢迎有事没事来唠嗑一下,如果你觉得本篇内容有帮助到你,可以转载但记得要关注,要标明原文哦,谢谢支持~

前端入门17-JavaScript进阶之作用域的更多相关文章

  1. 前端基础之JavaScript进阶

    一.流程控制 if - else var a = 10; if (a >5){ console.log("yes"); }else { console.log("n ...

  2. 结合个人经历总结的前端入门方法 (转自https://github.com/qiu-deqing/FE-learning)

    结合个人经历总结的前端入门方法 (https://github.com/qiu-deqing/FE-learning),里面有很详细的介绍. 之前一直想学习前端的,都不知道怎么下手都一年了啥也没学到, ...

  3. 2019年Web前端入门的自学路线

    本文最初发表于博客园,并在GitHub上持续更新前端的系列文章.欢迎在GitHub上关注我,一起入门和进阶前端. 以下是正文.本文内容不定期更新. 我前几天写过一篇文章:<裸辞两个月,海投一个月 ...

  4. 4、JavaScript进阶篇①——基础语法

    一.认识JS 你知道吗,Web前端开发师需要掌握什么技术?也许你已经了解HTML标记(也称为结构),知道了CSS样式(也称为表示),会使用HTML+CSS创建一个漂亮的页面,但这还不够,它只是静态页面 ...

  5. JavaScript进阶(一)

     OK接下来,我们再次梳理一遍js并且提高一个等级. 众所周知,web前端开发者需要了解html和css,会只用html和css创建一个漂亮的页 面,但是这肯定是不够的,因为它只是一个静态的页面,我们 ...

  6. JavaScript进阶 - 第1章 系好安全带,准备启航

    第1章 系好安全带,准备启航 1-1让你认识JS 你知道吗,Web前端开发师需要掌握什么技术?也许你已经了解HTML标记(也称为结构),知道了CSS样式(也称为表示),会使用HTML+CSS创建一个漂 ...

  7. JavaScript进阶之高阶函数篇

    JavaScript进阶之高阶函数篇 简介:欢迎大家来到woo爷说前端:今天给你们带来的是JavaScript进阶的知识,接下来的系列都是围绕着JavaScript进阶进行阐述:首先我们第一篇讲的是高 ...

  8. 前端面试之JavaScript中数组的方法!【残缺版!!】

    前端面试之JavaScript中数组常用的方法 7 join Array.join()方法将数组中所有元素都转化为字符串并连接在-起,返回最后生成的字 符串.可以指定一个可选的字符串在生成的字符串中来 ...

  9. 前端html、Javascript、CSS技术小结

    简单地总结了一下前端用过的html.javascript.css技术,算是清点一下,做个大略的小结,为进一步的学习给个纲领. 一.HTML 由于HTML5的兴起,简单地判断一个网页是否是html5网页 ...

  10. javascript笔记:javascript的关键所在---作用域链

    javascript里的作用域是理解javascript语言的关键所在,正确使用作用域原理才能写出高效的javascript代码,很多javascript技巧也是围绕作用域进行的,今天我要总结一下关于 ...

随机推荐

  1. 学用HBuilder开发App的看过来

    自己的呕心沥血之作吧,花了一年时间,系统介绍HTML5 App开发的相关技术. 越来越多的公司采用HTML5来快速开发移动跨平台App,它支持当前市场流行的移动设备. 本书主要介绍了HTML5在移动A ...

  2. 记一次通过c#运用GraphQL调用Github api

    阅读目录 GraphQL是什么 .net下如何运用GraphQL 运用GraphQL调用Github api 结语 一.Graphql是什么 最近在折腾使用Github api做个微信小程序练练手,本 ...

  3. 【安富莱专题教程第3期】开发板搭建Web服务器,利用花生壳让电脑和手机可以外网远程监控

    说明:1.  开发板Web服务器的设计可以看我们之前发布的史诗级网络教程:链接.2.  需要复杂些的Web设计模板,可以使用我们V6开发板发布的综合Demo:链接.3.  教程中使用的是花生壳免费版, ...

  4. [Swift]LeetCode600. 不含连续1的非负整数 | Non-negative Integers without Consecutive Ones

    Given a positive integer n, find the number of non-negativeintegers less than or equal to n, whose b ...

  5. mysql+postgresql备份与恢复

    mysql备份一个库, mysqldump  -u用户名 -p密码 [选项] [数据库名] > /备份路径/备份文件名 mysqldump -uuser -p123123 auth > / ...

  6. java程序员的NodeJS初识篇

    摘要 作为一个一直用java来写后端的程序员用NodeJS来写后台,实在不是很爽.这里记下这两个月的NodeJS学习所遇之坑,与java转NodeJS的同仁共勉.学习时间不长,若有理解错误,望指正. ...

  7. Java8 LocalDateTime获取时间戳(毫秒/秒)、LocalDateTime与String互转、Date与LocalDateTime互转

    本文目前提供:LocalDateTime获取时间戳(毫秒/秒).LocalDateTime与String互转.Date与LocalDateTime互转 文中都使用的时区都是东8区,也就是北京时间.这是 ...

  8. scala中spark运行内存不足

    用 bash spark-submit 在spark上跑代码的时候出现错误: ERROR executor.Executor: Exception in task 9.0 in stage 416.0 ...

  9. Zara精讲C#.Cache、它和Redis区别是什么???

    前言:今天在博客园看到大佬在用Cache,非常不懂,原来它是搞缓存的,原来我只知道Redis是搞这个的,才知道有这个玩腻. 那它们的区别是什么呢?? 区别: redis是分布式缓存,是将数据随机分配到 ...

  10. Python爬虫入门教程 23-100 石家庄链家租房数据抓取

    1. 写在前面 作为一个活跃在京津冀地区的开发者,要闲着没事就看看石家庄这个国际化大都市的一些数据,这篇博客爬取了链家网的租房信息,爬取到的数据在后面的博客中可以作为一些数据分析的素材. 我们需要爬取 ...